From bb3c03999e4c3a84ef42ebe040f4bec2597a2822 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 18 Dec 2021 14:08:05 +0100 Subject: [PATCH] Share FigureManager class between gtk3 and gtk4. The gtk version checks are not the nicest, but sharing most of the setup code between gtk3 and gtk4 still seems worthwhile? (Note that on gtk3, it is explicitly OK to not bother to destroy the vbox and toolbar, as gtk3 always destroys child widgets upon parent destruction.) --- lib/matplotlib/backends/_backend_gtk.py | 135 ++++++++++++++++++++++- lib/matplotlib/backends/backend_gtk3.py | 137 ++---------------------- lib/matplotlib/backends/backend_gtk4.py | 109 ++----------------- 3 files changed, 147 insertions(+), 234 deletions(-) diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index c2224a589b86..e53332ef4583 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -3,15 +3,18 @@ """ import logging +import sys import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib.backend_bases import _Backend, NavigationToolbar2, TimerBase +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import ( + _Backend, FigureManagerBase, NavigationToolbar2, TimerBase) from matplotlib.backend_tools import Cursors # The GTK3/GTK4 backends will have already called `gi.require_version` to set # the desired GTK. -from gi.repository import Gio, GLib, Gtk +from gi.repository import Gdk, Gio, GLib, Gtk _log = logging.getLogger(__name__) @@ -109,6 +112,134 @@ def _on_timer(self): return False +class _FigureManagerGTK(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : Gtk.Toolbar or Gtk.Box + The toolbar + vbox : Gtk.VBox + The Gtk.VBox containing the canvas and toolbar + window : Gtk.Window + The Gtk.Window + """ + + def __init__(self, canvas, num): + self._gtk_ver = gtk_ver = Gtk.get_major_version() + + app = _create_application() + self.window = Gtk.Window() + app.add_window(self.window) + super().__init__(canvas, num) + + if gtk_ver == 3: + self.window.set_wmclass("matplotlib", "Matplotlib") + icon_ext = "png" if sys.platform == "win32" else "svg" + self.window.set_icon_from_file( + str(cbook._get_data_path(f"images/matplotlib.{icon_ext}"))) + + self.vbox = Gtk.Box() + self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) + + if gtk_ver == 3: + self.window.add(self.vbox) + self.vbox.show() + self.canvas.show() + self.vbox.pack_start(self.canvas, True, True, 0) + elif gtk_ver == 4: + self.window.set_child(self.vbox) + self.vbox.prepend(self.canvas) + + # calculate size for window + w, h = self.canvas.get_width_height() + + if self.toolbar is not None: + if gtk_ver == 3: + self.toolbar.show() + self.vbox.pack_end(self.toolbar, False, False, 0) + elif gtk_ver == 4: + sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) + sw.set_child(self.toolbar) + self.vbox.append(sw) + min_size, nat_size = self.toolbar.get_preferred_size() + h += nat_size.height + + self.window.set_default_size(w, h) + + self._destroying = False + self.window.connect("destroy", lambda *args: Gcf.destroy(self)) + self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver], + 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.window.destroy() + self.canvas.destroy() + + def show(self): + # show the figure window + self.window.show() + self.canvas.draw() + if mpl.rcParams["figure.raise_window"]: + meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver] + if getattr(self.window, meth_name)(): + self.window.present() + else: + # If this is called by a callback early during init, + # self.window (a GtkWindow) may not have an associated + # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet, + # and present() would crash. + _api.warn_external("Cannot raise window yet to be setup") + + def full_screen_toggle(self): + is_fullscreen = { + 3: lambda w: (w.get_window().get_state() + & Gdk.WindowState.FULLSCREEN), + 4: lambda w: w.is_fullscreen(), + }[self._gtk_ver] + if is_fullscreen(self.window): + self.window.unfullscreen() + else: + self.window.fullscreen() + + 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): + width = int(width / self.canvas.device_pixel_ratio) + height = int(height / self.canvas.device_pixel_ratio) + if self.toolbar: + toolbar_size = self.toolbar.size_request() + height += toolbar_size.height + canvas_size = self.canvas.get_allocation() + if self._gtk_ver >= 4 or 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 _NavigationToolbar2GTK(NavigationToolbar2): # Must be implemented in GTK3/GTK4 backends: # * __init__ diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 60073a2d8b1e..7796726a6778 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -6,9 +6,7 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import ( - FigureCanvasBase, FigureManagerBase, ToolContainerBase) +from matplotlib.backend_bases import FigureCanvasBase, ToolContainerBase from matplotlib.backend_tools import Cursors from matplotlib.figure import Figure @@ -29,7 +27,7 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk from . import _backend_gtk from ._backend_gtk import ( - backend_version, _BackendGTK, _NavigationToolbar2GTK, + backend_version, _BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK, TimerGTK as TimerGTK3, ) @@ -293,130 +291,6 @@ def flush_events(self): context.iteration(True) -class FigureManagerGTK3(FigureManagerBase): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The FigureCanvas instance - num : int or str - The Figure number - toolbar : Gtk.Toolbar - The toolbar - vbox : Gtk.VBox - The Gtk.VBox containing the canvas and toolbar - window : Gtk.Window - The Gtk.Window - """ - - def __init__(self, canvas, num): - app = _backend_gtk._create_application() - self.window = Gtk.Window() - app.add_window(self.window) - super().__init__(canvas, num) - - self.window.set_wmclass("matplotlib", "Matplotlib") - icon_ext = "png" if sys.platform == "win32" else "svg" - self.window.set_icon_from_file( - str(cbook._get_data_path(f"images/matplotlib.{icon_ext}"))) - - 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, h = self.canvas.get_width_height() - - if self.toolbar is not None: - self.toolbar.show() - self.vbox.pack_end(self.toolbar, False, False, 0) - min_size, nat_size = self.toolbar.get_preferred_size() - h += nat_size.height - - 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() - - def show(self): - # show the figure window - self.window.show() - self.canvas.draw() - if mpl.rcParams['figure.raise_window']: - if self.window.get_window(): - self.window.present() - else: - # If this is called by a callback early during init, - # self.window (a GtkWindow) may not have an associated - # low-level GdkWindow (self.window.get_window()) yet, and - # present() would crash. - _api.warn_external("Cannot raise window yet to be setup") - - def full_screen_toggle(self): - if self.window.get_window().get_state() & Gdk.WindowState.FULLSCREEN: - self.window.unfullscreen() - else: - self.window.fullscreen() - - 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) - 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.""" - width = int(width / self.canvas.device_pixel_ratio) - height = int(height / self.canvas.device_pixel_ratio) - 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(_NavigationToolbar2GTK, Gtk.Toolbar): @_api.delete_parameter("3.6", "window") def __init__(self, canvas, window=None): @@ -730,8 +604,11 @@ def error_msg_gtk(msg, parent=None): FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK) backend_tools._register_tool_class( FigureCanvasGTK3, _backend_gtk.RubberbandGTK) -FigureManagerGTK3._toolbar2_class = NavigationToolbar2GTK3 -FigureManagerGTK3._toolmanager_toolbar_class = ToolbarGTK3 + + +class FigureManagerGTK3(_FigureManagerGTK): + _toolbar2_class = NavigationToolbar2GTK3 + _toolmanager_toolbar_class = ToolbarGTK3 @_BackendGTK.export diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index aeb4a7a0dffa..d5508022c109 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -5,9 +5,7 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import ( - FigureCanvasBase, FigureManagerBase, ToolContainerBase) +from matplotlib.backend_bases import FigureCanvasBase, ToolContainerBase try: import gi @@ -26,7 +24,7 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf from . import _backend_gtk from ._backend_gtk import ( - backend_version, _BackendGTK, _NavigationToolbar2GTK, + backend_version, _BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK, TimerGTK as TimerGTK4, ) @@ -249,102 +247,6 @@ def flush_events(self): context.iteration(True) -class FigureManagerGTK4(FigureManagerBase): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The FigureCanvas instance - num : int or str - The Figure number - toolbar : Gtk.Box - The toolbar - vbox : Gtk.VBox - The Gtk.VBox containing the canvas and toolbar - window : Gtk.Window - The Gtk.Window - """ - - def __init__(self, canvas, num): - app = _backend_gtk._create_application() - self.window = Gtk.Window() - app.add_window(self.window) - super().__init__(canvas, num) - - self.vbox = Gtk.Box() - self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) - self.window.set_child(self.vbox) - - self.vbox.prepend(self.canvas) - # calculate size for window - w, h = self.canvas.get_width_height() - - if self.toolbar is not None: - sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) - sw.set_child(self.toolbar) - self.vbox.append(sw) - min_size, nat_size = self.toolbar.get_preferred_size() - h += nat_size.height - - self.window.set_default_size(w, h) - - self._destroying = False - self.window.connect("destroy", lambda *args: Gcf.destroy(self)) - self.window.connect("close-request", 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.window.destroy() - self.canvas.destroy() - - def show(self): - # show the figure window - self.window.show() - self.canvas.draw() - if mpl.rcParams['figure.raise_window']: - if self.window.get_surface(): - self.window.present() - else: - # If this is called by a callback early during init, - # self.window (a GtkWindow) may not have an associated - # low-level GdkSurface (self.window.get_surface()) yet, and - # present() would crash. - _api.warn_external("Cannot raise window yet to be setup") - - def full_screen_toggle(self): - if not self.window.is_fullscreen(): - self.window.fullscreen() - else: - self.window.unfullscreen() - - 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.""" - width = int(width / self.canvas.device_pixel_ratio) - height = int(height / self.canvas.device_pixel_ratio) - if self.toolbar: - min_size, nat_size = self.toolbar.get_preferred_size() - height += nat_size.height - canvas_size = self.canvas.get_allocation() - self.window.set_default_size(width, height) - - class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box): @_api.delete_parameter("3.6", "window") def __init__(self, canvas, window=None): @@ -655,8 +557,11 @@ def trigger(self, *args, **kwargs): backend_tools._register_tool_class( FigureCanvasGTK4, _backend_gtk.RubberbandGTK) Toolbar = ToolbarGTK4 -FigureManagerGTK4._toolbar2_class = NavigationToolbar2GTK4 -FigureManagerGTK4._toolmanager_toolbar_class = ToolbarGTK4 + + +class FigureManagerGTK4(_FigureManagerGTK): + _toolbar2_class = NavigationToolbar2GTK4 + _toolmanager_toolbar_class = ToolbarGTK4 @_BackendGTK.export