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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy