diff options
author | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 14:39:34 +0300 |
---|---|---|
committer | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 16:42:24 +0300 |
commit | 77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch) | |
tree | c51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/matplotlib/backends/backend_gtk3.py | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-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.py | 587 |
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 |