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