From 03d8bfb5ea40cf48dfc38480fd11c1281c21a6d0 Mon Sep 17 00:00:00 2001 From: Nick Ward Date: Sat, 23 Mar 2013 14:27:50 +0000 Subject: [PATCH] Seperate navigation/toolbar refactor, QT4 backend --- lib/matplotlib/backend_bases.py | 196 ++++++++++++++----------- lib/matplotlib/backends/backend_qt4.py | 130 ++++++++-------- lib/matplotlib/figure.py | 6 +- 3 files changed, 183 insertions(+), 149 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index abcf66c790ad..419f44f5d6c8 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1501,7 +1501,7 @@ def __init__(self, figure): self.button_pick_id = self.mpl_connect('button_press_event', self.pick) self.scroll_pick_id = self.mpl_connect('scroll_event', self.pick) self.mouse_grabber = None # the axes currently grabbing mouse - self.toolbar = None # NavigationToolbar2 will set me + self.navigation = None if False: ## highlight the artists that are hit self.mpl_connect('motion_notify_event', self.onHilite) @@ -2339,17 +2339,15 @@ def stop_event_loop_default(self): self._looping = False -def key_press_handler(event, canvas, toolbar=None): +def key_press_handler(event, canvas, navigation): """ - Implement the default mpl key bindings for the canvas and toolbar - described at :ref:`key-event-handling` + Implement the default mpl key bindings for the canvas described at + :ref:`key-event-handling` *event* a :class:`KeyEvent` instance *canvas* a :class:`FigureCanvasBase` instance - *toolbar* - a :class:`NavigationToolbar2` instance """ # these bindings happen whether you are over an axes or not @@ -2374,31 +2372,30 @@ def key_press_handler(event, canvas, toolbar=None): # toggle fullscreen mode (default key 'f') if event.key in fullscreen_keys: canvas.manager.full_screen_toggle() - # quit the figure (defaut key 'ctrl+w') - if event.key in quit_keys: + elif event.key in quit_keys: Gcf.destroy_fig(canvas.figure) - - if toolbar is not None: - # home or reset mnemonic (default key 'h', 'home' and 'r') - if event.key in home_keys: - toolbar.home() - # forward / backward keys to enable left handed quick navigation - # (default key for backward: 'left', 'backspace' and 'c') - elif event.key in back_keys: - toolbar.back() - # (default key for forward: 'right' and 'v') - elif event.key in forward_keys: - toolbar.forward() - # pan mnemonic (default key 'p') - elif event.key in pan_keys: - toolbar.pan() - # zoom mnemonic (default key 'o') - elif event.key in zoom_keys: - toolbar.zoom() - # saving current figure (default key 's') - elif event.key in save_keys: - toolbar.save_figure() + # home or reset mnemonic (default key 'h', 'home' and 'r') + elif event.key in home_keys: + navigation.home() + # forward / backward keys to enable left handed quick navigation + # (default key for backward: 'left', 'backspace' and 'c') + elif event.key in back_keys: + navigation.back() + # (default key for forward: 'right' and 'v') + elif event.key in forward_keys: + navigation.forward() + # pan mnemonic (default key 'p') + elif event.key in pan_keys: + navigation.pan() + # zoom mnemonic (default key 'o') + elif event.key in zoom_keys: + navigation.zoom() + # saving current figure (default key 's') + elif event.key in save_keys: + navigation.save_figure() + + navigation.update_cursor(event.inaxes) if event.inaxes is None: return @@ -2505,7 +2502,7 @@ def key_press(self, event): Implement the default mpl key bindings defined at :ref:`key-event-handling` """ - key_press_handler(event, self.canvas, self.canvas.toolbar) + key_press_handler(event, self.canvas, self.canvas.navigation) def show_popup(self, msg): """ @@ -2534,9 +2531,68 @@ class Cursors: cursors = Cursors() -class NavigationToolbar2(object): +class Toolbar2Base(object): """ - Base class for the navigation cursor, version 2 + Base class for navigation toolbars, version 2 + + Backend specific sub-classes should define: + + :meth:`_init_toolbar` + create your toolbar widget + + :meth:`set_history_buttons` (optional) + you can change the history back / forward buttons to + indicate disabled / enabled state. + """ + # list of toolitems to add to the toolbar, format is: + # ( + # text, # the text of the button (often not visible to users) + # tooltip_text, # the tooltip shown on hover (where possible) + # image_file, # name of the image for the button (without the extension) + # name_of_method, # name of the method in NavigationBase to call + # ) + toolitems = ( + ('Home', 'Reset original view', 'home', 'home'), + ('Back', 'Back to previous view', 'back', 'back'), + ('Forward', 'Forward to next view', 'forward', 'forward'), + (None, None, None, None), + ('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'), + ('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'), + (None, None, None, None), + ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), + ('Save', 'Save the figure', 'filesave', 'save_figure'), + ) + + def _init_toolbar(self): + """ + This is where you actually build the GUI widgets (called by + __init__). The icons ``home.xpm``, ``back.xpm``, ``forward.xpm``, + ``hand.xpm``, ``zoom_to_rect.xpm`` and ``filesave.xpm`` are standard + across backends (there are ppm versions in CVS also). + + You just need to set the callbacks + + + home : self.home + back : self.back + forward : self.forward + hand : self.pan + zoom_to_rect : self.zoom + filesave : self.save_figure + + You only need to define the last one - the others are in the base + class implementation. + + """ + raise NotImplementedError + + def set_history_buttons(self): + """Enable or disable back/forward button""" + pass + +class NavigationBase(object): + """ + Base class for the navigation backends must implement a canvas that handles connections for 'button_press_event' and 'button_release_event'. See @@ -2551,9 +2607,6 @@ class NavigationToolbar2(object): :meth:`set_cursor` if you want the pointer icon to change - :meth:`_init_toolbar` - create your toolbar widget - :meth:`draw_rubberband` (optional) draw the zoom to rect "rubberband" rectangle @@ -2571,35 +2624,16 @@ class NavigationToolbar2(object): :meth:`set_message` (optional) display message - :meth:`set_history_buttons` (optional) - you can change the history back / forward buttons to - indicate disabled / enabled state. + :meth:`destroy` (optional) + clean up any toolbar/status bar widgets That's it, we'll do the rest! """ - # list of toolitems to add to the toolbar, format is: - # ( - # text, # the text of the button (often not visible to users) - # tooltip_text, # the tooltip shown on hover (where possible) - # image_file, # name of the image for the button (without the extension) - # name_of_method, # name of the method in NavigationToolbar2 to call - # ) - toolitems = ( - ('Home', 'Reset original view', 'home', 'home'), - ('Back', 'Back to previous view', 'back', 'back'), - ('Forward', 'Forward to next view', 'forward', 'forward'), - (None, None, None, None), - ('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'), - ('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'), - (None, None, None, None), - ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), - ('Save', 'Save the figure', 'filesave', 'save_figure'), - ) - - def __init__(self, canvas): + def __init__(self, canvas, toolbar=None): self.canvas = canvas - canvas.toolbar = self + canvas.navigation = self + self.toolbar = toolbar # a dict from axes index to a list of view limits self._views = cbook.Stack() self._positions = cbook.Stack() # stack of subplot positions @@ -2609,7 +2643,8 @@ def __init__(self, canvas): self._idRelease = None self._active = None self._lastCursor = None - self._init_toolbar() + if self.toolbar: + self.toolbar._init_toolbar() self._idDrag = self.canvas.mpl_connect( 'motion_notify_event', self.mouse_move) @@ -2622,6 +2657,14 @@ def __init__(self, canvas): self.mode = '' # a mode string for the status bar self.set_history_buttons() + def destroy(self): + """Destroy status and toolbar widgets.""" + pass + + def set_history_buttons(self): + if self.toolbar: + self.toolbar.set_history_buttons() + def set_message(self, s): """Display a message on toolbar or in status bar""" pass @@ -2654,30 +2697,8 @@ def home(self, *args): self.set_history_buttons() self._update_view() - def _init_toolbar(self): - """ - This is where you actually build the GUI widgets (called by - __init__). The icons ``home.xpm``, ``back.xpm``, ``forward.xpm``, - ``hand.xpm``, ``zoom_to_rect.xpm`` and ``filesave.xpm`` are standard - across backends (there are ppm versions in CVS also). - - You just need to set the callbacks - - home : self.home - back : self.back - forward : self.forward - hand : self.pan - zoom_to_rect : self.zoom - filesave : self.save_figure - - You only need to define the last one - the others are in the base - class implementation. - - """ - raise NotImplementedError - - def mouse_move(self, event): - if not event.inaxes or not self._active: + def update_cursor(self, inaxes=False): + if not inaxes or not self._active: if self._lastCursor != cursors.POINTER: self.set_cursor(cursors.POINTER) self._lastCursor = cursors.POINTER @@ -2692,6 +2713,9 @@ def mouse_move(self, event): self._lastCursor = cursors.MOVE + def mouse_move(self, event): + + self.update_cursor(event.inaxes) if event.inaxes and event.inaxes.get_navigate(): try: @@ -3092,7 +3116,3 @@ def zoom(self, *args): a.set_navigate_mode(self._active) self.set_message(self.mode) - - def set_history_buttons(self): - """Enable or disable back/forward button""" - pass diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index 3caad0bf90c1..fd94e496c1c2 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -8,8 +8,8 @@ from matplotlib import verbose from matplotlib.cbook import is_string_like, onetrue from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ - FigureManagerBase, FigureCanvasBase, NavigationToolbar2, IdleEvent, \ - cursors, TimerBase + FigureManagerBase, FigureCanvasBase, NavigationBase, Toolbar2Base, \ + IdleEvent, cursors, TimerBase from matplotlib.backend_bases import ShowBase from matplotlib._pylab_helpers import Gcf @@ -411,12 +411,14 @@ def __init__(self, canvas, num): self.window._destroying = False - self.toolbar = self._get_toolbar(self.canvas, self.window) - if self.toolbar is not None: - self.window.addToolBar(self.toolbar) - QtCore.QObject.connect(self.toolbar, QtCore.SIGNAL("message"), - self._show_message) - tbs_height = self.toolbar.sizeHint().height() + # TODO: Is canvas the correct QObject to be emitting status messages? + QtCore.QObject.connect(self.canvas, QtCore.SIGNAL("message"), + self._show_message) + + self.navigation = self._get_navigation(self.canvas) + if self.navigation.toolbar is not None: + self.window.addToolBar(self.navigation.toolbar) + tbs_height = self.navigation.toolbar.sizeHint().height() else: tbs_height = 0 @@ -435,8 +437,7 @@ def __init__(self, canvas, num): def notify_axes_change(fig): # This will be called whenever the current axes is changed - if self.toolbar is not None: - self.toolbar.update() + self.navigation.update() self.canvas.figure.add_axobserver(notify_axes_change) @QtCore.Slot() @@ -462,16 +463,8 @@ def _widgetclosed(self): # Gcf can get destroyed before the Gcf.destroy # line is run, leading to a useless AttributeError. - def _get_toolbar(self, canvas, parent): - # must be inited after the window, drawingArea and figure - # attrs are set - if matplotlib.rcParams['toolbar'] == 'classic': - print("Classic toolbar is not supported") - elif matplotlib.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2QT(canvas, parent, False) - else: - toolbar = None - return toolbar + def _get_navigation(self, canvas): + return NavigationQT(canvas, False) def resize(self, width, height): 'set the canvas size in pixels' @@ -489,8 +482,7 @@ def destroy(self, *args): self.window._destroying = True QtCore.QObject.disconnect(self.window, QtCore.SIGNAL('destroyed()'), self._widgetclosed) - if self.toolbar: - self.toolbar.destroy() + self.navigation.destroy() if DEBUG: print("destroy figure manager") self.window.close() @@ -501,16 +493,26 @@ def get_window_title(self): def set_window_title(self, title): self.window.setWindowTitle(title) -class NavigationToolbar2QT( NavigationToolbar2, QtGui.QToolBar ): - def __init__(self, canvas, parent, coordinates=True): - """ coordinates: should we show the coordinates on the right? """ - self.canvas = canvas - self.coordinates = coordinates - self._actions = {} - """A mapping of toolitem method names to their QActions""" +class Toolbar2QT(Toolbar2Base, QtGui.QToolBar): + def __init__(self, navigation, parent, coordinates=False): + self.navigation = navigation QtGui.QToolBar.__init__( self, parent ) - NavigationToolbar2.__init__( self, canvas ) + self._actions = {} # A mapping of toolitem method names to QActions + self.coordinates = coordinates + + # Add the x,y location widget at the right side of the toolbar + # The stretch factor is 1 which means any resizing of the toolbar + # will resize this label instead of the buttons. + if self.coordinates: + self.locLabel = QtGui.QLabel( "", self.canvas ) + self.locLabel.setAlignment( + QtCore.Qt.AlignRight | QtCore.Qt.AlignTop ) + self.locLabel.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Ignored)) + labelAction = self.addWidget(self.locLabel) + labelAction.setVisible(True) def _icon(self, name): return QtGui.QIcon(os.path.join(self.basedir, name)) @@ -523,7 +525,7 @@ def _init_toolbar(self): self.addSeparator() else: a = self.addAction(self._icon(image_file + '.png'), - text, getattr(self, callback)) + text, getattr(self.navigation, callback)) self._actions[callback] = a if callback in ['zoom', 'pan']: a.setCheckable(True) @@ -532,27 +534,39 @@ def _init_toolbar(self): if figureoptions is not None: a = self.addAction(self._icon("qt4_editor_options.png"), - 'Customize', self.edit_parameters) + 'Customize', self.navigation.edit_parameters) a.setToolTip('Edit curves line and axes parameters') - self.buttons = {} + def _update_buttons_checked(self, active): + #sync button checkstates to match active mode + self._actions['pan'].setChecked(active == 'PAN') + self._actions['zoom'].setChecked(active == 'ZOOM') - # Add the x,y location widget at the right side of the toolbar - # The stretch factor is 1 which means any resizing of the toolbar - # will resize this label instead of the buttons. - if self.coordinates: - self.locLabel = QtGui.QLabel( "", self ) - self.locLabel.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignTop ) - self.locLabel.setSizePolicy( - QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Ignored)) - labelAction = self.addWidget(self.locLabel) - labelAction.setVisible(True) +class NavigationQT(NavigationBase): + def __init__(self, canvas, coordinates=True): + """ coordinates: should we show the coordinates on the right? """ + toolbar = self._get_toolbar(canvas.window()) + NavigationBase.__init__( self, canvas, toolbar ) + self.coordinates = coordinates # reference holder for subplots_adjust window self.adj_window = None + def _get_toolbar(self, parent): + # must be inited after the window, drawingArea and figure + # attrs are set + if matplotlib.rcParams['toolbar'] == 'classic': + print("Classic toolbar is not supported") + elif matplotlib.rcParams['toolbar'] == 'toolbar2': + toolbar = Toolbar2QT(self, parent) + else: + toolbar = None + return toolbar + + def destroy(self): + if self.toolbar: + self.toolbar.destroy() + if figureoptions is not None: def edit_parameters(self): allaxes = self.canvas.figure.get_axes() @@ -585,26 +599,25 @@ def edit_parameters(self): figureoptions.figure_edit(axes, self) - def _update_buttons_checked(self): - #sync button checkstates to match active mode - self._actions['pan'].setChecked(self._active == 'PAN') - self._actions['zoom'].setChecked(self._active == 'ZOOM') - def pan(self, *args): - super(NavigationToolbar2QT, self).pan(*args) - self._update_buttons_checked() + super(NavigationQT, self).pan(*args) + if self.toolbar: + self.toolbar._update_buttons_checked(self._active) def zoom(self, *args): - super(NavigationToolbar2QT, self).zoom(*args) - self._update_buttons_checked() + super(NavigationQT, self).zoom(*args) + if self.toolbar: + self.toolbar._update_buttons_checked(self._active) def dynamic_update( self ): self.canvas.draw() def set_message( self, s ): - self.emit(QtCore.SIGNAL("message"), s) - if self.coordinates: - self.locLabel.setText(s.replace(', ', '\n')) + # TODO: Is canvas the correct QObject to be emitting status messages? + self.canvas.emit(QtCore.SIGNAL("message"), s) + coordinates = getattr(self.toolbar, "coordinates", False) + if coordinates: + self.toolbar.locLabel.setText(s.replace(', ', '\n')) def set_cursor( self, cursor ): if DEBUG: print('Set cursor' , cursor) @@ -656,7 +669,8 @@ def save_figure(self, *args): selectedFilter = filter filters.append(filter) filters = ';;'.join(filters) - fname = _getSaveFileName(self, "Choose a filename to save to", + # TODO: self.canvas might be wrong QT object? + fname = _getSaveFileName(self.canvas, "Choose a filename to save to", start, filters, selectedFilter) if fname: if startpath == '': diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 03a9966bb0d9..5879beb6634a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -922,9 +922,9 @@ def clf(self, keep_observers=False): ax.cla() self.delaxes(ax) # removes ax from self._axstack - toolbar = getattr(self.canvas, 'toolbar', None) - if toolbar is not None: - toolbar.update() + navigation = getattr(self.canvas, 'navigation', None) + if navigation: + navigation.update() self._axstack.clear() self.artists = [] self.lines = []