diff --git a/icons/cursor.png b/icons/cursor.png new file mode 100644 index 0000000..9f1987d Binary files /dev/null and b/icons/cursor.png differ diff --git a/icons/filesave.png b/icons/filesave.png new file mode 100644 index 0000000..a4db07d Binary files /dev/null and b/icons/filesave.png differ diff --git a/icons/freeze.png b/icons/freeze.png new file mode 100644 index 0000000..8c31349 Binary files /dev/null and b/icons/freeze.png differ diff --git a/icons/home.png b/icons/home.png new file mode 100644 index 0000000..bed4ffd Binary files /dev/null and b/icons/home.png differ diff --git a/icons/move.png b/icons/move.png new file mode 100644 index 0000000..a42024d Binary files /dev/null and b/icons/move.png differ diff --git a/icons/select.png b/icons/select.png new file mode 100644 index 0000000..3f2d88b Binary files /dev/null and b/icons/select.png differ diff --git a/icons/zoom_to_rect.png b/icons/zoom_to_rect.png new file mode 100644 index 0000000..26ad414 Binary files /dev/null and b/icons/zoom_to_rect.png differ diff --git a/system/plots.py b/system/plots.py index 11e0de2..bc61973 100644 --- a/system/plots.py +++ b/system/plots.py @@ -20,7 +20,7 @@ import networkx import scipy # global active mode. Used by toolbars to communicate correct mode -active_mode = 'DEFAULT' +active_mode = 'default' class ObjectTable: """A 2D table of elements.""" @@ -245,35 +245,78 @@ class MainView (gtk.Notebook): self.emit('view-changed', vf) -class Plot (gtk.Frame): +class View (gtk.Frame): + """The base class of everything that is shown in the center view of fluents. + + Most views should rather subclass Plot, which automatically handles freezing and + toolbars, and sets up matplotlib Figure and Canvas objects. + """ + def __init__(self, title): gtk.Frame.__init__(self) self.title = title - self.sel_obj = None + self.set_shadow_type(gtk.SHADOW_NONE) + self.set_label(title) + + def get_toolbar(self): + return None + + +class EmptyView (View): + """EmptyView is shown in ViewFrames that are unused.""" + def __init__(self): + View.__init__(self, 'Empty view') + self.set_label(None) + label = gtk.Label('No view') + ebox = gtk.EventBox() + ebox.add(label) + self.add(ebox) + label.show() + ebox.show() + self.show() + + +class Plot (View): + def __init__(self, title): + View.__init__(self, title) + self.selection_listener = None self.fig = Figure() self.canvas = FigureCanvas(self.fig) - self.set_shadow_type(gtk.SHADOW_NONE) self._background = None - self._sel_sensitive = True + self._frozen = False + self._toolbar = PlotToolbar(self) self.canvas.add_events(gtk.gdk.ENTER_NOTIFY_MASK) + self.current_dim = None - def set_selection_sensitive(self,event): - if event: - if event.get_active(): - logger.log('debug','Selection freezed') - self._sel_sensitive = False - else: - logger.log('debug','Selections active') - self._sel_sensitive = True - + def set_frozen(self, frozen): + """A frozen plot will not be updated when the current selection is changed.""" + self._frozen = frozen + if not frozen: + self.set_current_selection(self._current_selection) def get_title(self): return self.title - def selection_changed(self, selection): - if not self._sel_sensitive or not self.get_property('visible'): + def get_toolbar(self): + return self._toolbar + + def selection_changed(self, dim_name, selection): + """ Selection observer handle. + + A selection change in a plot is only drawn if: + 1.) plot is sensitive to selections (not freezed) + 2.) plot is visible (has a view) + 3.) the selections dim_name is the plot's dimension. + """ + + if self._frozen \ + or not self.get_property('visible') \ + or self.current_dim != dim_name: return + else: + self._current_selection = selection + self.set_current_selection(selection) def set_selection_listener(self, listener): @@ -285,21 +328,7 @@ class Plot (gtk.Frame): """ self.selection_listener = listener - def get_toolbar(self): - return None - -class EmptyView (Plot): - def __init__(self): - Plot.__init__(self, 'Empty view') - label = gtk.Label('No view') - ebox = gtk.EventBox() - ebox.add(label) - self.add(ebox) - label.show() - ebox.show() - self.show() - class LineViewPlot(Plot): """Line view of current selection, no interaction Only works on 2d-arrays @@ -315,7 +344,7 @@ class LineViewPlot(Plot): self.dataset = dataset Plot.__init__(self, name) self.ax = self.fig.add_subplot(111) - self.ax.set_title(self.get_title()) + #self.ax.set_title(self.get_title()) self.current_dim = self.dataset.get_dim_name(major_axis) if len(self._data.shape)==2 and not minor_axis: minor_axis = major_axis-1 @@ -359,14 +388,9 @@ class LineViewPlot(Plot): self.canvas.show() #FIXME: Lineview plot cannot do selections -> disable in toolbar - self._toolbar = PlotToolbar(self.canvas,self) - self._toolbar.chk.connect ('toggled' , self.set_selection_sensitive) + self._toolbar = PlotToolbar(self) self.canvas.mpl_connect('resize_event', self.clear_background) - - def get_toolbar(self): - return self._toolbar - def clear_background(self, event): self._background = None @@ -386,27 +410,23 @@ class LineViewPlot(Plot): line_coll = LineCollection(segs, colors=(1,0,0,1)) line_coll.set_clip_box(self.ax.bbox) self.ax.update_datalim(line_coll.get_verts(self.ax.transData)) - self._toolbar.forward() + if self.use_blit: self.ax.draw_artist(line_coll) - #print "\nLine collection clip box:" line_coll.get_clip_box().get_bounds() - #print "\nLine collection bbox:" - #print self.ax.bbox.get_bounds() - #print "Background bbox:" - #print self._bbox.get_bounds() - #self.canvas.blit(self._bbox) self.canvas.blit() - #self.ax.draw_artist(line_coll) else: self.ax.add_collection(line_coll) self.canvas.draw() + class ScatterMarkerPlot(Plot): """The ScatterMarkerPlot is faster than regular scatterplot, but -has no color and size options.""" - def __init__(self, dataset_1, dataset_2, id_dim, sel_dim, id_1, id_2,s=6, name="Scatter plot"): + has no color and size options.""" + + def __init__(self, dataset_1, dataset_2, id_dim, sel_dim, + id_1, id_2, s=6, name="Scatter plot"): Plot.__init__(self, name) self.use_blit = False self._background = None @@ -422,19 +442,10 @@ has no color and size options.""" self.xaxis_data = dataset_1._array[:, x_index] self.yaxis_data = dataset_2._array[:, y_index] self.ax.plot(self.xaxis_data, self.yaxis_data, 'o', markeredgewidth=0, markersize=s) - self.ax.set_title(self.get_title()) + #self.ax.set_title(self.get_title()) self.add(self.canvas) self.canvas.show() - # add toolbar - self._toolbar = PlotToolbar(self.canvas, self) - self._toolbar.chk.connect ('toggled', self.set_selection_sensitive) - self._toolbar.set_property('show-arrow', False) - self._toolbar.set_select_callback(self.rectangle_select_callback) - - def get_toolbar(self): - return self._toolbar - def rectangle_select_callback(self, x1, y1, x2, y2): ydata = self.yaxis_data xdata = self.xaxis_data @@ -467,7 +478,7 @@ has no color and size options.""" self._selection_line, = self.ax.plot(xdata_new, ydata_new,marker='o', markersize=self.ms, linestyle=None, markerfacecolor='r') - self._toolbar.forward() #update data lims before draw +# self._toolbar.forward() #update data lims before draw if self.use_blit: self.ax.draw_artist(self._selection_line) self.canvas.blit() @@ -496,7 +507,7 @@ class ScatterPlot(Plot): self.fig.colorbar(sc,ticks=[], fraction=.05) self.ax.axhline(0, color='k', lw=1., zorder=1) self.ax.axvline(0, color='k', lw=1., zorder=1) - self.ax.set_title(self.get_title()) + #self.ax.set_title(self.get_title()) # collection self.coll = self.ax.collections[0] @@ -504,15 +515,6 @@ class ScatterPlot(Plot): self.add(self.canvas) self.canvas.show() - # create toolbar - self._toolbar = PlotToolbar(self.canvas, self) - self._toolbar.chk.connect ('toggled' , self.set_selection_sensitive) - self._toolbar.set_property('show-arrow', False) - self._toolbar.set_select_callback(self.rectangle_select_callback) - - def get_toolbar(self): - return self._toolbar - def rectangle_select_callback(self, x1, y1, x2, y2): ydata = self.yaxis_data xdata = self.xaxis_data @@ -532,7 +534,9 @@ class ScatterPlot(Plot): def set_current_selection(self, selection): ids = selection[self.current_dim] # current identifiers if len(ids)==0: + print "nothing selected" return + #self._toolbar.forward() #update data lims before draw index = self.dataset_1.get_indices(self.current_dim, ids) if self.use_blit: if self._background is None: @@ -542,8 +546,7 @@ class ScatterPlot(Plot): if len(index)>0: lw.put(2.,index) self.coll.set_linewidth(lw) - self._toolbar.forward() #update data lims before draw - + if self.use_blit: self.canvas.blit() self.ax.draw_artist(self.coll) @@ -558,6 +561,7 @@ class NetworkPlot(Plot): self.dataset = dataset self.keywords = kw self.dim_name = self.dataset.get_dim_name(0) + self.current_dim = self.dim_name if not kw.has_key('name'): kw['name'] = self.dataset.get_name() if not kw.has_key('prog'): @@ -604,17 +608,10 @@ class NetworkPlot(Plot): # Initial draw networkx.draw_networkx(self.graph, ax=self.ax, **kw) - # Setup toolbar - self._toolbar = PlotToolbar(self.canvas, self) - self._toolbar.chk.connect ('toggled' , self.set_selection_sensitive) - self._toolbar.set_property('show-arrow', False) - self._toolbar.set_select_callback(self.rectangle_select_callback) - def get_toolbar(self): return self._toolbar def rectangle_select_callback(self, x1, y1, x2, y2): - 'event1 and event2 are the press and release events' pos = self.keywords['pos'] ydata = scipy.zeros((len(pos),), 'l') xdata = scipy.zeros((len(pos),), 'l') @@ -635,10 +632,10 @@ class NetworkPlot(Plot): ids = [node_ids[i] for i in index] - self.selection_listener(self.dataset.get_dim_name(0), ids) + self.selection_listener(self.current_dim, ids) def set_current_selection(self, selection): - ids = selection[self.dataset.get_dim_name(0)] # current identifiers + ids = selection[self.current_dim] # current identifiers node_set = set(self.graph.nodes()) selected_nodes = list(ids.intersection(node_set)) @@ -668,293 +665,383 @@ class NetworkPlot(Plot): self.canvas.draw() -class PlotToolbar(NavigationToolbar2,gtk.Toolbar): - # list of toolitems to add to the toolbar, format is: - # text, tooltip_text, image_file, callback(str) - toolitems = ( - ('Home', 'Reset original view', 'home.png', 'home'), - #('Back', 'Back to previous view','back.png', 'back'), - #('Forward', 'Forward to next view','forward.png', 'forward'), - #('Subplots', 'Configure subplots','subplots.png', 'configure_subplots'), - ('Save', 'Save the figure','filesave.png', 'save_figure'), - ) +class PlotMode: + """A PlotMode object corresponds to a mouse mode in a plot. - radiobuttons = ( - ('DEFAULT', 'Default mode', 'cursor.png', 'default'), - ('PAN', 'Pan axes with left mouse, zoom with right', 'move.png', 'pan'), - ('ZOOM', 'Zoom to rectangle','zoom_to_rect.png', 'zoom'), - ('SELECT', 'Select within rectangle', 'select.png', 'select'), - ) + When a mode is selected in the toolbar, the PlotMode corresponding + to the toolbar button is activated by calling setup(ax) for the axis + system ax. + """ + def __init__(self, plot, name, tooltip, image_file): + self.name = name + self.tooltip = tooltip + self.image_file = image_file + self.plot = plot + self.canvas = plot.canvas - def __init__(self, canvas, plot): - self.win = None + def get_icon(self): + """Returns the icon for the PlotMode""" + fname = os.path.join(fluents.ICONDIR, self.image_file) + image = gtk.Image() + image.set_from_file(fname) + return image + + def activate(self): + """Subclasses of PlotMode should do their initialization here. + + The activate method is called when a mode is activated, and is + used primarily to set up callback functions on events in the + canvas. + """ + pass + + def deactivate(self): + """Subclasses of PlotMode should do their cleanup here. + + The deactivate method is primarily by subclasses of PlotMode to + remove any callbacks they might have on the matplotlib canvas. + """ + pass + + def _mpl_disconnect_all(self): + """Disconnects all matplotlib callbacks defined on the canvas. + + This is a hack because the RectangleSelector in matplotlib does + not store its callbacks, so we need a workaround to remove them. + """ + callbacks = self.plot.canvas.callbacks + + for callbackd in callbacks.values(): + for c in callbackd.keys(): + del callbackd[c] + + +class DefaultPlotMode (PlotMode): + def __init__(self, plot): + PlotMode.__init__(self, plot, 'default', 'Default mode', 'cursor.png') + + def activate(self): + for k, v in self.canvas.callbacks.items(): + print k, v + + +class PanPlotMode (PlotMode): + def __init__(self, plot): + PlotMode.__init__(self, plot, 'pan', + 'Pan axes with left mouse, zoom with right', + 'move.png') + + # Holds handler IDs for callbacks. + self._button_press = None + self._button_release = None + self._motion_notify = None + + self._button_pressed = None + + def activate(self): + self._button_press = self.canvas.mpl_connect( + 'button_press_event', self._on_button_press) + self._button_relese = self.canvas.mpl_connect( + 'button_release_event', self._on_button_release) + #self._drag = self.canvas.mpl_connect( + # 'mouse_drag_event', self._on_drag) + + def deactivate(self): + if self._button_press: + self.canvas.mpl_disconnect(self._button_press) + + if self._button_release: + self.canvas.mpl_disconnect(self._button_release) + + def _on_button_press(self, event): + + if event.button == 1: + self._button_pressed = 1 + elif event.button == 3: + self._button_pressed = 3 + else: + self._button_pressed=None + return + + x, y = event.x, event.y + + # push the current view to define home if stack is empty + # if self._views.empty(): self.push_current() + + self._xypress=[] + for i, a in enumerate(self.canvas.figure.get_axes()): + if x is not None and y is not None and a.in_axes(x, y) \ + and a.get_navigate(): + xmin, xmax = a.get_xlim() + ymin, ymax = a.get_ylim() + lim = xmin, xmax, ymin, ymax + self._xypress.append((x, y, a, i, lim,a.transData.deepcopy())) + self.canvas.mpl_disconnect(self._motion_notify) + + cid = self.canvas.mpl_connect('motion_notify_event', + self._on_motion_notify) + self._motion_notify = cid + + def _on_motion_notify(self, event): + """The drag callback in pan/zoom mode""" + + def format_deltas(event, dx, dy): + """Returns the correct dx and dy based on the modifier keys""" + if event.key=='control': + if(abs(dx)>abs(dy)): + dy = dx + else: + dx = dy + elif event.key=='x': + dy = 0 + elif event.key=='y': + dx = 0 + elif event.key=='shift': + if 2*abs(dx) < abs(dy): + dx=0 + elif 2*abs(dy) < abs(dx): + dy=0 + elif(abs(dx)>abs(dy)): + dy=dy/abs(dy)*abs(dx) + else: + dx=dx/abs(dx)*abs(dy) + return (dx,dy) + + for cur_xypress in self._xypress: + lastx, lasty, a, ind, lim, trans = cur_xypress + xmin, xmax, ymin, ymax = lim + + #safer to use the recorded button at the press than current button: + #multiple button can get pressed during motion... + if self._button_pressed==1: + lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) ) + x, y = trans.inverse_xy_tup( (event.x, event.y) ) + if a.get_xscale()=='log': + dx=1-lastx/x + else: + dx=x-lastx + if a.get_yscale()=='log': + dy=1-lasty/y + else: + dy=y-lasty + + dx,dy=format_deltas(event,dx,dy) + + if a.get_xscale()=='log': + xmin *= 1-dx + xmax *= 1-dx + else: + xmin -= dx + xmax -= dx + if a.get_yscale()=='log': + ymin *= 1-dy + ymax *= 1-dy + else: + ymin -= dy + ymax -= dy + + elif self._button_pressed==3: + try: + dx=(lastx-event.x)/float(a.bbox.width()) + dy=(lasty-event.y)/float(a.bbox.height()) + dx,dy=format_deltas(event,dx,dy) + if a.get_aspect() != 'auto': + dx = 0.5*(dx + dy) + dy = dx + alphax = pow(10.0,dx) + alphay = pow(10.0,dy) + lastx, lasty = trans.inverse_xy_tup( (lastx, lasty) ) + if a.get_xscale()=='log': + xmin = lastx*(xmin/lastx)**alphax + xmax = lastx*(xmax/lastx)**alphax + else: + xmin = lastx+alphax*(xmin-lastx) + xmax = lastx+alphax*(xmax-lastx) + if a.get_yscale()=='log': + ymin = lasty*(ymin/lasty)**alphay + ymax = lasty*(ymax/lasty)**alphay + else: + ymin = lasty+alphay*(ymin-lasty) + ymax = lasty+alphay*(ymax-lasty) + + except OverflowError: + warnings.warn('Overflow while panning') + return + + a.set_xlim(xmin, xmax) + a.set_ylim(ymin, ymax) + + self.canvas.draw() + + def _on_button_release(self, event): + 'the release mouse button callback in pan/zoom mode' + self.canvas.mpl_disconnect(self._motion_notify) + if not self._xypress: return + self._xypress = None + self._button_pressed=None + self.canvas.draw() + + +class ZoomPlotMode (PlotMode): + def __init__(self, plot): + PlotMode.__init__(self, plot, 'zoom', + 'Zoom to rectangle','zoom_to_rect.png') + self._selectors = {} + + def activate(self): + for ax in self.canvas.figure.get_axes(): + props = dict(facecolor = 'blue', + edgecolor = 'black', + alpha = 0.3, + fill = True) + + rs = RectangleSelector(ax, self._on_select, drawtype='box', + useblit=True, rectprops = props) + self.canvas.draw() + self._selectors[rs] = ax + + def deactivate(self): + self._mpl_disconnect_all() + self._selectors = {} + + def _on_select(self, start, end): + ax = start.inaxes + + ax.set_xlim((min(start.xdata, end.xdata), max(start.xdata, end.xdata))) + ax.set_ylim((min(start.ydata, end.ydata), max(start.ydata, end.ydata))) + self.canvas.draw() + + +class SelectPlotMode (PlotMode): + def __init__(self, plot): + PlotMode.__init__(self, plot, 'select', + 'Select within rectangle', 'select.png') + self._selectors = {} + + def activate(self): + for ax in self.canvas.figure.get_axes(): + props = dict(facecolor = 'blue', + edgecolor = 'black', + alpha = 0.3, + fill = True) + + rs = RectangleSelector(ax, self._on_select, drawtype='box', + useblit=True, rectprops = props) + self.canvas.draw() + self._selectors[rs] = ax + + def deactivate(self): + self._mpl_disconnect_all() + self._selectors = {} + + def _on_select(self, start, end): + self.plot.rectangle_select_callback(start.xdata, start.ydata, + end.xdata, end.ydata) + + +class PlotToolbar(gtk.Toolbar): + + def __init__(self, plot): gtk.Toolbar.__init__(self) - NavigationToolbar2.__init__(self, canvas) - self._idleId = 0 - self._select_callback = None - canvas.connect('enter-notify-event', self.on_enter_notify) - - def _init_toolbar(self): - self.set_style(gtk.TOOLBAR_ICONS) + self.plot = plot + self.canvas = plot.canvas + self._current_mode = None self.tooltips = gtk.Tooltips() - self._states = {} - self._selector = None + + ## Maps toolbar buttons to PlotMode objects. + self._mode_buttons = {} + self.set_property('show-arrow', False) - basedir = fluents.ICONDIR - - # setup base buttons - for text, tooltip_text, image_file, callback in self.toolitems: - if text is None: - self.insert(gtk.SeparatorToolItem(), -1 ) - continue - fname = os.path.join(basedir, image_file) - image = gtk.Image() - image.set_from_file(fname) - tbutton = gtk.ToolButton(image, text) - self.insert(tbutton, -1) - tbutton.connect('clicked', getattr(self, callback)) - tbutton.set_tooltip(self.tooltips, tooltip_text, 'Private') + + #canvas.connect('enter-notify-event', self.on_enter_notify) + self.show() + self.add_mode(DefaultPlotMode(self.plot)) + self.add_mode(PanPlotMode(self.plot)) + self.add_mode(ZoomPlotMode(self.plot)) + self.add_mode(SelectPlotMode(self.plot)) self.insert(gtk.SeparatorToolItem(), -1) - # mode/state buttons - rbutton = None - for text,tooltip_text,image_file,callback in self.radiobuttons: - if text is None: - self.insert(gtk.SeparatorToolItem(), -1 ) - continue - fname = os.path.join(basedir, image_file) - image = gtk.Image() - image.set_from_file(fname) - rbutton = gtk.RadioToolButton(rbutton) - rbutton.set_icon_widget(image) - rbutton.connect('toggled', getattr(self, callback)) - rbutton.set_tooltip(self.tooltips, tooltip_text, 'Private') - self._states[text] = rbutton - self.insert(rbutton, -1) + # Set up freeze button + btn = gtk.ToggleToolButton() - self.insert(gtk.SeparatorToolItem(), -1) - - toolitem = gtk.ToolItem() - self.insert(toolitem, -1) - self.message = gtk.Label() - toolitem.add(self.message) + fname = os.path.join(fluents.ICONDIR, "freeze.png") + image = gtk.Image() + image.set_from_file(fname) - self.tb_freeze = gtk.ToolItem() - self.chk = gtk.CheckButton () - self.chk.set_label ('Freeze') - self.tb_freeze.add (self.chk) - self.tb_freeze.set_tooltip(self.tooltips, 'Freeze current selection') - self.insert(self.tb_freeze,-1) - - toolitem = gtk.SeparatorToolItem() - self.insert(toolitem, -1) + btn.set_icon_widget(image) + btn.connect('toggled', self._on_freeze_toggle) + self.insert(btn, -1) self.show_all() - self.fileselect = FileChooserDialog(title='Save the figure', parent=self.win,) - + def add_mode(self, mode): + """Adds a new mode to the toolbar.""" - def on_enter_notify(self, widget, event): - if self._active != active_mode: - self.set_mode(active_mode) - - def set_mode(self, active): - # if state is unkown or not set, set to default - if active == None or active not in self._states.keys(): - active = 'DEFAULT' - - # remove current Selector: - if self._selector: - print "Removing selector" - self._selector = None - # remove current button bindings - if self._idPress != None: - self._idPress = self.canvas.mpl_disconnect(self._idPress) - self.mode = '' - - if self._idRelease != None: - self._idRelease = self.canvas.mpl_disconnect(self._idRelease) - self.mode = '' - - for state, button in self._states.items(): - if state != active: - continue - - if state == 'SELECT': - ax, = self.canvas.figure.get_axes() - props = dict(facecolor='blue', edgecolor = 'black', - alpha=0.3, fill=True) - self._selector = RectangleSelector(ax, self.onselect, - drawtype='box', useblit=True, - rectprops=props) - self.mode = 'Select rectangle mode' - print self.mode - - elif state == 'PAN': - self._idPress = self.canvas.mpl_connect( - 'button_press_event', self.press_pan) - self._idRelease = self.canvas.mpl_connect( - 'button_release_event', self.release_pan) - self.mode = 'pan/zoom mode' - - elif state == 'ZOOM': - self._idPress = self.canvas.mpl_connect('button_press_event', self.press_zoom) - self._idRelease = self.canvas.mpl_connect('button_release_event', self.release_zoom) - self.mode = 'Zoom to rect mode' - - elif state == 'DEFAULT': - pass - else: - pass - - if not button.get_active(): - button.set_active(True) - - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self._active) - - self.set_message(self.mode) - - self._active = active - # update global active_mode (for all toolbar) - globals()['active_mode'] = active - - def get_mode(self): - if self._active == None: - return 'DEFAULT' - return self._active - - def default(self, button): - """Activates default mode""" - if not button.get_active(): - return - self.set_mode('DEFAULT') - - def select(self, button): - """Activate select mode""" - if not button.get_active(): - return - self.set_mode('SELECT') - - def onselect(self,eclick, erelease): - 'eclick and erelease are matplotlib events at press and release' - if self._select_callback: - self._select_callback(eclick.xdata, eclick.ydata, erelease.xdata, erelease.ydata) - - def pan(self, button): - """Activate the pan/zoom tool. pan with left button, zoom with right""" - if not button.get_active(): - return - self.set_mode('PAN') - - def zoom(self, button): - """Activate zoom to rect mode""" - if not button.get_active(): - return - self.set_mode('ZOOM') - - def mouse_move(self, event): - """Extend NavigationToolbar2.mouse_move to provide selection support.""" - # Only update the rubberband for selection mode when mouse is - # within the plotting area. - - #if event.inaxes and self._active=='SELECT': - # if self._lastCursor != cursors.SELECT_REGION: - # self.set_cursor(cursors.SELECT_REGION) - # self._lastCursor = cursors.SELECT_REGION - # if self._xypress is not None: - # x, y = event.x, event.y - # lastx, lasty, a, ind, lim, trans= self._xypress - # self.draw_rubberband(event, x, y, lastx, lasty) - - NavigationToolbar2.mouse_move(self, event) - - def set_select_callback(self, listener): - """Allow plots to register a callback for selection events. - - The callback will be called as listener(x1, y1, x2, y2). All - coordinates are in the plot coordinate system, not pixels or - widget coordinates. - """ - self._select_callback = listener - - def set_cursor(self, cursor): - self.canvas.window.set_cursor(cursord[cursor]) - - def dynamic_update(self): - # legacy method; new method is canvas.draw_idle - self.canvas.draw_idle() - - def mpl_draw_rubberband(self,event): - """Use RectangleSelector for rubberband drawing""" - - def draw_rubberband(self, event, x0, y0, x1, y1): - 'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744' - drawable = self.canvas.window - if drawable is None: - return - - gc = drawable.new_gc() - - height = self.canvas.figure.bbox.height() - y1 = height - y1 - y0 = height - y0 - - w = abs(x1 - x0) - h = abs(y1 - y0) - rect = [int(val)for val in min(x0,x1), min(y0, y1), w, h] - try: lastrect, imageBack = self._imageBack - except AttributeError: - #snap image back - if event.inaxes is None: - return - - ax = event.inaxes - l,b,w,h = [int(val) for val in ax.bbox.get_bounds()] - b = int(height)-(b+h) - axrect = l,b,w,h - self._imageBack = axrect, drawable.get_image(*axrect) - drawable.draw_rectangle(gc, False, *rect) - self._idleId = 0 + if len(self._mode_buttons) > 0: + other = self._mode_buttons.keys()[0] else: - def idle_draw(*args): - drawable.draw_image(gc, imageBack, 0, 0, *lastrect) - drawable.draw_rectangle(gc, False, *rect) - self._idleId = 0 - return False - if self._idleId == 0: - self._idleId = gobject.idle_add(idle_draw) + other = None - def save_figure(self, button): - fname = self.fileselect.get_filename_from_user() - if fname: - self.canvas.print_figure(fname) + btn = gtk.RadioToolButton(other) + btn.set_icon_widget(mode.get_icon()) + btn.set_tooltip(self.tooltips, mode.tooltip, 'Private') + btn.connect('toggled', self._on_mode_toggle) - def configure_subplots(self, button): - toolfig = Figure(figsize=(6,3)) - canvas = self._get_canvas(toolfig) - toolfig.subplots_adjust(top=0.9) - tool = SubplotTool(self.canvas.figure, toolfig) + self._mode_buttons[btn] = mode + self.insert(btn, -1) - w = int (toolfig.bbox.width()) - h = int (toolfig.bbox.height()) + if self._current_mode == None: + self._current_mode = mode - - window = gtk.Window() - window.set_title("Subplot Configuration Tool") - window.set_default_size(w, h) - vbox = gtk.VBox() - window.add(vbox) - vbox.show() - - canvas.show() - vbox.pack_start(canvas, True, True) - window.show() - - def _get_canvas(self, fig): - return FigureCanvas(fig) + def get_mode(self): + """Returns the active mode name.""" + if self._current_mode: + return self._current_mode.name + return None + def get_mode_by_name(self, mode_name): + """Returns the mode with the given name or None.""" + for m in self._mode_buttons.values(): + if m.name == mode_name: + return m + return None + + def get_button(self, mode_name): + """Returns the button that corresponds to a mode name.""" + for b, m in self._mode_buttons.items(): + if m.name == mode_name: + return b + return None + + def set_mode(self, mode_name): + """Sets a mode by name. Returns the mode or None""" + if mode_name == self._current_mode.name: + return None + + if self._current_mode: + self._current_mode.deactivate() + + new_mode = self.get_mode_by_name(mode_name) + if new_mode: + new_mode.activate() + self._current_mode = self.get_mode_by_name(mode_name) + else: + logger.log('warning', 'No such mode: %s' % mode_name) + + if self.get_button(mode_name) and \ + not self.get_button(mode_name).get_active(): + self.get_button(mode_name).set_active(True) + return self._current_mode + + + def _on_mode_toggle(self, button): + if button.get_active(): + self.set_mode(self._mode_buttons[button].name) + + def _on_freeze_toggle(self, button): + self.plot.set_frozen(button.get_active()) + # Create a view-changed signal that should be emitted every time # the active view changes.