import functools import logging import os from pathlib import Path import sys import matplotlib as mpl from matplotlib import backend_tools, cbook from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, StatusbarBase, TimerBase, ToolContainerBase, cursors) from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool try: import gi except ImportError as err: raise ImportError("The GTK3 backends require PyGObject") from err try: # :raises ValueError: If module/version is already loaded, already # required, or unavailable. gi.require_version("Gtk", "3.0") except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. raise ImportError from e from gi.repository import Gio, GLib, GObject, Gtk, Gdk _log = logging.getLogger(__name__) backend_version = "%s.%s.%s" % ( Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version()) try: cursord = { cursors.MOVE: Gdk.Cursor.new(Gdk.CursorType.FLEUR), cursors.HAND: Gdk.Cursor.new(Gdk.CursorType.HAND2), cursors.POINTER: Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR), cursors.SELECT_REGION: Gdk.Cursor.new(Gdk.CursorType.TCROSS), cursors.WAIT: Gdk.Cursor.new(Gdk.CursorType.WATCH), } except TypeError as exc: # Happens when running headless. Convert to ImportError to cooperate with # backend switching. raise ImportError(exc) from exc class TimerGTK3(TimerBase): """Subclass of `.TimerBase` using GTK3 timer events.""" def __init__(self, *args, **kwargs): self._timer = None TimerBase.__init__(self, *args, **kwargs) def _timer_start(self): # Need to stop it, otherwise we potentially leak a timer id that will # never be stopped. self._timer_stop() self._timer = GLib.timeout_add(self._interval, self._on_timer) def _timer_stop(self): if self._timer is not None: GLib.source_remove(self._timer) self._timer = None def _timer_set_interval(self): # Only stop and restart it if the timer has already been started if self._timer is not None: self._timer_stop() self._timer_start() def _on_timer(self): TimerBase._on_timer(self) # Gtk timeout_add() requires that the callback returns True if it # is to be called again. if self.callbacks and not self._single: return True else: self._timer = None return False class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk3" _timer_cls = TimerGTK3 keyvald = {65507: 'control', 65505: 'shift', 65513: 'alt', 65508: 'control', 65506: 'shift', 65514: 'alt', 65361: 'left', 65362: 'up', 65363: 'right', 65364: 'down', 65307: 'escape', 65470: 'f1', 65471: 'f2', 65472: 'f3', 65473: 'f4', 65474: 'f5', 65475: 'f6', 65476: 'f7', 65477: 'f8', 65478: 'f9', 65479: 'f10', 65480: 'f11', 65481: 'f12', 65300: 'scroll_lock', 65299: 'break', 65288: 'backspace', 65293: 'enter', 65379: 'insert', 65535: 'delete', 65360: 'home', 65367: 'end', 65365: 'pageup', 65366: 'pagedown', 65438: '0', 65436: '1', 65433: '2', 65435: '3', 65430: '4', 65437: '5', 65432: '6', 65429: '7', 65431: '8', 65434: '9', 65451: '+', 65453: '-', 65450: '*', 65455: '/', 65439: 'dec', 65421: 'enter', } # Setting this as a static constant prevents # this resulting expression from leaking event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.SCROLL_MASK) def __init__(self, figure): FigureCanvasBase.__init__(self, figure) GObject.GObject.__init__(self) self._idle_draw_id = 0 self._lastCursor = None self._rubberband_rect = None self.connect('scroll_event', self.scroll_event) self.connect('button_press_event', self.button_press_event) self.connect('button_release_event', self.button_release_event) self.connect('configure_event', self.configure_event) self.connect('draw', self.on_draw_event) self.connect('draw', self._post_draw) self.connect('key_press_event', self.key_press_event) self.connect('key_release_event', self.key_release_event) self.connect('motion_notify_event', self.motion_notify_event) self.connect('leave_notify_event', self.leave_notify_event) self.connect('enter_notify_event', self.enter_notify_event) self.connect('size_allocate', self.size_allocate) self.set_events(self.__class__.event_mask) self.set_double_buffered(True) self.set_can_focus(True) renderer_init = cbook._deprecate_method_override( __class__._renderer_init, self, allow_empty=True, since="3.3", addendum="Please initialize the renderer, if needed, in the " "subclass' __init__; a fully empty _renderer_init implementation " "may be kept for compatibility with earlier versions of " "Matplotlib.") if renderer_init: renderer_init() @cbook.deprecated("3.3", alternative="__init__") def _renderer_init(self): pass def destroy(self): #Gtk.DrawingArea.destroy(self) self.close_event() def scroll_event(self, widget, event): x = event.x # flipy so y=0 is bottom of canvas y = self.get_allocation().height - event.y step = 1 if event.direction == Gdk.ScrollDirection.UP else -1 FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) return False # finish event propagation? def button_press_event(self, widget, event): x = event.x # flipy so y=0 is bottom of canvas y = self.get_allocation().height - event.y FigureCanvasBase.button_press_event( self, x, y, event.button, guiEvent=event) return False # finish event propagation? def button_release_event(self, widget, event): x = event.x # flipy so y=0 is bottom of canvas y = self.get_allocation().height - event.y FigureCanvasBase.button_release_event( self, x, y, event.button, guiEvent=event) return False # finish event propagation? def key_press_event(self, widget, event): key = self._get_key(event) FigureCanvasBase.key_press_event(self, key, guiEvent=event) return True # stop event propagation def key_release_event(self, widget, event): key = self._get_key(event) FigureCanvasBase.key_release_event(self, key, guiEvent=event) return True # stop event propagation def motion_notify_event(self, widget, event): if event.is_hint: t, x, y, state = event.window.get_pointer() else: x, y = event.x, event.y # flipy so y=0 is bottom of canvas y = self.get_allocation().height - y FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) return False # finish event propagation? def leave_notify_event(self, widget, event): FigureCanvasBase.leave_notify_event(self, event) def enter_notify_event(self, widget, event): x = event.x # flipy so y=0 is bottom of canvas y = self.get_allocation().height - event.y FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) def size_allocate(self, widget, allocation): dpival = self.figure.dpi winch = allocation.width / dpival hinch = allocation.height / dpival self.figure.set_size_inches(winch, hinch, forward=False) FigureCanvasBase.resize_event(self) self.draw_idle() def _get_key(self, event): if event.keyval in self.keyvald: key = self.keyvald[event.keyval] elif event.keyval < 256: key = chr(event.keyval) else: key = None modifiers = [ (Gdk.ModifierType.MOD4_MASK, 'super'), (Gdk.ModifierType.MOD1_MASK, 'alt'), (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), ] for key_mask, prefix in modifiers: if event.state & key_mask: key = '{0}+{1}'.format(prefix, key) return key def configure_event(self, widget, event): if widget.get_property("window") is None: return w, h = event.width, event.height if w < 3 or h < 3: return # empty fig # resize the figure (in inches) dpi = self.figure.dpi self.figure.set_size_inches(w / dpi, h / dpi, forward=False) return False # finish event propagation? def _draw_rubberband(self, rect): self._rubberband_rect = rect # TODO: Only update the rubberband area. self.queue_draw() def _post_draw(self, widget, ctx): if self._rubberband_rect is None: return x0, y0, w, h = self._rubberband_rect x1 = x0 + w y1 = y0 + h # Draw the lines from x0, y0 towards x1, y1 so that the # dashes don't "jump" when moving the zoom box. ctx.move_to(x0, y0) ctx.line_to(x0, y1) ctx.move_to(x0, y0) ctx.line_to(x1, y0) ctx.move_to(x0, y1) ctx.line_to(x1, y1) ctx.move_to(x1, y0) ctx.line_to(x1, y1) ctx.set_antialias(1) ctx.set_line_width(1) ctx.set_dash((3, 3), 0) ctx.set_source_rgb(0, 0, 0) ctx.stroke_preserve() ctx.set_dash((3, 3), 3) ctx.set_source_rgb(1, 1, 1) ctx.stroke() def on_draw_event(self, widget, ctx): # to be overwritten by GTK3Agg or GTK3Cairo pass def draw(self): # docstring inherited if self.is_drawable(): self.queue_draw() def draw_idle(self): # docstring inherited if self._idle_draw_id != 0: return def idle_draw(*args): try: self.draw() finally: self._idle_draw_id = 0 return False self._idle_draw_id = GLib.idle_add(idle_draw) def flush_events(self): # docstring inherited Gdk.threads_enter() while Gtk.events_pending(): Gtk.main_iteration() Gdk.flush() Gdk.threads_leave() class FigureManagerGTK3(FigureManagerBase): """ Attributes ---------- canvas : `FigureCanvas` The FigureCanvas instance num : int or str The Figure number toolbar : Gtk.Toolbar The Gtk.Toolbar vbox : Gtk.VBox The Gtk.VBox containing the canvas and toolbar window : Gtk.Window The Gtk.Window """ def __init__(self, canvas, num): FigureManagerBase.__init__(self, canvas, num) self.window = Gtk.Window() self.window.set_wmclass("matplotlib", "Matplotlib") self.set_window_title("Figure %d" % num) try: self.window.set_icon_from_file(window_icon) except Exception: # Some versions of gtk throw a glib.GError but not all, so I am not # sure how to catch it. I am unhappy doing a blanket catch here, # but am not sure what a better way is - JDH _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) self.window.add(self.vbox) self.vbox.show() self.canvas.show() self.vbox.pack_start(self.canvas, True, True, 0) # calculate size for window w = int(self.canvas.figure.bbox.width) h = int(self.canvas.figure.bbox.height) self.toolbar = self._get_toolbar() def add_widget(child): child.show() self.vbox.pack_end(child, False, False, 0) size_request = child.size_request() return size_request.height if self.toolmanager: backend_tools.add_tools_to_manager(self.toolmanager) if self.toolbar: backend_tools.add_tools_to_container(self.toolbar) if self.toolbar is not None: self.toolbar.show() h += add_widget(self.toolbar) self.window.set_default_size(w, h) self._destroying = False self.window.connect("destroy", lambda *args: Gcf.destroy(self)) self.window.connect("delete_event", lambda *args: Gcf.destroy(self)) if mpl.is_interactive(): self.window.show() self.canvas.draw_idle() self.canvas.grab_focus() def destroy(self, *args): if self._destroying: # Otherwise, this can be called twice when the user presses 'q', # which calls Gcf.destroy(self), then this destroy(), then triggers # Gcf.destroy(self) once again via # `connect("destroy", lambda *args: Gcf.destroy(self))`. return self._destroying = True self.vbox.destroy() self.window.destroy() self.canvas.destroy() if self.toolbar: self.toolbar.destroy() if (Gcf.get_num_fig_managers() == 0 and not mpl.is_interactive() and Gtk.main_level() >= 1): Gtk.main_quit() def show(self): # show the figure window self.window.show() self.canvas.draw() if mpl.rcParams['figure.raise_window']: self.window.present() def full_screen_toggle(self): self._full_screen_flag = not self._full_screen_flag if self._full_screen_flag: self.window.fullscreen() else: self.window.unfullscreen() _full_screen_flag = False def _get_toolbar(self): # must be inited after the window, drawingArea and figure # attrs are set if mpl.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2GTK3(self.canvas, self.window) elif mpl.rcParams['toolbar'] == 'toolmanager': toolbar = ToolbarGTK3(self.toolmanager) else: toolbar = None return toolbar def get_window_title(self): return self.window.get_title() def set_window_title(self, title): self.window.set_title(title) def resize(self, width, height): """Set the canvas size in pixels.""" if self.toolbar: toolbar_size = self.toolbar.size_request() height += toolbar_size.height canvas_size = self.canvas.get_allocation() if canvas_size.width == canvas_size.height == 1: # A canvas size of (1, 1) cannot exist in most cases, because # window decorations would prevent such a small window. This call # must be before the window has been mapped and widgets have been # sized, so just change the window's starting size. self.window.set_default_size(width, height) else: self.window.resize(width, height) class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): def __init__(self, canvas, window): self.win = window GObject.GObject.__init__(self) self.set_style(Gtk.ToolbarStyle.ICONS) self._gtk_ids = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: self.insert(Gtk.SeparatorToolItem(), -1) continue image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string( str(cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) self._gtk_ids[text] = tbutton = ( Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) tbutton.set_label(text) tbutton.set_icon_widget(image) self.insert(tbutton, -1) # Save the handler id, so that we can block it as needed. tbutton._signal_handler = 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) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. toolitem = Gtk.ToolItem() self.insert(toolitem, -1) label = Gtk.Label() label.set_markup( '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') toolitem.add(label) toolitem = Gtk.ToolItem() self.insert(toolitem, -1) self.message = Gtk.Label() toolitem.add(self.message) self.show_all() NavigationToolbar2.__init__(self, canvas) @cbook.deprecated("3.3") @property def ctx(self): return self.canvas.get_property("window").cairo_create() def set_message(self, s): escaped = GLib.markup_escape_text(s) self.message.set_markup(f'{escaped}') def set_cursor(self, cursor): window = self.canvas.get_property("window") if window is not None: window.set_cursor(cursord[cursor]) Gtk.main_iteration() def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 y0 = height - y0 rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] self.canvas._draw_rubberband(rect) def remove_rubberband(self): self.canvas._draw_rubberband(None) def _update_buttons_checked(self): for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: button = self._gtk_ids.get(name) if button: with button.handler_block(button._signal_handler): button.set_active(self.mode.name == active) def pan(self, *args): super().pan(*args) self._update_buttons_checked() def zoom(self, *args): super().zoom(*args) self._update_buttons_checked() def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", parent=self.canvas.get_toplevel(), action=Gtk.FileChooserAction.SAVE, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK), ) for name, fmts \ in self.canvas.get_supported_filetypes_grouped().items(): ff = Gtk.FileFilter() ff.set_name(name) for fmt in fmts: ff.add_pattern("*." + fmt) dialog.add_filter(ff) if self.canvas.get_default_filetype() in fmts: dialog.set_filter(ff) @functools.partial(dialog.connect, "notify::filter") def on_notify_filter(*args): name = dialog.get_filter().get_name() fmt = self.canvas.get_supported_filetypes_grouped()[name][0] dialog.set_current_name( str(Path(dialog.get_current_name()).with_suffix("." + fmt))) dialog.set_current_folder(mpl.rcParams["savefig.directory"]) dialog.set_current_name(self.canvas.get_default_filename()) dialog.set_do_overwrite_confirmation(True) response = dialog.run() fname = dialog.get_filename() ff = dialog.get_filter() # Doesn't autoadjust to filename :/ fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0] dialog.destroy() if response != Gtk.ResponseType.OK: return # Save dir for next time, unless empty str (which means use cwd). if mpl.rcParams['savefig.directory']: mpl.rcParams['savefig.directory'] = os.path.dirname(fname) try: self.canvas.figure.savefig(fname, format=fmt) except Exception as e: error_msg_gtk(str(e), parent=self) def configure_subplots(self, button): toolfig = Figure(figsize=(6, 3)) canvas = type(self.canvas)(toolfig) toolfig.subplots_adjust(top=0.9) # Need to keep a reference to the tool. _tool = SubplotTool(self.canvas.figure, toolfig) w = int(toolfig.bbox.width) h = int(toolfig.bbox.height) window = Gtk.Window() try: window.set_icon_from_file(window_icon) except Exception: # we presumably already logged a message on the # failure of the main plot, don't keep reporting pass window.set_title("Subplot Configuration Tool") window.set_default_size(w, h) vbox = Gtk.Box() vbox.set_property("orientation", Gtk.Orientation.VERTICAL) window.add(vbox) vbox.show() canvas.show() vbox.pack_start(canvas, True, True, 0) window.show() def set_history_buttons(self): can_backward = self._nav_stack._pos > 0 can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 if 'Back' in self._gtk_ids: self._gtk_ids['Back'].set_sensitive(can_backward) if 'Forward' in self._gtk_ids: self._gtk_ids['Forward'].set_sensitive(can_forward) class ToolbarGTK3(ToolContainerBase, Gtk.Box): _icon_extension = '-symbolic.svg' def __init__(self, toolmanager): ToolContainerBase.__init__(self, toolmanager) Gtk.Box.__init__(self) self.set_property('orientation', Gtk.Orientation.HORIZONTAL) self._message = Gtk.Label() self.pack_end(self._message, False, False, 0) self.show_all() self._groups = {} self._toolitems = {} def add_toolitem(self, name, group, position, image_file, description, toggle): if toggle: tbutton = Gtk.ToggleToolButton() else: tbutton = Gtk.ToolButton() tbutton.set_label(name) if image_file is not None: image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string(image_file), Gtk.IconSize.LARGE_TOOLBAR) tbutton.set_icon_widget(image) if position is None: position = -1 self._add_button(tbutton, group, position) signal = tbutton.connect('clicked', self._call_tool, name) tbutton.set_tooltip_text(description) tbutton.show_all() self._toolitems.setdefault(name, []) self._toolitems[name].append((tbutton, signal)) def _add_button(self, button, group, position): if group not in self._groups: if self._groups: self._add_separator() toolbar = Gtk.Toolbar() toolbar.set_style(Gtk.ToolbarStyle.ICONS) self.pack_start(toolbar, False, False, 0) toolbar.show_all() self._groups[group] = toolbar self._groups[group].insert(button, position) def _call_tool(self, btn, name): self.trigger_tool(name) def toggle_toolitem(self, name, toggled): if name not in self._toolitems: return for toolitem, signal in self._toolitems[name]: toolitem.handler_block(signal) toolitem.set_active(toggled) toolitem.handler_unblock(signal) def remove_toolitem(self, name): if name not in self._toolitems: self.toolmanager.message_event('%s Not in toolbar' % name, self) return for group in self._groups: for toolitem, _signal in self._toolitems[name]: if toolitem in self._groups[group]: self._groups[group].remove(toolitem) del self._toolitems[name] def _add_separator(self): sep = Gtk.Separator() sep.set_property("orientation", Gtk.Orientation.VERTICAL) self.pack_start(sep, False, True, 0) sep.show_all() def set_message(self, s): self._message.set_label(s) @cbook.deprecated("3.3") class StatusbarGTK3(StatusbarBase, Gtk.Statusbar): def __init__(self, *args, **kwargs): StatusbarBase.__init__(self, *args, **kwargs) Gtk.Statusbar.__init__(self) self._context = self.get_context_id('message') def set_message(self, s): self.pop(self._context) self.push(self._context, s) class RubberbandGTK3(backend_tools.RubberbandBase): def draw_rubberband(self, x0, y0, x1, y1): NavigationToolbar2GTK3.draw_rubberband( self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) def remove_rubberband(self): NavigationToolbar2GTK3.remove_rubberband( self._make_classic_style_pseudo_toolbar()) class SaveFigureGTK3(backend_tools.SaveFigureBase): def trigger(self, *args, **kwargs): class PseudoToolbar: canvas = self.figure.canvas return NavigationToolbar2GTK3.save_figure(PseudoToolbar()) class SetCursorGTK3(backend_tools.SetCursorBase): def set_cursor(self, cursor): NavigationToolbar2GTK3.set_cursor( self._make_classic_style_pseudo_toolbar(), cursor) class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): @cbook.deprecated("3.2") @property def window(self): if not hasattr(self, "_window"): self._window = None return self._window @window.setter @cbook.deprecated("3.2") def window(self, window): self._window = window @cbook.deprecated("3.2") def init_window(self): if self.window: return self.window = Gtk.Window(title="Subplot Configuration Tool") try: self.window.window.set_icon_from_file(window_icon) except Exception: # we presumably already logged a message on the # failure of the main plot, don't keep reporting pass self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) self.window.add(self.vbox) self.vbox.show() self.window.connect('destroy', self.destroy) toolfig = Figure(figsize=(6, 3)) canvas = self.figure.canvas.__class__(toolfig) toolfig.subplots_adjust(top=0.9) SubplotTool(self.figure, toolfig) w = int(toolfig.bbox.width) h = int(toolfig.bbox.height) self.window.set_default_size(w, h) canvas.show() self.vbox.pack_start(canvas, True, True, 0) self.window.show() @cbook.deprecated("3.2") def destroy(self, *args): self.window.destroy() self.window = None def _get_canvas(self, fig): return self.canvas.__class__(fig) def trigger(self, *args): NavigationToolbar2GTK3.configure_subplots( self._make_classic_style_pseudo_toolbar(), None) class HelpGTK3(backend_tools.ToolHelpBase): def _normalize_shortcut(self, key): """ Convert Matplotlib key presses to GTK+ accelerator identifiers. Related to `FigureCanvasGTK3._get_key`. """ special = { 'backspace': 'BackSpace', 'pagedown': 'Page_Down', 'pageup': 'Page_Up', 'scroll_lock': 'Scroll_Lock', } parts = key.split('+') mods = ['<' + mod + '>' for mod in parts[:-1]] key = parts[-1] if key in special: key = special[key] elif len(key) > 1: key = key.capitalize() elif key.isupper(): mods += [''] return ''.join(mods) + key def _is_valid_shortcut(self, key): """ Check for a valid shortcut to be displayed. - GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`). - The shortcut window only shows keyboard shortcuts, not mouse buttons. """ return 'cmd+' not in key and not key.startswith('MouseButton.') def _show_shortcuts_window(self): section = Gtk.ShortcutsSection() for name, tool in sorted(self.toolmanager.tools.items()): if not tool.description: continue # Putting everything in a separate group allows GTK to # automatically split them into separate columns/pages, which is # useful because we have lots of shortcuts, some with many keys # that are very wide. group = Gtk.ShortcutsGroup() section.add(group) # A hack to remove the title since we have no group naming. group.forall(lambda widget, data: widget.set_visible(False), None) shortcut = Gtk.ShortcutsShortcut( accelerator=' '.join( self._normalize_shortcut(key) for key in self.toolmanager.get_tool_keymap(name) if self._is_valid_shortcut(key)), title=tool.name, subtitle=tool.description) group.add(shortcut) window = Gtk.ShortcutsWindow( title='Help', modal=True, transient_for=self._figure.canvas.get_toplevel()) section.show() # Must be done explicitly before add! window.add(section) window.show_all() def _show_shortcuts_dialog(self): dialog = Gtk.MessageDialog( self._figure.canvas.get_toplevel(), 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(), title="Help") dialog.run() dialog.destroy() def trigger(self, *args): if Gtk.check_version(3, 20, 0) is None: self._show_shortcuts_window() else: self._show_shortcuts_dialog() class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase): def trigger(self, *args, **kwargs): clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) window = self.canvas.get_window() x, y, width, height = window.get_geometry() pb = Gdk.pixbuf_get_from_window(window, x, y, width, height) clipboard.set_image(pb) # Define the file to use as the GTk icon if sys.platform == 'win32': icon_filename = 'matplotlib.png' else: icon_filename = 'matplotlib.svg' window_icon = str(cbook._get_data_path('images', icon_filename)) def error_msg_gtk(msg, parent=None): if parent is not None: # find the toplevel Gtk.Window parent = parent.get_toplevel() if not parent.is_toplevel(): parent = None if not isinstance(msg, str): msg = ','.join(map(str, msg)) dialog = Gtk.MessageDialog( parent=parent, type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, message_format=msg) dialog.run() dialog.destroy() backend_tools.ToolSaveFigure = SaveFigureGTK3 backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 backend_tools.ToolSetCursor = SetCursorGTK3 backend_tools.ToolRubberband = RubberbandGTK3 backend_tools.ToolHelp = HelpGTK3 backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3 Toolbar = ToolbarGTK3 @_Backend.export class _BackendGTK3(_Backend): FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 @staticmethod def trigger_manager_draw(manager): manager.canvas.draw_idle() @staticmethod def mainloop(): if Gtk.main_level() == 0: Gtk.main()