diff --git a/examples/user_interfaces/reconfigurable_toolbar_gtk3.py b/examples/user_interfaces/reconfigurable_toolbar_gtk3.py new file mode 100644 index 000000000000..7566fd68ee3d --- /dev/null +++ b/examples/user_interfaces/reconfigurable_toolbar_gtk3.py @@ -0,0 +1,18 @@ +import matplotlib +matplotlib.use('GTK3Agg') +#matplotlib.rcParams['toolbar'] = 'None' +import matplotlib.pyplot as plt + +fig = plt.figure() +ax = fig.add_subplot(111) +ax.plot([0, 1]) + +#Lets play with the buttons in the fig toolbar +# +#Back? who needs back? my mom always told me, don't look back, +fig.canvas.manager.toolbar.remove_tool(1) + +#Move home somewhere else +fig.canvas.manager.toolbar.move_tool(0, 6) + +plt.show() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 39489fb1ae5b..09e6aff9888c 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -25,6 +25,13 @@ the 'show' callable is then set to Show.__call__, inherited from ShowBase. +:class: `NavigationBase` + Class that holds the navigation state (or toolbar state). + This class is attached to `FigureManagerBase.navigation` + +:class:`ToolbarBase` + The base class that controls the GUI interface of the toolbar + passes all the requests to the navigation instance """ from __future__ import (absolute_import, division, print_function, @@ -2561,12 +2568,24 @@ class FigureManagerBase: *num* The figure number + + *navigation* + Navigation state holder """ + + navigation_class = None + """Navigation class that will be instantiated as navigation for this child""" + def __init__(self, canvas, num): self.canvas = canvas canvas.manager = self # store a pointer to parent self.num = num - + + if self.navigation_class is not None: + self.navigation = self.navigation_class(self.canvas) + else: + self.navigation = None + self.key_press_handler_id = self.canvas.mpl_connect('key_press_event', self.key_press) """ @@ -3207,3 +3226,318 @@ def zoom(self, *args): def set_history_buttons(self): """Enable or disable back/forward button""" pass + + +class NavigationBase(NavigationToolbar2): + """Holder for navigation information + + Attributes + ---------- + toolbar : Toolbar + Instance derivate of `ToolbarBase` + + Examples + ---------- + To access this instance from a figure instance use + + >>> figure.canvas.navigation + + Notes + ---------- + Every call to this toolbar that is not defined in `NavigationToolbar2` or here will be passed to + `toolbar` via `__getattr__` + + In general it is not necessary to overrride this class. If you need to change the toolbar + change the backend derivate of `ToolbarBase` + + There is no need to instantiate this class, this will be done automatically from + `FigureManagerBase.__init__` + """ + + def __init__(self, canvas): + self.toolbar = None + NavigationToolbar2.__init__(self, canvas) + + #Until this is merged with NavigationToolbar2 we have to provide this method + #for NavigationToolbar2.__init__ to work + def _init_toolbar(self): + self.ctx = None + + def attach(self, toolbar): + """Add itself to the given toolbar + + Parameters + ---------- + toolbar: Toolbar + Derivate of `ToolbarBase` + + """ + if self.toolbar is not None: + self.detach() + self.toolbar = toolbar + if toolbar is not None: + self.toolbar._add_navigation(self) + + def detach(self): + """Remove this instance from the control of `toolbar` + + Notes + ---------- + This method is called from `FigureManager.destroy` + """ + if self.toolbar is not None: + self.toolbar._remove_navigation(self) + self.toolbar = None + + def set_message(self, text): + if self.toolbar: + self.toolbar.set_message(text) + + def destroy(self): + self.detach() + self.toolbar = None + + +class ToolbarBase(object): + """Base class for the real GUI toolbar + + This class defines the basic methods that the backend specific implementation + has to have. + + Notes + ---------- + The mandatory methods for a specific backend are + + - `add_toolitem` + - `connect_toolitem` + - `init_toolbar` + - `save_figure` + + The suggested methods to implement are + + - `remove_tool` + - `move_tool` + - `set_visible_tool` + - `add_separator` + - `add_message` + + Examples + ---------- + To access this instance from a figure isntance + + >>> figure.canvas.navigation.toolbar + + Some undefined attributes of `Navigation` call this class via + `Navigation.__getattr__`, most of the time it can be accesed directly with + + >>> figure.canvas.toolbar + """ + toolitems = ({'text': 'Home', + 'tooltip_text': 'Reset original view', + 'image': 'home', + 'callback': 'home'}, + + {'text': 'Back', + 'tooltip_text': 'Back to previous view', + 'image': 'back', + 'callback': 'back'}, + + {'text': 'Forward', + 'tooltip_text': 'Forward to next view', + 'image': 'forward', + 'callback': 'forward'}, + + None, + + {'text': 'Pan', + 'tooltip_text': 'Pan axes with left mouse, zoom with right', + 'image': 'move', + 'callback': 'pan'}, + + {'text': 'Zoom', + 'tooltip_text': 'Zoom to rectangle', + 'image': 'zoom_to_rect', + 'callback': 'zoom'}, + + None, + + {'text': 'Subplots', + 'tooltip_text': 'Configure subplots', + 'image': 'subplots', + 'callback': 'configure_subplots'}, + + {'text': 'Save', + 'tooltip_text': 'Save the figure', + 'image': 'filesave', + 'callback': 'save_figure'}, + ) + #FIXME: overwriting the signature in the documentation is not working + """toolitems=({}) + + List of Dictionnaries containing the default toolitems to add to the toolbar + + Each dict element of contains + - text : Text or name for the tool + - tooltip_text : Tooltip text + - image : Image to use + - callback : Function callback definied in this class or derivates + """ + + def __init__(self): + self._navigation = None + self.init_toolbar() + self.add_message() + + for pos, item in enumerate(self.toolitems): + if item is None: + self.add_separator(pos=pos) + continue + + btn = item.copy() + callback = btn.pop('callback') + tbutton = self.add_toolitem(pos=pos, **btn) + if tbutton: + self.connect_toolitem(tbutton, callback) + + self._current = None + + def init_toolbar(self): + """Initialized the toolbar + + Creates the frame to place the toolitems + """ + raise NotImplementedError + + def remove_tool(self, pos): + """Remove the tool located at given position + + .. note:: It is recommended to implement this method + + Parameters + ---------- + pos : Int + Position where the tool to remove is located + """ + #remote item from the toolbar, + pass + + def set_visible_tool(self, toolitem, visible): + """Toggle the visibility of a toolitem + + Parameters + ---------- + toolitem: backend specific + toolitem returned by `add_toolitem` + visible: bool + if true set visible, + if false set invisible + + """ + pass + + def move_tool(self, pos_ini, pos_fin): + """Move the tool between to positions + + .. note:: It is recommended to implement this method + + Parameters + ---------- + pos_ini : Int + Position (coordinates) where the tool to is located + pos_fin : Int + New position (coordinates) where the tool will reside + """ + #move item in the toolbar + pass + + def connect_toolitem(self, toolitem, callback, *args, **kwargs): + """Connect the tooitem to the callback + + This is backend specific, takes the arguments and connect the added tool to + the callback passing *args and **kwargs to the callback + + The action is the 'clicked' or whatever name in the backend for the activation of the tool + + Parameters + ---------- + toolitem : backend specific + Toolitem returned by `add_toolitem` + callback : method + Method that will be called when the toolitem is activated + + Examples + ---------- + In Gtk3 this method is implemented as + + >>> def connect_toolitem(self, button, callback, *args, **kwargs): + >>> def mcallback(btn, cb, args, kwargs): + >>> getattr(self, cb)(*args, **kwargs) + >>> + >>> button.connect('clicked', mcallback, callback, args, kwargs) + + Notes + ---------- + The need for this method is to get rid of all the backend specific signal handling + """ + + raise NotImplementedError + + def __getattr__(self, name): + #The callbacks in self.toolitems are handled directly by navigation + cbs = [it['callback'] for it in self.toolitems if it is not None] + if name in cbs: + return getattr(self._navigation, name) + raise AttributeError('Unknown attribute %s' % name) + + def _add_navigation(self, navigation): + #Set the navigation controlled by this toolbar + self._navigation = navigation + + def _remove_navigation(self, navigation): + #Remove the navigation controlled by this toolbar + + self._navigation = None + + def add_toolitem(self, text='_', pos=-1, + tooltip_text='', image=None): + + """Add toolitem to the toolbar + + Parameters + ---------- + pos : Int, optional + Position to add the tool + text : string, optional + Text for the tool + tooltip_text : string, optional + Text for the tooltip + image : string, optional + Reference to an image file to be used to represent the tool + + Returns + ------- + toolitem: Toolitem created, backend specific + + Notes + ---------- + There is no need to call this method directly, it is called from `add_tool` + """ + + raise NotImplementedError + + def add_separator(self, pos=-1): + """Add a separator to the toolbar + + Parameters + ---------- + pos : Int, optional + Position to add the separator + """ + pass + + def add_message(self): + """Add message container + + The message in this container will be setted by `NavigationBase.set_message`` + """ + pass diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 740d8bb0e872..58255c2fdb01 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -29,7 +29,8 @@ def fn_name(): return sys._getframe(1).f_code.co_name import matplotlib from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import RendererBase, GraphicsContextBase, \ - FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase + FigureManagerBase, FigureCanvasBase, NavigationToolbar2, cursors, TimerBase, \ + NavigationBase, ToolbarBase from matplotlib.backend_bases import ShowBase from matplotlib.cbook import is_string_like, is_writable_file_like @@ -421,7 +422,7 @@ def destroy(*args): def notify_axes_change(fig): 'this will be called whenever the current axes is changed' - if self.toolbar is not None: self.toolbar.update() + if self.navigation is not None: self.navigation.update() self.canvas.figure.add_axobserver(notify_axes_change) self.canvas.grab_focus() @@ -431,9 +432,9 @@ def destroy(self, *args): self.vbox.destroy() self.window.destroy() self.canvas.destroy() + self.navigation.destroy() if self.toolbar: self.toolbar.destroy() - self.__dict__.clear() #Is this needed? Other backends don't have it. if Gcf.get_num_fig_managers()==0 and \ not matplotlib.is_interactive() and \ @@ -452,12 +453,12 @@ def full_screen_toggle (self): self.window.unfullscreen() _full_screen_flag = False - def _get_toolbar(self, canvas): # must be inited after the window, drawingArea and figure # attrs are set if rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2GTK3 (canvas, self.window) + toolbar = ToolbarGTK3() + self.navigation.attach(toolbar) else: toolbar = None return toolbar @@ -476,16 +477,7 @@ def resize(self, width, height): self.window.resize(width, height) -class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): - def __init__(self, canvas, window): - self.win = window - GObject.GObject.__init__(self) - NavigationToolbar2.__init__(self, canvas) - self.ctx = None - - def set_message(self, s): - self.message.set_label(s) - +class NavigationGTK3(NavigationBase): def set_cursor(self, cursor): self.canvas.get_property("window").set_cursor(cursord[cursor]) #self.canvas.set_cursor(cursord[cursor]) @@ -519,40 +511,10 @@ def draw_rubberband(self, event, x0, y0, x1, y1): self.ctx.set_source_rgb(0, 0, 0) self.ctx.stroke() - def _init_toolbar(self): - self.set_style(Gtk.ToolbarStyle.ICONS) - basedir = os.path.join(rcParams['datapath'],'images') - - 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 + '.png') - image = Gtk.Image() - image.set_from_file(fname) - tbutton = Gtk.ToolButton() - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self.insert(tbutton, -1) - tbutton.connect('clicked', getattr(self, callback)) - tbutton.set_tooltip_text(tooltip_text) - - toolitem = Gtk.SeparatorToolItem() - self.insert(toolitem, -1) - toolitem.set_draw(False) - toolitem.set_expand(True) - - toolitem = Gtk.ToolItem() - self.insert(toolitem, -1) - self.message = Gtk.Label() - toolitem.add(self.message) - - self.show_all() - def get_filechooser(self): fc = FileChooserDialog( title='Save the figure', - parent=self.win, + parent=self.canvas.manager.window, path=os.path.expanduser(rcParams.get('savefig.directory', '')), filetypes=self.canvas.get_supported_filetypes(), default_filetype=self.canvas.get_default_filetype()) @@ -576,7 +538,7 @@ def save_figure(self, *args): except Exception as e: error_msg_gtk(str(e), parent=self) - def configure_subplots(self, button): + def configure_subplots(self): toolfig = Figure(figsize=(6,3)) canvas = self._get_canvas(toolfig) toolfig.subplots_adjust(top=0.9) @@ -611,6 +573,99 @@ def _get_canvas(self, fig): return self.canvas.__class__(fig) +FigureManagerGTK3.navigation_class = NavigationGTK3 + + +class ToolbarGTK3(ToolbarBase, Gtk.Box): + + def set_visible_tool(self, toolitem, visible): + toolitem.set_visible(visible) + + def connect_toolitem(self, button, callback, *args, **kwargs): + def mcallback(btn, cb, args, kwargs): + getattr(self, cb)(*args, **kwargs) + + button.connect('clicked', mcallback, callback, args, kwargs) + + def add_toolitem(self, text='_', pos=-1, + tooltip_text='', image=None): + timage = None + if image: + timage = Gtk.Image() + + if os.path.isfile(image): + timage.set_from_file(image) + else: + basedir = os.path.join(rcParams['datapath'], 'images') + fname = os.path.join(basedir, image + '.png') + if os.path.isfile(fname): + timage.set_from_file(fname) + else: + #TODO: Add the right mechanics to pass the image from string +# from gi.repository import GdkPixbuf +# pixbuf = GdkPixbuf.Pixbuf.new_from_inline(image, False) + timage = False + + tbutton = Gtk.ToolButton() + + tbutton.set_label(text) + if timage: + tbutton.set_icon_widget(timage) + timage.show() + tbutton.set_tooltip_text(tooltip_text) + self._toolbar.insert(tbutton, pos) + tbutton.show() + return tbutton + + def remove_tool(self, pos): + widget = self._toolbar.get_nth_item(pos) + if not widget: + self.set_message('Impossible to remove tool %d' % pos) + return + self._toolbar.remove(widget) + + def move_tool(self, pos_ini, pos_fin): + widget = self._toolbar.get_nth_item(pos_ini) + if not widget: + self.set_message('Impossible to remove tool %d' % pos_ini) + return + self._toolbar.remove(widget) + self._toolbar.insert(widget, pos_fin) + + def add_separator(self, pos=-1): + toolitem = Gtk.SeparatorToolItem() + self._toolbar.insert(toolitem, pos) + toolitem.show() + return toolitem + + def init_toolbar(self): + Gtk.Box.__init__(self) + self.set_property("orientation", Gtk.Orientation.VERTICAL) + self._toolbar = Gtk.Toolbar() + self._toolbar.set_style(Gtk.ToolbarStyle.ICONS) + self.pack_start(self._toolbar, False, False, 0) + self.show_all() + + def add_message(self): + box = Gtk.Box() + box.set_property("orientation", Gtk.Orientation.HORIZONTAL) + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + box.pack_start(sep, False, True, 0) + self.message = Gtk.Label() + box.pack_end(self.message, False, False, 0) + box.show_all() + self.pack_end(box, False, False, 5) + + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.HORIZONTAL) + self.pack_end(sep, False, True, 0) + self.show_all() + + def set_message(self, text): + self.message.set_label(text) + + class FileChooserDialog(Gtk.FileChooserDialog): """GTK+ file selector which remembers the last file/directory selected and presents the user with a menu of supported image formats