def __init__(self, app, project, plot):
        gtk.VBox.__init__(self)

        self._current_selector = None
        
        self.app = app

        self._construct_actiongroups()        
        self.statusbar = self._construct_statusbar()
        self.coords = self._construct_coords()

        self.btn_cancel = self._construct_cancel_button()

        self.context_id = self.statusbar.get_context_id("coordinates")
        #self.statusbar.push(self.context_id, "X: 10, Y: 20")
        #self.statusbar.pop(self.context_id)
        
        vbox = self.vbox = gtk.VBox()
        hbox = gtk.HBox()
        hbox.pack_start(self.btn_cancel, False, padding=4)
        hbox.pack_start(self.coords, False, padding=4)
        hbox.pack_start(self.statusbar, padding=4)
        hbox.show()
        vbox.pack_end(hbox, False, True)
        vbox.show()

        self.add(vbox)

        # set up project/plot
        self.project = project        
        self.plot = None
        self.backend = None
        self.cursor = None

        self._signals = []
        self.cblist = []

        self.set_plot(plot)
        self.project.sig_connect("close", (lambda sender: self.destroy()))

        # set up file selector for export dialog
        # TODO: this could be put into a plugin, since it is desirable to have
        # TODO: such a method in the shell frontend as well.
        self.fileselect = FileChooserDialog(title='Save the figure', parent=None)
Esempio n. 2
0
    def __init__(self, project, plot):

        basewidget.BaseWidget.__init__(self, project)

        self._current_selector = None
        
        # coords
        self.coords = gtk.Label()
        self.coords.show()

        self.context_id = self.get_statusbar().get_context_id("coordinates")
        
        vbox = self.vbox = gtk.VBox()
        hbox = gtk.HBox()
        ##hbox.pack_start(self.btn_cancel, False, padding=4)
        hbox.pack_start(self.coords, False, padding=4)
        hbox.show()
        vbox.pack_end(hbox, False, True)
        vbox.show()

        self.add(vbox)

        # set up project/plot
        self.project = project        
        self.plot = None
        self.backend = None
        self.cursor = None

        self._signals = []
        self.cblist = []

        self.set_plot(plot)
        self.project.sig_connect("close", (lambda sender: self.destroy()))

        # set up file selector for export dialog
        # TODO: this could be put into a plugin, since it is desirable to have
        # TODO: such a method in the shell frontend as well.
        self.fileselect = FileChooserDialog(title='Save the figure', parent=None)
