backend_gtk4.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. import functools
  2. import io
  3. import os
  4. import matplotlib as mpl
  5. from matplotlib import _api, backend_tools, cbook
  6. from matplotlib.backend_bases import (
  7. ToolContainerBase, MouseButton,
  8. KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent)
  9. try:
  10. from gi import require_version as gi_require_version
  11. except ImportError as err:
  12. raise ImportError("The GTK4 backends require PyGObject") from err
  13. try:
  14. # :raises ValueError: If module/version is already loaded, already
  15. # required, or unavailable.
  16. gi_require_version("Gtk", "4.0")
  17. except ValueError as e:
  18. # in this case we want to re-raise as ImportError so the
  19. # auto-backend selection logic correctly skips.
  20. raise ImportError(e) from e
  21. import gi
  22. from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
  23. from . import _backend_gtk
  24. from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
  25. _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
  26. TimerGTK as TimerGTK4,
  27. )
  28. _GOBJECT_GE_3_47 = gi.version_info >= (3, 47, 0)
  29. _GTK_GE_4_12 = Gtk.check_version(4, 12, 0) is None
  30. class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
  31. required_interactive_framework = "gtk4"
  32. supports_blit = False
  33. manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
  34. def __init__(self, figure=None):
  35. super().__init__(figure=figure)
  36. self.set_hexpand(True)
  37. self.set_vexpand(True)
  38. self._idle_draw_id = 0
  39. self._rubberband_rect = None
  40. self.set_draw_func(self._draw_func)
  41. self.connect('resize', self.resize_event)
  42. if _GTK_GE_4_12:
  43. self.connect('realize', self._realize_event)
  44. else:
  45. self.connect('notify::scale-factor', self._update_device_pixel_ratio)
  46. click = Gtk.GestureClick()
  47. click.set_button(0) # All buttons.
  48. click.connect('pressed', self.button_press_event)
  49. click.connect('released', self.button_release_event)
  50. self.add_controller(click)
  51. key = Gtk.EventControllerKey()
  52. key.connect('key-pressed', self.key_press_event)
  53. key.connect('key-released', self.key_release_event)
  54. self.add_controller(key)
  55. motion = Gtk.EventControllerMotion()
  56. motion.connect('motion', self.motion_notify_event)
  57. motion.connect('enter', self.enter_notify_event)
  58. motion.connect('leave', self.leave_notify_event)
  59. self.add_controller(motion)
  60. scroll = Gtk.EventControllerScroll.new(
  61. Gtk.EventControllerScrollFlags.VERTICAL)
  62. scroll.connect('scroll', self.scroll_event)
  63. self.add_controller(scroll)
  64. self.set_focusable(True)
  65. css = Gtk.CssProvider()
  66. style = '.matplotlib-canvas { background-color: white; }'
  67. if Gtk.check_version(4, 9, 3) is None:
  68. css.load_from_data(style, -1)
  69. else:
  70. css.load_from_data(style.encode('utf-8'))
  71. style_ctx = self.get_style_context()
  72. style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
  73. style_ctx.add_class("matplotlib-canvas")
  74. def destroy(self):
  75. CloseEvent("close_event", self)._process()
  76. def set_cursor(self, cursor):
  77. # docstring inherited
  78. self.set_cursor_from_name(_backend_gtk.mpl_to_gtk_cursor_name(cursor))
  79. def _mpl_coords(self, xy=None):
  80. """
  81. Convert the *xy* position of a GTK event, or of the current cursor
  82. position if *xy* is None, to Matplotlib coordinates.
  83. GTK use logical pixels, but the figure is scaled to physical pixels for
  84. rendering. Transform to physical pixels so that all of the down-stream
  85. transforms work as expected.
  86. Also, the origin is different and needs to be corrected.
  87. """
  88. if xy is None:
  89. surface = self.get_native().get_surface()
  90. is_over, x, y, mask = surface.get_device_position(
  91. self.get_display().get_default_seat().get_pointer())
  92. else:
  93. x, y = xy
  94. x = x * self.device_pixel_ratio
  95. # flip y so y=0 is bottom of canvas
  96. y = self.figure.bbox.height - y * self.device_pixel_ratio
  97. return x, y
  98. def scroll_event(self, controller, dx, dy):
  99. MouseEvent(
  100. "scroll_event", self, *self._mpl_coords(), step=dy,
  101. modifiers=self._mpl_modifiers(controller),
  102. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  103. )._process()
  104. return True
  105. def button_press_event(self, controller, n_press, x, y):
  106. MouseEvent(
  107. "button_press_event", self, *self._mpl_coords((x, y)),
  108. controller.get_current_button(),
  109. modifiers=self._mpl_modifiers(controller),
  110. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  111. )._process()
  112. self.grab_focus()
  113. def button_release_event(self, controller, n_press, x, y):
  114. MouseEvent(
  115. "button_release_event", self, *self._mpl_coords((x, y)),
  116. controller.get_current_button(),
  117. modifiers=self._mpl_modifiers(controller),
  118. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  119. )._process()
  120. def key_press_event(self, controller, keyval, keycode, state):
  121. KeyEvent(
  122. "key_press_event", self, self._get_key(keyval, keycode, state),
  123. *self._mpl_coords(),
  124. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  125. )._process()
  126. return True
  127. def key_release_event(self, controller, keyval, keycode, state):
  128. KeyEvent(
  129. "key_release_event", self, self._get_key(keyval, keycode, state),
  130. *self._mpl_coords(),
  131. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  132. )._process()
  133. return True
  134. def motion_notify_event(self, controller, x, y):
  135. MouseEvent(
  136. "motion_notify_event", self, *self._mpl_coords((x, y)),
  137. buttons=self._mpl_buttons(controller),
  138. modifiers=self._mpl_modifiers(controller),
  139. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  140. )._process()
  141. def enter_notify_event(self, controller, x, y):
  142. LocationEvent(
  143. "figure_enter_event", self, *self._mpl_coords((x, y)),
  144. modifiers=self._mpl_modifiers(),
  145. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  146. )._process()
  147. def leave_notify_event(self, controller):
  148. LocationEvent(
  149. "figure_leave_event", self, *self._mpl_coords(),
  150. modifiers=self._mpl_modifiers(),
  151. guiEvent=controller.get_current_event() if _GOBJECT_GE_3_47 else None,
  152. )._process()
  153. def resize_event(self, area, width, height):
  154. self._update_device_pixel_ratio()
  155. dpi = self.figure.dpi
  156. winch = width * self.device_pixel_ratio / dpi
  157. hinch = height * self.device_pixel_ratio / dpi
  158. self.figure.set_size_inches(winch, hinch, forward=False)
  159. ResizeEvent("resize_event", self)._process()
  160. self.draw_idle()
  161. def _mpl_buttons(self, controller):
  162. # NOTE: This spews "Broken accounting of active state" warnings on
  163. # right click on macOS.
  164. surface = self.get_native().get_surface()
  165. is_over, x, y, event_state = surface.get_device_position(
  166. self.get_display().get_default_seat().get_pointer())
  167. # NOTE: alternatively we could use
  168. # event_state = controller.get_current_event_state()
  169. # but for button_press/button_release this would report the state
  170. # *prior* to the event rather than after it; the above reports the
  171. # state *after* it.
  172. mod_table = [
  173. (MouseButton.LEFT, Gdk.ModifierType.BUTTON1_MASK),
  174. (MouseButton.MIDDLE, Gdk.ModifierType.BUTTON2_MASK),
  175. (MouseButton.RIGHT, Gdk.ModifierType.BUTTON3_MASK),
  176. (MouseButton.BACK, Gdk.ModifierType.BUTTON4_MASK),
  177. (MouseButton.FORWARD, Gdk.ModifierType.BUTTON5_MASK),
  178. ]
  179. return {name for name, mask in mod_table if event_state & mask}
  180. def _mpl_modifiers(self, controller=None):
  181. if controller is None:
  182. surface = self.get_native().get_surface()
  183. is_over, x, y, event_state = surface.get_device_position(
  184. self.get_display().get_default_seat().get_pointer())
  185. else:
  186. event_state = controller.get_current_event_state()
  187. mod_table = [
  188. ("ctrl", Gdk.ModifierType.CONTROL_MASK),
  189. ("alt", Gdk.ModifierType.ALT_MASK),
  190. ("shift", Gdk.ModifierType.SHIFT_MASK),
  191. ("super", Gdk.ModifierType.SUPER_MASK),
  192. ]
  193. return [name for name, mask in mod_table if event_state & mask]
  194. def _get_key(self, keyval, keycode, state):
  195. unikey = chr(Gdk.keyval_to_unicode(keyval))
  196. key = cbook._unikey_or_keysym_to_mplkey(
  197. unikey,
  198. Gdk.keyval_name(keyval))
  199. modifiers = [
  200. ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"),
  201. ("alt", Gdk.ModifierType.ALT_MASK, "alt"),
  202. ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"),
  203. ("super", Gdk.ModifierType.SUPER_MASK, "super"),
  204. ]
  205. mods = [
  206. mod for mod, mask, mod_key in modifiers
  207. if (mod_key != key and state & mask
  208. and not (mod == "shift" and unikey.isprintable()))]
  209. return "+".join([*mods, key])
  210. def _realize_event(self, obj):
  211. surface = self.get_native().get_surface()
  212. surface.connect('notify::scale', self._update_device_pixel_ratio)
  213. self._update_device_pixel_ratio()
  214. def _update_device_pixel_ratio(self, *args, **kwargs):
  215. # We need to be careful in cases with mixed resolution displays if
  216. # device_pixel_ratio changes.
  217. if _GTK_GE_4_12:
  218. scale = self.get_native().get_surface().get_scale()
  219. else:
  220. scale = self.get_scale_factor()
  221. assert scale is not None
  222. if self._set_device_pixel_ratio(scale):
  223. self.draw()
  224. def _draw_rubberband(self, rect):
  225. self._rubberband_rect = rect
  226. # TODO: Only update the rubberband area.
  227. self.queue_draw()
  228. def _draw_func(self, drawing_area, ctx, width, height):
  229. self.on_draw_event(self, ctx)
  230. self._post_draw(self, ctx)
  231. def _post_draw(self, widget, ctx):
  232. if self._rubberband_rect is None:
  233. return
  234. lw = 1
  235. dash = 3
  236. x0, y0, w, h = (dim / self.device_pixel_ratio
  237. for dim in self._rubberband_rect)
  238. x1 = x0 + w
  239. y1 = y0 + h
  240. # Draw the lines from x0, y0 towards x1, y1 so that the
  241. # dashes don't "jump" when moving the zoom box.
  242. ctx.move_to(x0, y0)
  243. ctx.line_to(x0, y1)
  244. ctx.move_to(x0, y0)
  245. ctx.line_to(x1, y0)
  246. ctx.move_to(x0, y1)
  247. ctx.line_to(x1, y1)
  248. ctx.move_to(x1, y0)
  249. ctx.line_to(x1, y1)
  250. ctx.set_antialias(1)
  251. ctx.set_line_width(lw)
  252. ctx.set_dash((dash, dash), 0)
  253. ctx.set_source_rgb(0, 0, 0)
  254. ctx.stroke_preserve()
  255. ctx.set_dash((dash, dash), dash)
  256. ctx.set_source_rgb(1, 1, 1)
  257. ctx.stroke()
  258. def on_draw_event(self, widget, ctx):
  259. # to be overwritten by GTK4Agg or GTK4Cairo
  260. pass
  261. def draw(self):
  262. # docstring inherited
  263. if self.is_drawable():
  264. self.queue_draw()
  265. def draw_idle(self):
  266. # docstring inherited
  267. if self._idle_draw_id != 0:
  268. return
  269. def idle_draw(*args):
  270. try:
  271. self.draw()
  272. finally:
  273. self._idle_draw_id = 0
  274. return False
  275. self._idle_draw_id = GLib.idle_add(idle_draw)
  276. def flush_events(self):
  277. # docstring inherited
  278. context = GLib.MainContext.default()
  279. while context.pending():
  280. context.iteration(True)
  281. class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box):
  282. def __init__(self, canvas):
  283. Gtk.Box.__init__(self)
  284. self.add_css_class('toolbar')
  285. self._gtk_ids = {}
  286. for text, tooltip_text, image_file, callback in self.toolitems:
  287. if text is None:
  288. self.append(Gtk.Separator())
  289. continue
  290. image = Gtk.Image.new_from_gicon(
  291. Gio.Icon.new_for_string(
  292. str(cbook._get_data_path('images',
  293. f'{image_file}-symbolic.svg'))))
  294. self._gtk_ids[text] = button = (
  295. Gtk.ToggleButton() if callback in ['zoom', 'pan'] else
  296. Gtk.Button())
  297. button.set_child(image)
  298. button.add_css_class('flat')
  299. button.add_css_class('image-button')
  300. # Save the handler id, so that we can block it as needed.
  301. button._signal_handler = button.connect(
  302. 'clicked', getattr(self, callback))
  303. button.set_tooltip_text(tooltip_text)
  304. self.append(button)
  305. # This filler item ensures the toolbar is always at least two text
  306. # lines high. Otherwise the canvas gets redrawn as the mouse hovers
  307. # over images because those use two-line messages which resize the
  308. # toolbar.
  309. label = Gtk.Label()
  310. label.set_markup(
  311. '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
  312. label.set_hexpand(True) # Push real message to the right.
  313. self.append(label)
  314. self.message = Gtk.Label()
  315. self.message.set_justify(Gtk.Justification.RIGHT)
  316. self.append(self.message)
  317. _NavigationToolbar2GTK.__init__(self, canvas)
  318. def save_figure(self, *args):
  319. dialog = Gtk.FileChooserNative(
  320. title='Save the figure',
  321. transient_for=self.canvas.get_root(),
  322. action=Gtk.FileChooserAction.SAVE,
  323. modal=True)
  324. self._save_dialog = dialog # Must keep a reference.
  325. ff = Gtk.FileFilter()
  326. ff.set_name('All files')
  327. ff.add_pattern('*')
  328. dialog.add_filter(ff)
  329. dialog.set_filter(ff)
  330. formats = []
  331. default_format = None
  332. for i, (name, fmts) in enumerate(
  333. self.canvas.get_supported_filetypes_grouped().items()):
  334. ff = Gtk.FileFilter()
  335. ff.set_name(name)
  336. for fmt in fmts:
  337. ff.add_pattern(f'*.{fmt}')
  338. dialog.add_filter(ff)
  339. formats.append(name)
  340. if self.canvas.get_default_filetype() in fmts:
  341. default_format = i
  342. # Setting the choice doesn't always work, so make sure the default
  343. # format is first.
  344. formats = [formats[default_format], *formats[:default_format],
  345. *formats[default_format+1:]]
  346. dialog.add_choice('format', 'File format', formats, formats)
  347. dialog.set_choice('format', formats[0])
  348. dialog.set_current_folder(Gio.File.new_for_path(
  349. os.path.expanduser(mpl.rcParams['savefig.directory'])))
  350. dialog.set_current_name(self.canvas.get_default_filename())
  351. @functools.partial(dialog.connect, 'response')
  352. def on_response(dialog, response):
  353. file = dialog.get_file()
  354. fmt = dialog.get_choice('format')
  355. fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0]
  356. dialog.destroy()
  357. self._save_dialog = None
  358. if response != Gtk.ResponseType.ACCEPT:
  359. return
  360. # Save dir for next time, unless empty str (which means use cwd).
  361. if mpl.rcParams['savefig.directory']:
  362. parent = file.get_parent()
  363. mpl.rcParams['savefig.directory'] = parent.get_path()
  364. try:
  365. self.canvas.figure.savefig(file.get_path(), format=fmt)
  366. except Exception as e:
  367. msg = Gtk.MessageDialog(
  368. transient_for=self.canvas.get_root(),
  369. message_type=Gtk.MessageType.ERROR,
  370. buttons=Gtk.ButtonsType.OK, modal=True,
  371. text=str(e))
  372. msg.show()
  373. dialog.show()
  374. return self.UNKNOWN_SAVED_STATUS
  375. class ToolbarGTK4(ToolContainerBase, Gtk.Box):
  376. _icon_extension = '-symbolic.svg'
  377. def __init__(self, toolmanager):
  378. ToolContainerBase.__init__(self, toolmanager)
  379. Gtk.Box.__init__(self)
  380. self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
  381. # Tool items are created later, but must appear before the message.
  382. self._tool_box = Gtk.Box()
  383. self.append(self._tool_box)
  384. self._groups = {}
  385. self._toolitems = {}
  386. # This filler item ensures the toolbar is always at least two text
  387. # lines high. Otherwise the canvas gets redrawn as the mouse hovers
  388. # over images because those use two-line messages which resize the
  389. # toolbar.
  390. label = Gtk.Label()
  391. label.set_markup(
  392. '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
  393. label.set_hexpand(True) # Push real message to the right.
  394. self.append(label)
  395. self._message = Gtk.Label()
  396. self._message.set_justify(Gtk.Justification.RIGHT)
  397. self.append(self._message)
  398. def add_toolitem(self, name, group, position, image_file, description,
  399. toggle):
  400. if toggle:
  401. button = Gtk.ToggleButton()
  402. else:
  403. button = Gtk.Button()
  404. button.set_label(name)
  405. button.add_css_class('flat')
  406. if image_file is not None:
  407. image = Gtk.Image.new_from_gicon(
  408. Gio.Icon.new_for_string(image_file))
  409. button.set_child(image)
  410. button.add_css_class('image-button')
  411. if position is None:
  412. position = -1
  413. self._add_button(button, group, position)
  414. signal = button.connect('clicked', self._call_tool, name)
  415. button.set_tooltip_text(description)
  416. self._toolitems.setdefault(name, [])
  417. self._toolitems[name].append((button, signal))
  418. def _find_child_at_position(self, group, position):
  419. children = [None]
  420. child = self._groups[group].get_first_child()
  421. while child is not None:
  422. children.append(child)
  423. child = child.get_next_sibling()
  424. return children[position]
  425. def _add_button(self, button, group, position):
  426. if group not in self._groups:
  427. if self._groups:
  428. self._add_separator()
  429. group_box = Gtk.Box()
  430. self._tool_box.append(group_box)
  431. self._groups[group] = group_box
  432. self._groups[group].insert_child_after(
  433. button, self._find_child_at_position(group, position))
  434. def _call_tool(self, btn, name):
  435. self.trigger_tool(name)
  436. def toggle_toolitem(self, name, toggled):
  437. if name not in self._toolitems:
  438. return
  439. for toolitem, signal in self._toolitems[name]:
  440. toolitem.handler_block(signal)
  441. toolitem.set_active(toggled)
  442. toolitem.handler_unblock(signal)
  443. def remove_toolitem(self, name):
  444. for toolitem, _signal in self._toolitems.pop(name, []):
  445. for group in self._groups:
  446. if toolitem in self._groups[group]:
  447. self._groups[group].remove(toolitem)
  448. def _add_separator(self):
  449. sep = Gtk.Separator()
  450. sep.set_property("orientation", Gtk.Orientation.VERTICAL)
  451. self._tool_box.append(sep)
  452. def set_message(self, s):
  453. self._message.set_label(s)
  454. @backend_tools._register_tool_class(FigureCanvasGTK4)
  455. class SaveFigureGTK4(backend_tools.SaveFigureBase):
  456. def trigger(self, *args, **kwargs):
  457. NavigationToolbar2GTK4.save_figure(
  458. self._make_classic_style_pseudo_toolbar())
  459. @backend_tools._register_tool_class(FigureCanvasGTK4)
  460. class HelpGTK4(backend_tools.ToolHelpBase):
  461. def _normalize_shortcut(self, key):
  462. """
  463. Convert Matplotlib key presses to GTK+ accelerator identifiers.
  464. Related to `FigureCanvasGTK4._get_key`.
  465. """
  466. special = {
  467. 'backspace': 'BackSpace',
  468. 'pagedown': 'Page_Down',
  469. 'pageup': 'Page_Up',
  470. 'scroll_lock': 'Scroll_Lock',
  471. }
  472. parts = key.split('+')
  473. mods = ['<' + mod + '>' for mod in parts[:-1]]
  474. key = parts[-1]
  475. if key in special:
  476. key = special[key]
  477. elif len(key) > 1:
  478. key = key.capitalize()
  479. elif key.isupper():
  480. mods += ['<shift>']
  481. return ''.join(mods) + key
  482. def _is_valid_shortcut(self, key):
  483. """
  484. Check for a valid shortcut to be displayed.
  485. - GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`).
  486. - The shortcut window only shows keyboard shortcuts, not mouse buttons.
  487. """
  488. return 'cmd+' not in key and not key.startswith('MouseButton.')
  489. def trigger(self, *args):
  490. section = Gtk.ShortcutsSection()
  491. for name, tool in sorted(self.toolmanager.tools.items()):
  492. if not tool.description:
  493. continue
  494. # Putting everything in a separate group allows GTK to
  495. # automatically split them into separate columns/pages, which is
  496. # useful because we have lots of shortcuts, some with many keys
  497. # that are very wide.
  498. group = Gtk.ShortcutsGroup()
  499. section.append(group)
  500. # A hack to remove the title since we have no group naming.
  501. child = group.get_first_child()
  502. while child is not None:
  503. child.set_visible(False)
  504. child = child.get_next_sibling()
  505. shortcut = Gtk.ShortcutsShortcut(
  506. accelerator=' '.join(
  507. self._normalize_shortcut(key)
  508. for key in self.toolmanager.get_tool_keymap(name)
  509. if self._is_valid_shortcut(key)),
  510. title=tool.name,
  511. subtitle=tool.description)
  512. group.append(shortcut)
  513. window = Gtk.ShortcutsWindow(
  514. title='Help',
  515. modal=True,
  516. transient_for=self._figure.canvas.get_root())
  517. window.set_child(section)
  518. window.show()
  519. @backend_tools._register_tool_class(FigureCanvasGTK4)
  520. class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase):
  521. def trigger(self, *args, **kwargs):
  522. with io.BytesIO() as f:
  523. self.canvas.print_rgba(f)
  524. w, h = self.canvas.get_width_height()
  525. pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(),
  526. GdkPixbuf.Colorspace.RGB, True,
  527. 8, w, h, w*4)
  528. clipboard = self.canvas.get_clipboard()
  529. clipboard.set(pb)
  530. backend_tools._register_tool_class(
  531. FigureCanvasGTK4, _backend_gtk.ConfigureSubplotsGTK)
  532. backend_tools._register_tool_class(
  533. FigureCanvasGTK4, _backend_gtk.RubberbandGTK)
  534. Toolbar = ToolbarGTK4
  535. class FigureManagerGTK4(_FigureManagerGTK):
  536. _toolbar2_class = NavigationToolbar2GTK4
  537. _toolmanager_toolbar_class = ToolbarGTK4
  538. @_BackendGTK.export
  539. class _BackendGTK4(_BackendGTK):
  540. FigureCanvas = FigureCanvasGTK4
  541. FigureManager = FigureManagerGTK4