aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py
diff options
context:
space:
mode:
authorshumkovnd <shumkovnd@yandex-team.com>2023-11-10 14:39:34 +0300
committershumkovnd <shumkovnd@yandex-team.com>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py')
-rw-r--r--contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py587
1 files changed, 587 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py b/contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py
new file mode 100644
index 00000000000..d6acd5547b8
--- /dev/null
+++ b/contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py
@@ -0,0 +1,587 @@
+import functools
+import logging
+import os
+from pathlib import Path
+
+import matplotlib as mpl
+from matplotlib import _api, backend_tools, cbook
+from matplotlib.backend_bases import (
+ ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
+ ResizeEvent)
+
+try:
+ import gi
+except ImportError as err:
+ raise ImportError("The GTK3 backends require PyGObject") from err
+
+try:
+ # :raises ValueError: If module/version is already loaded, already
+ # required, or unavailable.
+ gi.require_version("Gtk", "3.0")
+except ValueError as e:
+ # in this case we want to re-raise as ImportError so the
+ # auto-backend selection logic correctly skips.
+ raise ImportError(e) from e
+
+from gi.repository import Gio, GLib, GObject, Gtk, Gdk
+from . import _backend_gtk
+from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
+ _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
+ TimerGTK as TimerGTK3,
+)
+
+
+_log = logging.getLogger(__name__)
+
+
+@functools.cache
+def _mpl_to_gtk_cursor(mpl_cursor):
+ return Gdk.Cursor.new_from_name(
+ Gdk.Display.get_default(),
+ _backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
+
+
+class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
+ required_interactive_framework = "gtk3"
+ manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
+ # Setting this as a static constant prevents
+ # this resulting expression from leaking
+ event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
+ | Gdk.EventMask.BUTTON_RELEASE_MASK
+ | Gdk.EventMask.EXPOSURE_MASK
+ | Gdk.EventMask.KEY_PRESS_MASK
+ | Gdk.EventMask.KEY_RELEASE_MASK
+ | Gdk.EventMask.ENTER_NOTIFY_MASK
+ | Gdk.EventMask.LEAVE_NOTIFY_MASK
+ | Gdk.EventMask.POINTER_MOTION_MASK
+ | Gdk.EventMask.SCROLL_MASK)
+
+ def __init__(self, figure=None):
+ super().__init__(figure=figure)
+
+ self._idle_draw_id = 0
+ self._rubberband_rect = None
+
+ self.connect('scroll_event', self.scroll_event)
+ self.connect('button_press_event', self.button_press_event)
+ self.connect('button_release_event', self.button_release_event)
+ self.connect('configure_event', self.configure_event)
+ self.connect('screen-changed', self._update_device_pixel_ratio)
+ self.connect('notify::scale-factor', self._update_device_pixel_ratio)
+ self.connect('draw', self.on_draw_event)
+ self.connect('draw', self._post_draw)
+ self.connect('key_press_event', self.key_press_event)
+ self.connect('key_release_event', self.key_release_event)
+ self.connect('motion_notify_event', self.motion_notify_event)
+ self.connect('enter_notify_event', self.enter_notify_event)
+ self.connect('leave_notify_event', self.leave_notify_event)
+ self.connect('size_allocate', self.size_allocate)
+
+ self.set_events(self.__class__.event_mask)
+
+ self.set_can_focus(True)
+
+ css = Gtk.CssProvider()
+ css.load_from_data(b".matplotlib-canvas { background-color: white; }")
+ style_ctx = self.get_style_context()
+ style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+ style_ctx.add_class("matplotlib-canvas")
+
+ def destroy(self):
+ CloseEvent("close_event", self)._process()
+
+ def set_cursor(self, cursor):
+ # docstring inherited
+ window = self.get_property("window")
+ if window is not None:
+ window.set_cursor(_mpl_to_gtk_cursor(cursor))
+ context = GLib.MainContext.default()
+ context.iteration(True)
+
+ def _mpl_coords(self, event=None):
+ """
+ Convert the position of a GTK event, or of the current cursor position
+ if *event* is None, to Matplotlib coordinates.
+
+ GTK use logical pixels, but the figure is scaled to physical pixels for
+ rendering. Transform to physical pixels so that all of the down-stream
+ transforms work as expected.
+
+ Also, the origin is different and needs to be corrected.
+ """
+ if event is None:
+ window = self.get_window()
+ t, x, y, state = window.get_device_position(
+ window.get_display().get_device_manager().get_client_pointer())
+ else:
+ x, y = event.x, event.y
+ x = x * self.device_pixel_ratio
+ # flip y so y=0 is bottom of canvas
+ y = self.figure.bbox.height - y * self.device_pixel_ratio
+ return x, y
+
+ def scroll_event(self, widget, event):
+ step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
+ MouseEvent("scroll_event", self,
+ *self._mpl_coords(event), step=step,
+ modifiers=self._mpl_modifiers(event.state),
+ guiEvent=event)._process()
+ return False # finish event propagation?
+
+ def button_press_event(self, widget, event):
+ MouseEvent("button_press_event", self,
+ *self._mpl_coords(event), event.button,
+ modifiers=self._mpl_modifiers(event.state),
+ guiEvent=event)._process()
+ return False # finish event propagation?
+
+ def button_release_event(self, widget, event):
+ MouseEvent("button_release_event", self,
+ *self._mpl_coords(event), event.button,
+ modifiers=self._mpl_modifiers(event.state),
+ guiEvent=event)._process()
+ return False # finish event propagation?
+
+ def key_press_event(self, widget, event):
+ KeyEvent("key_press_event", self,
+ self._get_key(event), *self._mpl_coords(),
+ guiEvent=event)._process()
+ return True # stop event propagation
+
+ def key_release_event(self, widget, event):
+ KeyEvent("key_release_event", self,
+ self._get_key(event), *self._mpl_coords(),
+ guiEvent=event)._process()
+ return True # stop event propagation
+
+ def motion_notify_event(self, widget, event):
+ MouseEvent("motion_notify_event", self, *self._mpl_coords(event),
+ modifiers=self._mpl_modifiers(event.state),
+ guiEvent=event)._process()
+ return False # finish event propagation?
+
+ def enter_notify_event(self, widget, event):
+ gtk_mods = Gdk.Keymap.get_for_display(
+ self.get_display()).get_modifier_state()
+ LocationEvent("figure_enter_event", self, *self._mpl_coords(event),
+ modifiers=self._mpl_modifiers(gtk_mods),
+ guiEvent=event)._process()
+
+ def leave_notify_event(self, widget, event):
+ gtk_mods = Gdk.Keymap.get_for_display(
+ self.get_display()).get_modifier_state()
+ LocationEvent("figure_leave_event", self, *self._mpl_coords(event),
+ modifiers=self._mpl_modifiers(gtk_mods),
+ guiEvent=event)._process()
+
+ def size_allocate(self, widget, allocation):
+ dpival = self.figure.dpi
+ winch = allocation.width * self.device_pixel_ratio / dpival
+ hinch = allocation.height * self.device_pixel_ratio / dpival
+ self.figure.set_size_inches(winch, hinch, forward=False)
+ ResizeEvent("resize_event", self)._process()
+ self.draw_idle()
+
+ @staticmethod
+ def _mpl_modifiers(event_state, *, exclude=None):
+ modifiers = [
+ ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
+ ("alt", Gdk.ModifierType.MOD1_MASK, "alt"),
+ ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
+ ("super", Gdk.ModifierType.MOD4_MASK, "super"),
+ ]
+ return [name for name, mask, key in modifiers
+ if exclude != key and event_state & mask]
+
+ def _get_key(self, event):
+ unikey = chr(Gdk.keyval_to_unicode(event.keyval))
+ key = cbook._unikey_or_keysym_to_mplkey(
+ unikey, Gdk.keyval_name(event.keyval))
+ mods = self._mpl_modifiers(event.state, exclude=key)
+ if "shift" in mods and unikey.isprintable():
+ mods.remove("shift")
+ return "+".join([*mods, key])
+
+ def _update_device_pixel_ratio(self, *args, **kwargs):
+ # We need to be careful in cases with mixed resolution displays if
+ # device_pixel_ratio changes.
+ if self._set_device_pixel_ratio(self.get_scale_factor()):
+ # The easiest way to resize the canvas is to emit a resize event
+ # since we implement all the logic for resizing the canvas for that
+ # event.
+ self.queue_resize()
+ self.queue_draw()
+
+ def configure_event(self, widget, event):
+ if widget.get_property("window") is None:
+ return
+ w = event.width * self.device_pixel_ratio
+ h = event.height * self.device_pixel_ratio
+ if w < 3 or h < 3:
+ return # empty fig
+ # resize the figure (in inches)
+ dpi = self.figure.dpi
+ self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
+ return False # finish event propagation?
+
+ def _draw_rubberband(self, rect):
+ self._rubberband_rect = rect
+ # TODO: Only update the rubberband area.
+ self.queue_draw()
+
+ def _post_draw(self, widget, ctx):
+ if self._rubberband_rect is None:
+ return
+
+ x0, y0, w, h = (dim / self.device_pixel_ratio
+ for dim in self._rubberband_rect)
+ x1 = x0 + w
+ y1 = y0 + h
+
+ # Draw the lines from x0, y0 towards x1, y1 so that the
+ # dashes don't "jump" when moving the zoom box.
+ ctx.move_to(x0, y0)
+ ctx.line_to(x0, y1)
+ ctx.move_to(x0, y0)
+ ctx.line_to(x1, y0)
+ ctx.move_to(x0, y1)
+ ctx.line_to(x1, y1)
+ ctx.move_to(x1, y0)
+ ctx.line_to(x1, y1)
+
+ ctx.set_antialias(1)
+ ctx.set_line_width(1)
+ ctx.set_dash((3, 3), 0)
+ ctx.set_source_rgb(0, 0, 0)
+ ctx.stroke_preserve()
+
+ ctx.set_dash((3, 3), 3)
+ ctx.set_source_rgb(1, 1, 1)
+ ctx.stroke()
+
+ def on_draw_event(self, widget, ctx):
+ # to be overwritten by GTK3Agg or GTK3Cairo
+ pass
+
+ def draw(self):
+ # docstring inherited
+ if self.is_drawable():
+ self.queue_draw()
+
+ def draw_idle(self):
+ # docstring inherited
+ if self._idle_draw_id != 0:
+ return
+ def idle_draw(*args):
+ try:
+ self.draw()
+ finally:
+ self._idle_draw_id = 0
+ return False
+ self._idle_draw_id = GLib.idle_add(idle_draw)
+
+ def flush_events(self):
+ # docstring inherited
+ context = GLib.MainContext.default()
+ while context.pending():
+ context.iteration(True)
+
+
+class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar):
+ def __init__(self, canvas):
+ GObject.GObject.__init__(self)
+
+ self.set_style(Gtk.ToolbarStyle.ICONS)
+
+ self._gtk_ids = {}
+ for text, tooltip_text, image_file, callback in self.toolitems:
+ if text is None:
+ self.insert(Gtk.SeparatorToolItem(), -1)
+ continue
+ image = Gtk.Image.new_from_gicon(
+ Gio.Icon.new_for_string(
+ str(cbook._get_data_path('images',
+ f'{image_file}-symbolic.svg'))),
+ Gtk.IconSize.LARGE_TOOLBAR)
+ self._gtk_ids[text] = button = (
+ Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
+ Gtk.ToolButton())
+ button.set_label(text)
+ button.set_icon_widget(image)
+ # Save the handler id, so that we can block it as needed.
+ button._signal_handler = button.connect(
+ 'clicked', getattr(self, callback))
+ button.set_tooltip_text(tooltip_text)
+ self.insert(button, -1)
+
+ # This filler item ensures the toolbar is always at least two text
+ # lines high. Otherwise the canvas gets redrawn as the mouse hovers
+ # over images because those use two-line messages which resize the
+ # toolbar.
+ toolitem = Gtk.ToolItem()
+ self.insert(toolitem, -1)
+ label = Gtk.Label()
+ label.set_markup(
+ '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
+ toolitem.set_expand(True) # Push real message to the right.
+ toolitem.add(label)
+
+ toolitem = Gtk.ToolItem()
+ self.insert(toolitem, -1)
+ self.message = Gtk.Label()
+ self.message.set_justify(Gtk.Justification.RIGHT)
+ toolitem.add(self.message)
+
+ self.show_all()
+
+ _NavigationToolbar2GTK.__init__(self, canvas)
+
+ def save_figure(self, *args):
+ dialog = Gtk.FileChooserDialog(
+ title="Save the figure",
+ parent=self.canvas.get_toplevel(),
+ action=Gtk.FileChooserAction.SAVE,
+ buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
+ )
+ for name, fmts \
+ in self.canvas.get_supported_filetypes_grouped().items():
+ ff = Gtk.FileFilter()
+ ff.set_name(name)
+ for fmt in fmts:
+ ff.add_pattern(f'*.{fmt}')
+ dialog.add_filter(ff)
+ if self.canvas.get_default_filetype() in fmts:
+ dialog.set_filter(ff)
+
+ @functools.partial(dialog.connect, "notify::filter")
+ def on_notify_filter(*args):
+ name = dialog.get_filter().get_name()
+ fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
+ dialog.set_current_name(
+ str(Path(dialog.get_current_name()).with_suffix(f'.{fmt}')))
+
+ dialog.set_current_folder(mpl.rcParams["savefig.directory"])
+ dialog.set_current_name(self.canvas.get_default_filename())
+ dialog.set_do_overwrite_confirmation(True)
+
+ response = dialog.run()
+ fname = dialog.get_filename()
+ ff = dialog.get_filter() # Doesn't autoadjust to filename :/
+ fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
+ dialog.destroy()
+ if response != Gtk.ResponseType.OK:
+ return
+ # Save dir for next time, unless empty str (which means use cwd).
+ if mpl.rcParams['savefig.directory']:
+ mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
+ try:
+ self.canvas.figure.savefig(fname, format=fmt)
+ except Exception as e:
+ dialog = Gtk.MessageDialog(
+ parent=self.canvas.get_toplevel(), message_format=str(e),
+ type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
+ dialog.run()
+ dialog.destroy()
+
+
+class ToolbarGTK3(ToolContainerBase, Gtk.Box):
+ _icon_extension = '-symbolic.svg'
+
+ def __init__(self, toolmanager):
+ ToolContainerBase.__init__(self, toolmanager)
+ Gtk.Box.__init__(self)
+ self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
+ self._message = Gtk.Label()
+ self._message.set_justify(Gtk.Justification.RIGHT)
+ self.pack_end(self._message, False, False, 0)
+ self.show_all()
+ self._groups = {}
+ self._toolitems = {}
+
+ def add_toolitem(self, name, group, position, image_file, description,
+ toggle):
+ if toggle:
+ button = Gtk.ToggleToolButton()
+ else:
+ button = Gtk.ToolButton()
+ button.set_label(name)
+
+ if image_file is not None:
+ image = Gtk.Image.new_from_gicon(
+ Gio.Icon.new_for_string(image_file),
+ Gtk.IconSize.LARGE_TOOLBAR)
+ button.set_icon_widget(image)
+
+ if position is None:
+ position = -1
+
+ self._add_button(button, group, position)
+ signal = button.connect('clicked', self._call_tool, name)
+ button.set_tooltip_text(description)
+ button.show_all()
+ self._toolitems.setdefault(name, [])
+ self._toolitems[name].append((button, signal))
+
+ def _add_button(self, button, group, position):
+ if group not in self._groups:
+ if self._groups:
+ self._add_separator()
+ toolbar = Gtk.Toolbar()
+ toolbar.set_style(Gtk.ToolbarStyle.ICONS)
+ self.pack_start(toolbar, False, False, 0)
+ toolbar.show_all()
+ self._groups[group] = toolbar
+ self._groups[group].insert(button, position)
+
+ def _call_tool(self, btn, name):
+ self.trigger_tool(name)
+
+ def toggle_toolitem(self, name, toggled):
+ if name not in self._toolitems:
+ return
+ for toolitem, signal in self._toolitems[name]:
+ toolitem.handler_block(signal)
+ toolitem.set_active(toggled)
+ toolitem.handler_unblock(signal)
+
+ def remove_toolitem(self, name):
+ if name not in self._toolitems:
+ self.toolmanager.message_event(f'{name} not in toolbar', self)
+ return
+
+ for group in self._groups:
+ for toolitem, _signal in self._toolitems[name]:
+ if toolitem in self._groups[group]:
+ self._groups[group].remove(toolitem)
+ del self._toolitems[name]
+
+ def _add_separator(self):
+ sep = Gtk.Separator()
+ sep.set_property("orientation", Gtk.Orientation.VERTICAL)
+ self.pack_start(sep, False, True, 0)
+ sep.show_all()
+
+ def set_message(self, s):
+ self._message.set_label(s)
+
+
+@backend_tools._register_tool_class(FigureCanvasGTK3)
+class SaveFigureGTK3(backend_tools.SaveFigureBase):
+ def trigger(self, *args, **kwargs):
+ NavigationToolbar2GTK3.save_figure(
+ self._make_classic_style_pseudo_toolbar())
+
+
+@backend_tools._register_tool_class(FigureCanvasGTK3)
+class HelpGTK3(backend_tools.ToolHelpBase):
+ def _normalize_shortcut(self, key):
+ """
+ Convert Matplotlib key presses to GTK+ accelerator identifiers.
+
+ Related to `FigureCanvasGTK3._get_key`.
+ """
+ special = {
+ 'backspace': 'BackSpace',
+ 'pagedown': 'Page_Down',
+ 'pageup': 'Page_Up',
+ 'scroll_lock': 'Scroll_Lock',
+ }
+
+ parts = key.split('+')
+ mods = ['<' + mod + '>' for mod in parts[:-1]]
+ key = parts[-1]
+
+ if key in special:
+ key = special[key]
+ elif len(key) > 1:
+ key = key.capitalize()
+ elif key.isupper():
+ mods += ['<shift>']
+
+ return ''.join(mods) + key
+
+ def _is_valid_shortcut(self, key):
+ """
+ Check for a valid shortcut to be displayed.
+
+ - GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
+ - The shortcut window only shows keyboard shortcuts, not mouse buttons.
+ """
+ return 'cmd+' not in key and not key.startswith('MouseButton.')
+
+ def _show_shortcuts_window(self):
+ section = Gtk.ShortcutsSection()
+
+ for name, tool in sorted(self.toolmanager.tools.items()):
+ if not tool.description:
+ continue
+
+ # Putting everything in a separate group allows GTK to
+ # automatically split them into separate columns/pages, which is
+ # useful because we have lots of shortcuts, some with many keys
+ # that are very wide.
+ group = Gtk.ShortcutsGroup()
+ section.add(group)
+ # A hack to remove the title since we have no group naming.
+ group.forall(lambda widget, data: widget.set_visible(False), None)
+
+ shortcut = Gtk.ShortcutsShortcut(
+ accelerator=' '.join(
+ self._normalize_shortcut(key)
+ for key in self.toolmanager.get_tool_keymap(name)
+ if self._is_valid_shortcut(key)),
+ title=tool.name,
+ subtitle=tool.description)
+ group.add(shortcut)
+
+ window = Gtk.ShortcutsWindow(
+ title='Help',
+ modal=True,
+ transient_for=self._figure.canvas.get_toplevel())
+ section.show() # Must be done explicitly before add!
+ window.add(section)
+
+ window.show_all()
+
+ def _show_shortcuts_dialog(self):
+ dialog = Gtk.MessageDialog(
+ self._figure.canvas.get_toplevel(),
+ 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
+ title="Help")
+ dialog.run()
+ dialog.destroy()
+
+ def trigger(self, *args):
+ if Gtk.check_version(3, 20, 0) is None:
+ self._show_shortcuts_window()
+ else:
+ self._show_shortcuts_dialog()
+
+
+@backend_tools._register_tool_class(FigureCanvasGTK3)
+class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
+ def trigger(self, *args, **kwargs):
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ window = self.canvas.get_window()
+ x, y, width, height = window.get_geometry()
+ pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
+ clipboard.set_image(pb)
+
+
+Toolbar = ToolbarGTK3
+backend_tools._register_tool_class(
+ FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK)
+backend_tools._register_tool_class(
+ FigureCanvasGTK3, _backend_gtk.RubberbandGTK)
+
+
+class FigureManagerGTK3(_FigureManagerGTK):
+ _toolbar2_class = NavigationToolbar2GTK3
+ _toolmanager_toolbar_class = ToolbarGTK3
+
+
+@_BackendGTK.export
+class _BackendGTK3(_BackendGTK):
+ FigureCanvas = FigureCanvasGTK3
+ FigureManager = FigureManagerGTK3