Initial commit
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user