class MatplotlibWidget(gtk.VBox):


    __gsignals__ = {
        'edit-mode-started' : (gobject.SIGNAL_RUN_FIRST , gobject.TYPE_NONE, ()),
        'edit-mode-ended' : (gobject.SIGNAL_RUN_FIRST , gobject.TYPE_NONE, ())        
        }

    
    actions_dict = {
        'Plot':
        [
        ('PlotMenu', None, '_Plot'),
        ('Replot', 'sloppy-replot', '_Replot', '<control>R', 'Replot', '_cb_replot'),
        ('Edit', gtk.STOCK_PROPERTIES, '_Edit', '<control>E', 'Edit', '_cb_edit'),
        ('ExportViaMPL', gtk.STOCK_SAVE_AS, 'Export via matplotlib...', None, 'Export via Matplotlib', 'on_export_via_matplotlib'),
        ],
        'Analysis':
        [
        ('AnalysisMenu', None, '_Analysis')
        ],
        'Display':
        [
        ('DisplayMenu', None, '_Display'),
        ('ZoomIn', gtk.STOCK_ZOOM_IN, '_Zoom In', 'plus', 'Zoom', '_cb_zoom_in'),
        ('ZoomOut', gtk.STOCK_ZOOM_OUT, '_Zoom Out', 'minus', 'Zoom', '_cb_zoom_out'),
        ('ZoomFit', gtk.STOCK_ZOOM_FIT, '_Zoom Fit', '0', 'Zoom', '_cb_zoom_fit'),
        ('ZoomRect', gtk.STOCK_ZOOM_IN, '_Zoom Rectangle', 'r', 'Zoom', '_cb_zoom_rect'),
        ('ToggleLogScale', None, 'Toggle Logarithmic Scale', 'l', 'Toggle Logscale', '_cb_toggle_logscale'),
        ('MovePlot', None, 'Move Plot', 'm', '', '_cb_move_plot'),
        ('DataCursor', None, 'Data Cursor', 'c', '', '_cb_data_cursor'),
        ('SelectLine', None, 'Select Line', 's', '', '_cb_select_line'),
        ('ZoomAxes', None, 'Zoom Axes', 'z', '', '_cb_zoom_axes')
        ],
        }

    uistring = """
    <ui>    
      <menubar name='MainMenu'>      
        <menu action='PlotMenu'>
          <placeholder name='PlotMenuActions'>
            <menuitem action='Replot'/>
            <menuitem action='Edit'/>
            <separator/>
            <menuitem action='ExportViaMPL'/>
          </placeholder>
        </menu>        
        <menu action='AnalysisMenu'>
          <menuitem action='DataCursor'/>
        </menu>        
        <menu action='DisplayMenu'>
          <menuitem action='ToggleLogScale'/>
          <separator/>
          <menuitem action='ZoomRect'/>
          <menuitem action='ZoomIn'/>
          <menuitem action='ZoomOut'/>
          <menuitem action='ZoomFit'/>
          <menuitem action='ZoomAxes'/>          
          <separator/>
          <menuitem action='MovePlot'/>
        </menu>        
      </menubar>      
      <toolbar name='MainToolbar'>
        <placeholder name='MainToolbarEdit'>
        <toolitem action='Edit'/>
        </placeholder>
        <toolitem action='ZoomIn'/>
        <toolitem action='ZoomFit'/>
        <separator/>              
        <toolitem action='Replot'/>
      </toolbar>
    </ui>
    """



    def __init__(self, app, project, plot):
        gtk.VBox.__init__(self)

        self._current_selector = None
        
        self.app = app

        self._construct_actiongroups()        
        self.statusbar = self._construct_statusbar()
        self.coords = self._construct_coords()

        self.btn_cancel = self._construct_cancel_button()

        self.context_id = self.statusbar.get_context_id("coordinates")
        #self.statusbar.push(self.context_id, "X: 10, Y: 20")
        #self.statusbar.pop(self.context_id)
        
        vbox = self.vbox = gtk.VBox()
        hbox = gtk.HBox()
        hbox.pack_start(self.btn_cancel, False, padding=4)
        hbox.pack_start(self.coords, False, padding=4)
        hbox.pack_start(self.statusbar, padding=4)
        hbox.show()
        vbox.pack_end(hbox, False, True)
        vbox.show()

        self.add(vbox)

        # set up project/plot
        self.project = project        
        self.plot = None
        self.backend = None
        self.cursor = None
        self._signals = []

        self.set_plot(plot)
        Signals.connect(self.project, "close", (lambda sender: self.destroy()))

        # set up file selector for export dialog
        # TODO: this could be put into a plugin, since it is desirable to have
        # TODO: such a method in the shell frontend as well.
        self.fileselect = FileChooserDialog(title='Save the figure', parent=None)



    def _construct_actiongroups(self):
        actiongroups = list()
        for key, actions in self.actions_dict.iteritems():
            ag = gtk.ActionGroup(key)
            ag.add_actions( uihelper.map_actions(actions, self) )
            actiongroups.append(ag)
        self.actiongroups = actiongroups

    def get_actiongroups(self):
        return self.actiongroups

    def get_uistring(self):
        return self.uistring




    def _construct_statusbar(self):
        statusbar = gtk.Statusbar()
        statusbar.show()
        return statusbar

    def _construct_coords(self):
        label = gtk.Label()
        label.show()
        return label 

    def _construct_cancel_button(self):
        button = gtk.Button(stock=gtk.STOCK_CANCEL)
        button.set_sensitive(False)
        button.show()
        return button
        
        

    #----------------------------------------------------------------------
    def set_coords(self, x, y):
        if x is not None and y is not None:
            self.coords.set_text("X: %1.2f, Y: %1.2f" % (x,y))
        else:
            self.coords.set_text("invalid coordinates")
        
    def set_plot(self, plot):
        # TODO: remove old plot
        # TODO: connect to plot's title    

        if plot is not None:
            backend = self.project.request_backend('matplotlib', plot=plot)

            #backend.canvas.set_size_request(800, 600)
            sw = uihelper.add_scrollbars(backend.canvas, viewport=True)
            sw.show()
            self.vbox.pack_start(sw)
        else:
            backend = None
           
        # disconnect old stuff
        if self.backend is not None and self.backend != backend:
            self.backend.disconnect()

        for signal in self._signals:
            Signals.disconnect(signal)
        self._signals = []
        
        if self.cursor is not None:
            self.cursor.finish()

        # connect new backend
        self.plot = plot
        self.backend = backend


        if backend is not None:
            self._signals.extend(
                [Signals.connect(plot, "plot-changed", (lambda sender: backend.draw())),
                 Signals.connect(plot, "closed", (lambda sender: Signals.emit(self, 'closed')))]
                )
            try:
                backend.draw()
            except:
                #gtkutils.info_msg("Nothing to plot.")
                raise            

            # Cursor
            self.cursor = mpl_selector.Cursor(self.backend.figure)
            Signals.connect(self.cursor, "move",
                            (lambda sender,x,y: self.set_coords(x,y)))
            self.cursor.init()

        
    #----------------------------------------------------------------------
    def _cb_replot(self, action):
        self.backend.draw()

    def _cb_edit(self, action):
        self.app.edit_layer( self.plot, self.plot.layers[0] )        

    #----------------------------------------------------------------------

    def zoom_to_region(self, layer, region, undolist=[]):
       
        ul = UndoList()

        x0 = min( region[0], region[2] )
        x1 = max( region[0], region[2] )
            
        y0 = min( region[1], region[3] )
        y1 = max( region[1], region[3] )

        # Do not zoom if x0 == x1 or if y0 == y1, because
        # otherwise matplotlib will mess up.  Of course, if x0 and
        # x1 are None (or y0 and y1), i.e. if autoscale is turned on,
        # then it is ok to zoom.
        if ((x0 is not None) and (x0 == x1)) or \
           ((y0 is not None) and (y0 == y1)):            
            ul.append( NullUndo() )
            return          

        def set_axis(axis, start, end):
            if axis.start is not None and axis.end is not None:
                swap_axes = axis.start > axis.end
            else:
                swap_axes = False

            if swap_axes is True:
                _start, _end = end, start
            else:
                _start, _end = start, end
                
            uwrap.set(axis, start=_start, end=_end, undolist=ul)

        set_axis(layer.xaxis, x0, x1)
        set_axis(layer.yaxis, y0, y1)
        
        uwrap.emit_last( self.plot, "plot-changed", undolist=ul )
        
        undolist.append(ul)

    def axes_from_xy(self, x, y):
        " x,y should be plot coordinates, not screen coordinates. "
        for layer in self.plot.layers:
            axes = self.backend.layer_to_axes[layer]
            if axes.bbox.contains(x,y) == 1:
                return axes
        else:
            return None

    # might be used in ZoomSelector as well
    # => either static method or put it somewhere else
    def calculate_zoom_region(self, axes, dx=0.1, dy=0.1):
        xmin, xmax = axes.get_xlim()
        width = (xmax-xmin)
        if axes.get_xscale() == 'log':
            alphax = pow(10.0, dx)
            xmin *= alphax
            xmax /= alphax
        else: # linear
            xmin += width*dx
            xmax -= width*dx

        ymin, ymax = axes.get_ylim()
        height = ymax-ymin
        if axes.get_yscale() == 'log':
            alphay = pow(10.0, dy)
            ymin *= alphay
            ymax /= alphay
        else: # linear
            ymin += height*dy
            ymax -= height*dy

        return (xmin, ymin, xmax, ymax)


    #------------------------------------------------------------------------------
        
    def _cb_zoom_rect(self, action):

        def finish_zooming(sender):
            self.statusbar.pop(
                self.statusbar.get_context_id('action-zoom'))

            ul = UndoList().describe("Zoom Region")
            layer = self.backend.axes_to_layer[sender.axes]
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)           
        
        s = mpl_selector.SelectRegion(self.backend.figure)
        Signals.connect(s, 'finished', finish_zooming)
        self.statusbar.push(
            self.statusbar.get_context_id('action-zoom'),
            "Use the left mouse button to zoom.")

        self.select(s)


    def _cb_zoom_fit(self, action):
        self.abort_selection()


        x,y,state = self.backend.canvas.window.get_pointer()
        y = self.backend.canvas.figure.bbox.height() - y
        axes = self.axes_from_xy(x,y)
        
        if axes is not None:
            layer = self.backend.axes_to_layer[axes]            
            region = (None,None,None,None)
            self.zoom_to_region(layer, region, undolist=self.app.project.journal)


    def _cb_zoom_in(self, action):
        self.abort_selection()

        x,y,state = self.backend.canvas.window.get_pointer()
        y = self.backend.canvas.figure.bbox.height() - y
        axes = self.axes_from_xy(x,y)
        
        if axes is not None:
            layer = self.backend.axes_to_layer[axes]            
            region = self.calculate_zoom_region(axes)
            self.zoom_to_region(layer, region, undolist=self.app.project.journal)
        
        
    def _cb_zoom_out(self, action):
        self.abort_selection()

        x,y,state = self.backend.canvas.window.get_pointer()
        y = self.backend.canvas.figure.bbox.height() - y
        axes = self.axes_from_xy(x,y)
        
        if axes is not None:
            layer = self.backend.axes_to_layer[axes]
            region = self.calculate_zoom_region(axes, dx=-0.1, dy=-0.1)
            self.zoom_to_region(layer, region, undolist=self.app.project.journal)

              

    def _cb_toggle_logscale(self, action):
        self.abort_selection()
        
        p = self.app.plugins['Default']
        p.toggle_logscale_y(self.plot, self.plot.layers[0])

        

    def _cb_move_plot(self, action):

        def finish_moving(sender):
            ul = UndoList().describe("Move Graph")
            layer = self.backend.axes_to_layer[sender.axes]
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)           
           
        s = mpl_selector.MoveAxes(self.backend.figure)        
        Signals.connect(s, "finished", finish_moving)

        self.select(s)
        


    def _cb_data_cursor(self, action):

        s = mpl_selector.DataCursor(self.backend.figure)

        def abort_selector(sender, context_id):
            self.statusbar.pop(context_id)
            
        def finish_selector(sender, context_id):
            self.statusbar.pop(context_id)
            xvalue, yvalue = sender.point

        def update_position(sender, context_id, line, index, point):
            # Note that 'line' is a Line2d instance from matplotlib!
            x, y = point
            self.statusbar.pop(context_id)
            self.statusbar.push(context_id, "[%4d] %f,%f" % (index, x, y))

        context_id = self.statusbar.get_context_id("data_cursor")
        Signals.connect(s, "update-position", update_position, context_id)
        Signals.connect(s, "finished", finish_selector, context_id)
        Signals.connect(s, "aborted", abort_selector, context_id)
        
        self.select(s)


    def _cb_select_line(self, action):
            
        def finish_select_line(sender):
            print "FINISHED SELECT LINE", sender.line

        s = mpl_selector.SelectLine(self.backend.figure,mode=mpl_selector.SELECTLINE_VERTICAL)
        Signals.connect(s, "finished", finish_select_line)
        
        self.select(s)


    def _cb_zoom_axes(self, action):

        def finish_moving(sender):
            ul = UndoList().describe("Zoom")
            layer = self.backend.axes_to_layer[sender.axes]
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)           
           
        s = mpl_selector.ZoomAxes(self.backend.figure)
        Signals.connect(s, "finished", finish_moving)

        self.select(s)
        
        
    #----------------------------------------------------------------------
    def abort_selection(self):
        if self._current_selector is not None:
            self._current_selector.abort()
            self._current_selector = None

        for ag in self.get_actiongroups():
            ag.set_sensitive(True)

        self.btn_cancel.set_sensitive(False)


    def select(self, selector):

        self.emit("edit-mode-started")

        self._current_selector = None
        
        def on_finish(sender):
            # Be careful not to call self.abort_selection() in this place.
            self._current_selector = None
            self.btn_cancel.set_sensitive(False)
            self.emit("edit-mode-ended")

        self.btn_cancel.set_sensitive(True)
        self.btn_cancel.connect("clicked", (lambda sender: self.abort_selection()))
        
        Signals.connect(selector, "finished", on_finish)
        Signals.connect(selector, "aborted", on_finish)
        self._current_selector = selector
        selector.init()


   #----------------------------------------------------------------------
   # other callbacks

   
    def on_export_via_matplotlib(self, action):
        self.abort_selection()

        # TODO: pick filename based on self.get_plot().key
        fname = self.fileselect.get_filename_from_user()
        if fname is not None:
            self.backend.canvas.print_figure(fname)
