| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- """
- Common code for GTK3 and GTK4 backends.
- """
- import logging
- import sys
- import matplotlib as mpl
- from matplotlib import _api, backend_tools, cbook
- from matplotlib._pylab_helpers import Gcf
- from matplotlib.backend_bases import (
- _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
- TimerBase)
- from matplotlib.backend_tools import Cursors
- import gi
- # The GTK3/GTK4 backends will have already called `gi.require_version` to set
- # the desired GTK.
- from gi.repository import Gdk, Gio, GLib, Gtk
- try:
- gi.require_foreign("cairo")
- except ImportError as e:
- raise ImportError("Gtk-based backends require cairo") from e
- _log = logging.getLogger(__name__)
- _application = None # Placeholder
- def _shutdown_application(app):
- # The application might prematurely shut down if Ctrl-C'd out of IPython,
- # so close all windows.
- for win in app.get_windows():
- win.close()
- # The PyGObject wrapper incorrectly thinks that None is not allowed, or we
- # would call this:
- # Gio.Application.set_default(None)
- # Instead, we set this property and ignore default applications with it:
- app._created_by_matplotlib = True
- global _application
- _application = None
- def _create_application():
- global _application
- if _application is None:
- app = Gio.Application.get_default()
- if app is None or getattr(app, '_created_by_matplotlib', False):
- # display_is_valid returns False only if on Linux and neither X11
- # nor Wayland display can be opened.
- if not mpl._c_internal_utils.display_is_valid():
- raise RuntimeError('Invalid DISPLAY variable')
- _application = Gtk.Application.new('org.matplotlib.Matplotlib3',
- Gio.ApplicationFlags.NON_UNIQUE)
- # The activate signal must be connected, but we don't care for
- # handling it, since we don't do any remote processing.
- _application.connect('activate', lambda *args, **kwargs: None)
- _application.connect('shutdown', _shutdown_application)
- _application.register()
- cbook._setup_new_guiapp()
- else:
- _application = app
- return _application
- def mpl_to_gtk_cursor_name(mpl_cursor):
- return _api.check_getitem({
- Cursors.MOVE: "move",
- Cursors.HAND: "pointer",
- Cursors.POINTER: "default",
- Cursors.SELECT_REGION: "crosshair",
- Cursors.WAIT: "wait",
- Cursors.RESIZE_HORIZONTAL: "ew-resize",
- Cursors.RESIZE_VERTICAL: "ns-resize",
- }, cursor=mpl_cursor)
- class TimerGTK(TimerBase):
- """Subclass of `.TimerBase` using GTK timer events."""
- def __init__(self, *args, **kwargs):
- self._timer = None
- super().__init__(*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):
- super()._on_timer()
- # 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 _FigureCanvasGTK(FigureCanvasBase):
- _timer_cls = TimerGTK
- 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:
- 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()
- @classmethod
- def start_main_loop(cls):
- global _application
- if _application is None:
- return
- try:
- _application.run() # Quits when all added windows close.
- except KeyboardInterrupt:
- # Ensure all windows can process their close event from
- # _shutdown_application.
- context = GLib.MainContext.default()
- while context.pending():
- context.iteration(True)
- raise
- finally:
- # Running after quit is undefined, so create a new one next time.
- _application = None
- 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:
- min_size, nat_size = self.toolbar.get_preferred_size()
- height += nat_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__
- # * save_figure
- def set_message(self, s):
- escaped = GLib.markup_escape_text(s)
- self.message.set_markup(f'<small>{escaped}</small>')
- 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 set_history_buttons(self):
- can_backward = self._nav_stack._pos > 0
- can_forward = self._nav_stack._pos < len(self._nav_stack) - 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 RubberbandGTK(backend_tools.RubberbandBase):
- def draw_rubberband(self, x0, y0, x1, y1):
- _NavigationToolbar2GTK.draw_rubberband(
- self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
- def remove_rubberband(self):
- _NavigationToolbar2GTK.remove_rubberband(
- self._make_classic_style_pseudo_toolbar())
- class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase):
- def trigger(self, *args):
- _NavigationToolbar2GTK.configure_subplots(self, None)
- class _BackendGTK(_Backend):
- backend_version = "{}.{}.{}".format(
- Gtk.get_major_version(),
- Gtk.get_minor_version(),
- Gtk.get_micro_version(),
- )
- mainloop = _FigureManagerGTK.start_main_loop
|