mirror of
https://github.com/kristoferssolo/School.git
synced 2025-10-21 20:10:38 +00:00
182 lines
5.8 KiB
Python
182 lines
5.8 KiB
Python
"""
|
|
Common code for GTK3 and GTK4 backends.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import matplotlib as mpl
|
|
from matplotlib import backend_tools, cbook
|
|
from matplotlib.backend_bases import (
|
|
_Backend, NavigationToolbar2, TimerBase,
|
|
)
|
|
|
|
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
|
|
# the desired GTK.
|
|
from gi.repository import Gio, GLib, Gtk
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
backend_version = "%s.%s.%s" % (
|
|
Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version())
|
|
|
|
# Placeholder
|
|
_application = None
|
|
|
|
|
|
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
|
|
|
|
|
|
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 _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._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 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, Gtk.Window):
|
|
def _get_canvas(self, fig):
|
|
return self.canvas.__class__(fig)
|
|
|
|
def trigger(self, *args):
|
|
_NavigationToolbar2GTK.configure_subplots(
|
|
self._make_classic_style_pseudo_toolbar(), None)
|
|
|
|
|
|
class _BackendGTK(_Backend):
|
|
@staticmethod
|
|
def mainloop():
|
|
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
|