Esempio n. 4
0
class MatplotlibWidget(basewidget.BaseWidget):

    __gsignals__ = {
        'edit-mode-started' : (gobject.SIGNAL_RUN_FIRST , gobject.TYPE_NONE, ()),
        'edit-mode-ended' : (gobject.SIGNAL_RUN_FIRST , gobject.TYPE_NONE, ())        
        }

    
    actions_dict = {
        'File':
        [
        ('CloseTab', gtk.STOCK_CLOSE, 'Close Tab', None, '', 'on_action_CloseTab')
        ],
        'Plot':
        [
        ('PlotMenu', None, '_Plot'),
        ('Replot', 'sloppy-replot', '_Replot', '<control>R', 'Replot', 'on_action_Replot'),
        ('ExportViaMPL', gtk.STOCK_SAVE_AS, 'Export via matplotlib...', None, 'Export via Matplotlib', 'on_action_ExportViaMPL'),
        ('ExportViaGnuplot', gtk.STOCK_SAVE_AS, 'Export via gnuplot...', None, 'Export via Gnuplot', 'on_action_ExportViaGnuplot'),
        ],
        'Analysis':
        [
        ('AnalysisMenu', None, '_Analysis')
        ],
        'Display':
        [
        ('DisplayMenu', None, '_Display'),
        ('ZoomIn', gtk.STOCK_ZOOM_IN, '_Zoom In', '<control>plus', 'Zoom', 'on_action_ZoomIn'),
        ('ZoomOut', gtk.STOCK_ZOOM_OUT, '_Zoom Out', '<control>minus', 'Zoom', 'on_action_ZoomOut'),
        ('ZoomFit', gtk.STOCK_ZOOM_FIT, '_Zoom Fit', '<control>0', 'Zoom', 'on_action_ZoomFit'),
        ('ZoomRect', gtk.STOCK_ZOOM_FIT, '_Zoom Rectangle', '<control>r', 'Zoom', 'on_action_ZoomRect'),
        ('MoveAxes', None, 'Move Plot', '<control>m', '', 'on_action_MoveAxes'),
        ('DataCursor', None, 'Data Cursor', '<control>c', '', 'on_action_DataCursor'),
        ('SelectLine', None, 'Select Line', None, '', 'on_action_SelectLine'),
        ('ZoomAxes', None, 'Zoom Axes', None, '', 'on_action_ZoomAxes')
        ]
        }


    def __init__(self, project, plot):

        basewidget.BaseWidget.__init__(self, project)

        self._current_selector = None
        
        # coords
        self.coords = gtk.Label()
        self.coords.show()

        self.context_id = self.get_statusbar().get_context_id("coordinates")
        
        vbox = self.vbox = gtk.VBox()
        hbox = gtk.HBox()
        ##hbox.pack_start(self.btn_cancel, False, padding=4)
        hbox.pack_start(self.coords, False, padding=4)
        hbox.show()
        vbox.pack_end(hbox, False, True)
        vbox.show()

        self.add(vbox)

        # set up project/plot
        self.project = project        
        self.plot = None
        self.backend = None
        self.cursor = None

        self._signals = []
        self.cblist = []

        self.set_plot(plot)
        self.project.sig_connect("close", (lambda sender: self.destroy()))

        # set up file selector for export dialog
        # TODO: this could be put into a plugin, since it is desirable to have
        # TODO: such a method in the shell frontend as well.
        self.fileselect = FileChooserDialog(title='Save the figure', parent=None)


    def get_uistring(self):
        return globals.app.get_uistring('plot-widget')


    def get_title(self):
        if self.plot is None:
            return "(no plot)"
        else:
            return "P: %s" % self.plot.key
        
    
    #----------------------------------------------------------------------
    def set_coords(self, x, y):
        if x is not None and y is not None:
            self.coords.set_text("X: %1.2f, Y: %1.2f" % (x,y))
        else:
            self.coords.set_text("invalid coordinates")
        
    def set_plot(self, plot):
        # TODO: remove old plot
        # TODO: connect to plot's title    

        if plot is not None:
            backend = self.project.request_backend('matplotlib', plot=plot)
            
            # TODO: set canvas size depending on outer settings, on dpi
            # TODO: and zoom level

            # minium canvas size
            canvas_width, canvas_height = 800,600
            zoom = 0.5
            canvas_width *= zoom
            canvas_height *= zoom
            # TODO: set_size_request only sets a minimum size
            # TODO: maybe have a look in dpi and figsize arguments
            # TODO: to matplotlib backend.
            backend.canvas.resize(canvas_width, canvas_height)
            #backend.canvas.set_size_request(canvas_width, canvas_height)
            backend.figure.set_figsize_inches( 2,3 )
            backend.figure.set_dpi(100)
            
            # set up object picking
            # DISABLED
            #backend.canvas.mpl_connect('button_press_event', self.button_press_event)

            if True:
                # add rulers
                hruler = gtk.HRuler()
                hruler.set_metric(gtk.PIXELS)
                hruler.set_range(0, canvas_width, 0, canvas_width)

                vruler = gtk.VRuler()
                vruler.set_metric(gtk.PIXELS) # gtk.INCHES, gtk.CENTIMETERS
                vruler.set_range(0, canvas_height, 0, canvas_height)

                # motion notification
                def motion_notify(ruler, event):
                    return ruler.emit("motion_notify_event", event)
                #backend.canvas.connect_object("motion_notify_event", motion_notify, ruler)

                # put scrollbars around canvas
                scrolled_window = gtk.ScrolledWindow()
                scrolled_window.add_with_viewport(backend.canvas)
            
                # the layout is done using a table
                layout = gtk.Table(rows=3, columns=2)
                layout.attach(hruler, 1, 2, 0, 1, gtk.EXPAND|gtk.SHRINK|gtk.FILL, gtk.FILL)
                layout.attach(vruler, 0, 1, 1, 2, gtk.FILL, gtk.EXPAND|gtk.SHRINK|gtk.FILL)
                layout.attach(scrolled_window, 1, 2, 1, 2, gtk.EXPAND|gtk.FILL, gtk.EXPAND|gtk.FILL)
            else:
                layout = backend.canvas
                
            layout.show_all()
            self.vbox.pack_start(layout)
            
            # add scrollbar
            #sw = uihelper.add_scrollbars(backend.canvas, viewport=True)
            #sw.show_all()            
            #self.vbox.pack_start(sw)
        else:
            backend = None
           
        # disconnect old stuff
        if self.backend is not None and self.backend != backend:
            self.backend.disconnect()

        for cb in self.cblist:
            cb.disconnect()
        self.cblist = []
        
        if self.cursor is not None:
            self.cursor.finish()

        # connect new backend
        self.plot = plot
        self.backend = backend


        if backend is not None:
            self.cblist += [
                plot.sig_connect("changed", (lambda sender: backend.draw())),
                plot.sig_connect("closed", (lambda sender: self.destroy())),
                backend.sig_connect("closed", (lambda sender: self.destroy()))
                ]
            
            try:
                backend.draw()
            except:
                raise            

            # Cursor
            self.cursor = mpl_selector.Cursor(self.backend.figure)
            self.cursor.sig_connect("move", (lambda sender,x,y: self.set_coords(x,y)))
            self.cursor.init()

            self.project.active_object = self.plot


    def request_active_layer(self):
        layer = self.backend.request_active_layer()
        if layer is not None:
            return layer
        else:
            globals.app.err_msg("No active layer!")
            raise UserCancel
    
        
    #----------------------------------------------------------------------
    def on_action_Replot(self, action):
        self.backend.redraw(force=True)
        
    #----------------------------------------------------------------------

    def zoom_to_region(self, layer, region, undolist=[]):

        old_region = (layer.xaxis.start,
                      layer.yaxis.start,
                      layer.xaxis.end,
                      layer.yaxis.end)
        
        x0 = min( region[0], region[2] )
        x1 = max( region[0], region[2] )            
        y0 = min( region[1], region[3] )
        y1 = max( region[1], region[3] )

        # Do not zoom if x0 == x1 or if y0 == y1, because
        # otherwise matplotlib will mess up.  Of course, if x0 and
        # x1 are None (or y0 and y1), i.e. if autoscale is turned on,
        # then it is ok to zoom.
        if ((x0 is not None) and (x0 == x1)) or \
           ((y0 is not None) and (y0 == y1)):            
            undolist.append(NullUndo())
            return

        def set_axis(axis, start, end):
            if axis.start is not None and axis.end is not None:
                swap_axes = axis.start > axis.end
            else:
                swap_axes = False

            if swap_axes is True:
                _start, _end = end, start
            else:
                _start, _end = start, end

            print "-------"
            axis.set(start=_start, end=_end)
            print "-------"            

        self.backend.block_redraw()
        set_axis(layer.xaxis, x0, x1)
        set_axis(layer.yaxis, y0, y1)
        self.backend.unblock_redraw()

        ul = UndoList("Zoom Region")        
        ul.append(UndoInfo(self.zoom_to_region, layer, old_region))
        undolist.append(ul)

    def axes_from_xy(self, x, y):
        " x,y should be plot coordinates, not screen coordinates. "
        for layer in self.plot.layers:
            axes = self.backend.layer_to_axes[layer]
            if axes.bbox.contains(x,y) == 1:
                return axes
        else:
            return None

    # might be used in ZoomSelector as well
    # => either static method or put it somewhere else
    def calculate_zoom_region(self, axes, dx=0.1, dy=0.1):
        xmin, xmax = axes.get_xlim()
        width = (xmax-xmin)
        if axes.get_xscale() == 'log':
            alphax = pow(10.0, dx)
            xmin *= alphax
            xmax /= alphax
        else: # linear
            xmin += width*dx
            xmax -= width*dx

        ymin, ymax = axes.get_ylim()
        height = ymax-ymin
        if axes.get_yscale() == 'log':
            alphay = pow(10.0, dy)
            ymin *= alphay
            ymax /= alphay
        else: # linear
            ymin += height*dy
            ymax -= height*dy

        return (xmin, ymin, xmax, ymax)


    #------------------------------------------------------------------------------

    def on_action_ZoomRect(self, action):

        def finish_zooming(sender):
            sb = self.get_statusbar()
            sb.pop(sb.get_context_id('action-zoom'))
            ul = UndoList().describe("Zoom Region")
            layer = self.backend.request_active_layer()
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)

        layer = self.request_active_layer()
        axes = self.backend.get_painter(layer).axes
        s = mpl_selector.SelectRegion(self.backend.figure, axes=axes)
        s.sig_connect('finished', finish_zooming)

        sb = self.get_statusbar()
        sb.push(sb.get_context_id('action-zoom'),
                "Use the left mouse button to zoom.")
        self.select(s)

    def on_action_ZoomFit(self, action):
        self.abort_selection()
        layer = self.request_active_layer()
        if layer is not None:
            region = (None,None,None,None)
            self.zoom_to_region(layer, region, undolist=globals.app.project.journal)
    
    def on_action_ZoomIn(self, action):
        self.abort_selection()
        layer = self.request_active_layer()
        if layer is not None:
            axes = self.backend.get_painter(layer).axes
            region = self.calculate_zoom_region(axes)
            self.zoom_to_region(layer, region, undolist=globals.app.project.journal)
        
    def on_action_ZoomOut(self, action):
        self.abort_selection()
        layer = self.request_active_layer()
        if layer is not None:
            axes = self.backend.get_painter(layer).axes
            region = self.calculate_zoom_region(axes, dx=-0.1, dy=-0.1)
            self.zoom_to_region(layer, region, undolist=globals.app.project.journal)

    def on_action_MoveAxes(self, action):

        def finish_moving(sender):
            ul = UndoList().describe("Move Graph")
            layer = self.backend.request_active_layer()
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)           

        layer = self.request_active_layer()
        axes = self.backend.get_painter(layer).axes
        s = mpl_selector.MoveAxes(self.backend.figure, axes)
        s.sig_connect("finished", finish_moving)
        self.select(s)
        

    def on_action_DataCursor(self, action):
        layer = self.request_active_layer()
        axes = self.backend.get_painter(layer).axes
        s = mpl_selector.DataCursor(self.backend.figure, axes)

        def abort_selector(sender):
            sb = self.get_statusbar()
            context_id = sb.get_context_id("data_cursor")            
            sb.pop(context_id)
            
        def finish_selector(sender):
            self.get_statusbar()
            context_id = sb.get_context_id("data_cursor")            
            sb.pop(context_id)
            xvalue, yvalue = sender.point

        def update_position(sender, line, index, point):
            # Note that 'line' is a Line2d instance from matplotlib!           
            x, y = point
            sb = self.get_statusbar()
            context_id = sb.get_context_id("data_cursor")            
            sb.pop(context_id)
            sb.push(context_id, "X: %f, Y: %f ('%s', value #%s)" %
                    (x, y, line.get_label(), index+1))

        context_id = self.get_statusbar().get_context_id("data_cursor")
        s.sig_connect("update-position", update_position)
        s.sig_connect("finished", finish_selector)
        s.sig_connect("aborted", abort_selector)             
        self.select(s)


    def on_action_SelectLine(self, action):
            
        def finish_select_line(sender):
            print "FINISHED SELECT LINE", sender.line

        layer = self.request_active_layer()
        axes = self.backend.get_painter(layer).axes
        s = mpl_selector.SelectLine(self.backend.figure, axes,
                                    mode=mpl_selector.SELECTLINE_VERTICAL)
        s.sig_connect("finished", finish_select_line)        
        self.select(s)


    def on_action_ZoomAxes(self, action):

        def finish_moving(sender):
            ul = UndoList().describe("Zoom Axes")
            layer = self.backend.request_active_layer()
            self.zoom_to_region(layer, sender.region, undolist=ul)
            self.project.journal.add_undo(ul)           

        layer = self.request_active_layer()
        axes = self.backend.get_painter(layer).axes
        s = mpl_selector.ZoomAxes(self.backend.figure, axes)
        s.sig_connect("finished", finish_moving)
        self.select(s)


    def on_action_CloseTab(self, action):
        self.deactivate()
        self.destroy()        
        
    #----------------------------------------------------------------------
    def abort_selection(self):
        if self._current_selector is not None:
            self._current_selector.abort()
            self._current_selector = None

        for ag in self.get_actiongroups():
            ag.set_sensitive(True)

        globals.app.sig_emit('end-user-action')

    def select(self, selector):
        self.emit("edit-mode-started")
        self._current_selector = None
            
        def on_finish(sender):            
            # Be careful not to call self.abort_selection() in this place.
            print "---"
            print "FINISHING"            
            self._current_selector = None
            globals.app.sig_emit('end-user-action')
            self.emit("edit-mode-ended")

        globals.app.sig_emit('begin-user-action')
        globals.app.sig_connect('cancel-user-action', lambda sender: self.abort_selection())
        selector.sig_connect("finished", on_finish)
        selector.sig_connect("aborted", on_finish)
        self._current_selector = selector
        selector.init()


   #----------------------------------------------------------------------
   # other callbacks

   
    def on_action_ExportViaMPL(self, action):
        self.abort_selection()

        # TODO: pick filename based on self.get_plot().key
        fname = self.fileselect.get_filename_from_user()
        if fname is not None:
            self.backend.canvas.print_figure(fname)


    def on_action_ExportViaGnuplot(self, action):
        self.abort_selection()

        globals.app.plot_postscript(self.project, self.plot)
        



    def button_press_event(self, event):
        canvas = event.canvas
        width = canvas.figure.bbox.width()
        height = canvas.figure.bbox.height()

        hits = self.pick(event.canvas, event.x, event.y, epsilon=20)

        # we only use the first hit right now
        if len(hits) > 0:
            hit = hits[0]
            self.select_mpl_object(hit)


    def pick(self, canvas, x, y, epsilon=5):
        # Taken from matplotlib example library: object_picker.py
        
        """
        Return the artist at location x,y with an error tolerance epsilon
        (in pixels)
        """

        clickBBox = matplotlib.transforms.lbwh_to_bbox(x-epsilon/2, y-epsilon/2, epsilon, epsilon)
        matplotlib.patches.draw_bbox(clickBBox, canvas._renderer)

        def over_text(t):
            bbox = t.get_window_extent(canvas._renderer)
            return clickBBox.overlaps(bbox)

        def over_line(line):
            # can't use the line bbox because it covers the entire extent
            # of the line
            trans = line.get_transform()
            xdata, ydata = trans.numerix_x_y(line.get_xdata(valid_only = True),
                                             line.get_ydata(valid_only = True))
            distances = numpy.sqrt((x-xdata)**2 + (y-ydata)**2)
            return numpy.lib.mlab.min(distances)<epsilon

        hits = []
        
        for ax in canvas.figure.axes:

            for line in ax.get_lines():
                if over_line(line):
                    hits.append(line)

            text = ax.get_xticklabels()
            text.extend( ax.get_yticklabels() )

            for t in text:
                if over_text(t):
                    hits.append(t)
                    print "TEXT", hits

        return hits

    def select_mpl_object(self, obj):

        def find_object(obj):

            for layer_painter in self.backend.painters.itervalues():

                # is it a line ? => Line
                for line, mpl_line in layer_painter.line_cache.iteritems():
                    if obj == mpl_line:
                        return line

                # is it an axis ? => Layer
                if obj in layer_painter.axes.get_xticklabels():
                    return layer_painter.obj
                if obj in layer_painter.axes.get_yticklabels():
                    return layer_painter.obj

            return None

                
        sloppy_object = find_object(obj)
        if sloppy_object is not None:
            if isinstance(sloppy_object, objects.Line):
                globals.app.edit_line(sloppy_object)
            elif isinstance(sloppy_object, objects.Layer):
                globals.app.edit_layer(self.backend.plot, sloppy_object)