backend_gtk3.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. import functools
  2. import logging
  3. import os
  4. from pathlib import Path
  5. import sys
  6. import matplotlib as mpl
  7. from matplotlib import backend_tools, cbook
  8. from matplotlib._pylab_helpers import Gcf
  9. from matplotlib.backend_bases import (
  10. _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
  11. StatusbarBase, TimerBase, ToolContainerBase, cursors)
  12. from matplotlib.figure import Figure
  13. from matplotlib.widgets import SubplotTool
  14. try:
  15. import gi
  16. except ImportError as err:
  17. raise ImportError("The GTK3 backends require PyGObject") from err
  18. try:
  19. # :raises ValueError: If module/version is already loaded, already
  20. # required, or unavailable.
  21. gi.require_version("Gtk", "3.0")
  22. except ValueError as e:
  23. # in this case we want to re-raise as ImportError so the
  24. # auto-backend selection logic correctly skips.
  25. raise ImportError from e
  26. from gi.repository import Gio, GLib, GObject, Gtk, Gdk
  27. _log = logging.getLogger(__name__)
  28. backend_version = "%s.%s.%s" % (
  29. Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version())
  30. try:
  31. cursord = {
  32. cursors.MOVE: Gdk.Cursor.new(Gdk.CursorType.FLEUR),
  33. cursors.HAND: Gdk.Cursor.new(Gdk.CursorType.HAND2),
  34. cursors.POINTER: Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR),
  35. cursors.SELECT_REGION: Gdk.Cursor.new(Gdk.CursorType.TCROSS),
  36. cursors.WAIT: Gdk.Cursor.new(Gdk.CursorType.WATCH),
  37. }
  38. except TypeError as exc:
  39. # Happens when running headless. Convert to ImportError to cooperate with
  40. # backend switching.
  41. raise ImportError(exc) from exc
  42. class TimerGTK3(TimerBase):
  43. """Subclass of `.TimerBase` using GTK3 timer events."""
  44. def __init__(self, *args, **kwargs):
  45. self._timer = None
  46. TimerBase.__init__(self, *args, **kwargs)
  47. def _timer_start(self):
  48. # Need to stop it, otherwise we potentially leak a timer id that will
  49. # never be stopped.
  50. self._timer_stop()
  51. self._timer = GLib.timeout_add(self._interval, self._on_timer)
  52. def _timer_stop(self):
  53. if self._timer is not None:
  54. GLib.source_remove(self._timer)
  55. self._timer = None
  56. def _timer_set_interval(self):
  57. # Only stop and restart it if the timer has already been started
  58. if self._timer is not None:
  59. self._timer_stop()
  60. self._timer_start()
  61. def _on_timer(self):
  62. TimerBase._on_timer(self)
  63. # Gtk timeout_add() requires that the callback returns True if it
  64. # is to be called again.
  65. if self.callbacks and not self._single:
  66. return True
  67. else:
  68. self._timer = None
  69. return False
  70. class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
  71. required_interactive_framework = "gtk3"
  72. _timer_cls = TimerGTK3
  73. keyvald = {65507: 'control',
  74. 65505: 'shift',
  75. 65513: 'alt',
  76. 65508: 'control',
  77. 65506: 'shift',
  78. 65514: 'alt',
  79. 65361: 'left',
  80. 65362: 'up',
  81. 65363: 'right',
  82. 65364: 'down',
  83. 65307: 'escape',
  84. 65470: 'f1',
  85. 65471: 'f2',
  86. 65472: 'f3',
  87. 65473: 'f4',
  88. 65474: 'f5',
  89. 65475: 'f6',
  90. 65476: 'f7',
  91. 65477: 'f8',
  92. 65478: 'f9',
  93. 65479: 'f10',
  94. 65480: 'f11',
  95. 65481: 'f12',
  96. 65300: 'scroll_lock',
  97. 65299: 'break',
  98. 65288: 'backspace',
  99. 65293: 'enter',
  100. 65379: 'insert',
  101. 65535: 'delete',
  102. 65360: 'home',
  103. 65367: 'end',
  104. 65365: 'pageup',
  105. 65366: 'pagedown',
  106. 65438: '0',
  107. 65436: '1',
  108. 65433: '2',
  109. 65435: '3',
  110. 65430: '4',
  111. 65437: '5',
  112. 65432: '6',
  113. 65429: '7',
  114. 65431: '8',
  115. 65434: '9',
  116. 65451: '+',
  117. 65453: '-',
  118. 65450: '*',
  119. 65455: '/',
  120. 65439: 'dec',
  121. 65421: 'enter',
  122. }
  123. # Setting this as a static constant prevents
  124. # this resulting expression from leaking
  125. event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
  126. | Gdk.EventMask.BUTTON_RELEASE_MASK
  127. | Gdk.EventMask.EXPOSURE_MASK
  128. | Gdk.EventMask.KEY_PRESS_MASK
  129. | Gdk.EventMask.KEY_RELEASE_MASK
  130. | Gdk.EventMask.ENTER_NOTIFY_MASK
  131. | Gdk.EventMask.LEAVE_NOTIFY_MASK
  132. | Gdk.EventMask.POINTER_MOTION_MASK
  133. | Gdk.EventMask.POINTER_MOTION_HINT_MASK
  134. | Gdk.EventMask.SCROLL_MASK)
  135. def __init__(self, figure):
  136. FigureCanvasBase.__init__(self, figure)
  137. GObject.GObject.__init__(self)
  138. self._idle_draw_id = 0
  139. self._lastCursor = None
  140. self._rubberband_rect = None
  141. self.connect('scroll_event', self.scroll_event)
  142. self.connect('button_press_event', self.button_press_event)
  143. self.connect('button_release_event', self.button_release_event)
  144. self.connect('configure_event', self.configure_event)
  145. self.connect('draw', self.on_draw_event)
  146. self.connect('draw', self._post_draw)
  147. self.connect('key_press_event', self.key_press_event)
  148. self.connect('key_release_event', self.key_release_event)
  149. self.connect('motion_notify_event', self.motion_notify_event)
  150. self.connect('leave_notify_event', self.leave_notify_event)
  151. self.connect('enter_notify_event', self.enter_notify_event)
  152. self.connect('size_allocate', self.size_allocate)
  153. self.set_events(self.__class__.event_mask)
  154. self.set_double_buffered(True)
  155. self.set_can_focus(True)
  156. renderer_init = cbook._deprecate_method_override(
  157. __class__._renderer_init, self, allow_empty=True, since="3.3",
  158. addendum="Please initialize the renderer, if needed, in the "
  159. "subclass' __init__; a fully empty _renderer_init implementation "
  160. "may be kept for compatibility with earlier versions of "
  161. "Matplotlib.")
  162. if renderer_init:
  163. renderer_init()
  164. @cbook.deprecated("3.3", alternative="__init__")
  165. def _renderer_init(self):
  166. pass
  167. def destroy(self):
  168. #Gtk.DrawingArea.destroy(self)
  169. self.close_event()
  170. def scroll_event(self, widget, event):
  171. x = event.x
  172. # flipy so y=0 is bottom of canvas
  173. y = self.get_allocation().height - event.y
  174. step = 1 if event.direction == Gdk.ScrollDirection.UP else -1
  175. FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
  176. return False # finish event propagation?
  177. def button_press_event(self, widget, event):
  178. x = event.x
  179. # flipy so y=0 is bottom of canvas
  180. y = self.get_allocation().height - event.y
  181. FigureCanvasBase.button_press_event(
  182. self, x, y, event.button, guiEvent=event)
  183. return False # finish event propagation?
  184. def button_release_event(self, widget, event):
  185. x = event.x
  186. # flipy so y=0 is bottom of canvas
  187. y = self.get_allocation().height - event.y
  188. FigureCanvasBase.button_release_event(
  189. self, x, y, event.button, guiEvent=event)
  190. return False # finish event propagation?
  191. def key_press_event(self, widget, event):
  192. key = self._get_key(event)
  193. FigureCanvasBase.key_press_event(self, key, guiEvent=event)
  194. return True # stop event propagation
  195. def key_release_event(self, widget, event):
  196. key = self._get_key(event)
  197. FigureCanvasBase.key_release_event(self, key, guiEvent=event)
  198. return True # stop event propagation
  199. def motion_notify_event(self, widget, event):
  200. if event.is_hint:
  201. t, x, y, state = event.window.get_pointer()
  202. else:
  203. x, y = event.x, event.y
  204. # flipy so y=0 is bottom of canvas
  205. y = self.get_allocation().height - y
  206. FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
  207. return False # finish event propagation?
  208. def leave_notify_event(self, widget, event):
  209. FigureCanvasBase.leave_notify_event(self, event)
  210. def enter_notify_event(self, widget, event):
  211. x = event.x
  212. # flipy so y=0 is bottom of canvas
  213. y = self.get_allocation().height - event.y
  214. FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
  215. def size_allocate(self, widget, allocation):
  216. dpival = self.figure.dpi
  217. winch = allocation.width / dpival
  218. hinch = allocation.height / dpival
  219. self.figure.set_size_inches(winch, hinch, forward=False)
  220. FigureCanvasBase.resize_event(self)
  221. self.draw_idle()
  222. def _get_key(self, event):
  223. if event.keyval in self.keyvald:
  224. key = self.keyvald[event.keyval]
  225. elif event.keyval < 256:
  226. key = chr(event.keyval)
  227. else:
  228. key = None
  229. modifiers = [
  230. (Gdk.ModifierType.MOD4_MASK, 'super'),
  231. (Gdk.ModifierType.MOD1_MASK, 'alt'),
  232. (Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
  233. ]
  234. for key_mask, prefix in modifiers:
  235. if event.state & key_mask:
  236. key = '{0}+{1}'.format(prefix, key)
  237. return key
  238. def configure_event(self, widget, event):
  239. if widget.get_property("window") is None:
  240. return
  241. w, h = event.width, event.height
  242. if w < 3 or h < 3:
  243. return # empty fig
  244. # resize the figure (in inches)
  245. dpi = self.figure.dpi
  246. self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
  247. return False # finish event propagation?
  248. def _draw_rubberband(self, rect):
  249. self._rubberband_rect = rect
  250. # TODO: Only update the rubberband area.
  251. self.queue_draw()
  252. def _post_draw(self, widget, ctx):
  253. if self._rubberband_rect is None:
  254. return
  255. x0, y0, w, h = self._rubberband_rect
  256. x1 = x0 + w
  257. y1 = y0 + h
  258. # Draw the lines from x0, y0 towards x1, y1 so that the
  259. # dashes don't "jump" when moving the zoom box.
  260. ctx.move_to(x0, y0)
  261. ctx.line_to(x0, y1)
  262. ctx.move_to(x0, y0)
  263. ctx.line_to(x1, y0)
  264. ctx.move_to(x0, y1)
  265. ctx.line_to(x1, y1)
  266. ctx.move_to(x1, y0)
  267. ctx.line_to(x1, y1)
  268. ctx.set_antialias(1)
  269. ctx.set_line_width(1)
  270. ctx.set_dash((3, 3), 0)
  271. ctx.set_source_rgb(0, 0, 0)
  272. ctx.stroke_preserve()
  273. ctx.set_dash((3, 3), 3)
  274. ctx.set_source_rgb(1, 1, 1)
  275. ctx.stroke()
  276. def on_draw_event(self, widget, ctx):
  277. # to be overwritten by GTK3Agg or GTK3Cairo
  278. pass
  279. def draw(self):
  280. # docstring inherited
  281. if self.is_drawable():
  282. self.queue_draw()
  283. def draw_idle(self):
  284. # docstring inherited
  285. if self._idle_draw_id != 0:
  286. return
  287. def idle_draw(*args):
  288. try:
  289. self.draw()
  290. finally:
  291. self._idle_draw_id = 0
  292. return False
  293. self._idle_draw_id = GLib.idle_add(idle_draw)
  294. def flush_events(self):
  295. # docstring inherited
  296. Gdk.threads_enter()
  297. while Gtk.events_pending():
  298. Gtk.main_iteration()
  299. Gdk.flush()
  300. Gdk.threads_leave()
  301. class FigureManagerGTK3(FigureManagerBase):
  302. """
  303. Attributes
  304. ----------
  305. canvas : `FigureCanvas`
  306. The FigureCanvas instance
  307. num : int or str
  308. The Figure number
  309. toolbar : Gtk.Toolbar
  310. The Gtk.Toolbar
  311. vbox : Gtk.VBox
  312. The Gtk.VBox containing the canvas and toolbar
  313. window : Gtk.Window
  314. The Gtk.Window
  315. """
  316. def __init__(self, canvas, num):
  317. FigureManagerBase.__init__(self, canvas, num)
  318. self.window = Gtk.Window()
  319. self.window.set_wmclass("matplotlib", "Matplotlib")
  320. self.set_window_title("Figure %d" % num)
  321. try:
  322. self.window.set_icon_from_file(window_icon)
  323. except Exception:
  324. # Some versions of gtk throw a glib.GError but not all, so I am not
  325. # sure how to catch it. I am unhappy doing a blanket catch here,
  326. # but am not sure what a better way is - JDH
  327. _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1])
  328. self.vbox = Gtk.Box()
  329. self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  330. self.window.add(self.vbox)
  331. self.vbox.show()
  332. self.canvas.show()
  333. self.vbox.pack_start(self.canvas, True, True, 0)
  334. # calculate size for window
  335. w = int(self.canvas.figure.bbox.width)
  336. h = int(self.canvas.figure.bbox.height)
  337. self.toolbar = self._get_toolbar()
  338. def add_widget(child):
  339. child.show()
  340. self.vbox.pack_end(child, False, False, 0)
  341. size_request = child.size_request()
  342. return size_request.height
  343. if self.toolmanager:
  344. backend_tools.add_tools_to_manager(self.toolmanager)
  345. if self.toolbar:
  346. backend_tools.add_tools_to_container(self.toolbar)
  347. if self.toolbar is not None:
  348. self.toolbar.show()
  349. h += add_widget(self.toolbar)
  350. self.window.set_default_size(w, h)
  351. self._destroying = False
  352. self.window.connect("destroy", lambda *args: Gcf.destroy(self))
  353. self.window.connect("delete_event", lambda *args: Gcf.destroy(self))
  354. if mpl.is_interactive():
  355. self.window.show()
  356. self.canvas.draw_idle()
  357. self.canvas.grab_focus()
  358. def destroy(self, *args):
  359. if self._destroying:
  360. # Otherwise, this can be called twice when the user presses 'q',
  361. # which calls Gcf.destroy(self), then this destroy(), then triggers
  362. # Gcf.destroy(self) once again via
  363. # `connect("destroy", lambda *args: Gcf.destroy(self))`.
  364. return
  365. self._destroying = True
  366. self.vbox.destroy()
  367. self.window.destroy()
  368. self.canvas.destroy()
  369. if self.toolbar:
  370. self.toolbar.destroy()
  371. if (Gcf.get_num_fig_managers() == 0 and not mpl.is_interactive() and
  372. Gtk.main_level() >= 1):
  373. Gtk.main_quit()
  374. def show(self):
  375. # show the figure window
  376. self.window.show()
  377. self.canvas.draw()
  378. if mpl.rcParams['figure.raise_window']:
  379. self.window.present()
  380. def full_screen_toggle(self):
  381. self._full_screen_flag = not self._full_screen_flag
  382. if self._full_screen_flag:
  383. self.window.fullscreen()
  384. else:
  385. self.window.unfullscreen()
  386. _full_screen_flag = False
  387. def _get_toolbar(self):
  388. # must be inited after the window, drawingArea and figure
  389. # attrs are set
  390. if mpl.rcParams['toolbar'] == 'toolbar2':
  391. toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
  392. elif mpl.rcParams['toolbar'] == 'toolmanager':
  393. toolbar = ToolbarGTK3(self.toolmanager)
  394. else:
  395. toolbar = None
  396. return toolbar
  397. def get_window_title(self):
  398. return self.window.get_title()
  399. def set_window_title(self, title):
  400. self.window.set_title(title)
  401. def resize(self, width, height):
  402. """Set the canvas size in pixels."""
  403. if self.toolbar:
  404. toolbar_size = self.toolbar.size_request()
  405. height += toolbar_size.height
  406. canvas_size = self.canvas.get_allocation()
  407. if canvas_size.width == canvas_size.height == 1:
  408. # A canvas size of (1, 1) cannot exist in most cases, because
  409. # window decorations would prevent such a small window. This call
  410. # must be before the window has been mapped and widgets have been
  411. # sized, so just change the window's starting size.
  412. self.window.set_default_size(width, height)
  413. else:
  414. self.window.resize(width, height)
  415. class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
  416. def __init__(self, canvas, window):
  417. self.win = window
  418. GObject.GObject.__init__(self)
  419. self.set_style(Gtk.ToolbarStyle.ICONS)
  420. self._gtk_ids = {}
  421. for text, tooltip_text, image_file, callback in self.toolitems:
  422. if text is None:
  423. self.insert(Gtk.SeparatorToolItem(), -1)
  424. continue
  425. image = Gtk.Image.new_from_gicon(
  426. Gio.Icon.new_for_string(
  427. str(cbook._get_data_path('images',
  428. f'{image_file}-symbolic.svg'))),
  429. Gtk.IconSize.LARGE_TOOLBAR)
  430. self._gtk_ids[text] = tbutton = (
  431. Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
  432. Gtk.ToolButton())
  433. tbutton.set_label(text)
  434. tbutton.set_icon_widget(image)
  435. self.insert(tbutton, -1)
  436. # Save the handler id, so that we can block it as needed.
  437. tbutton._signal_handler = tbutton.connect(
  438. 'clicked', getattr(self, callback))
  439. tbutton.set_tooltip_text(tooltip_text)
  440. toolitem = Gtk.SeparatorToolItem()
  441. self.insert(toolitem, -1)
  442. toolitem.set_draw(False)
  443. toolitem.set_expand(True)
  444. # This filler item ensures the toolbar is always at least two text
  445. # lines high. Otherwise the canvas gets redrawn as the mouse hovers
  446. # over images because those use two-line messages which resize the
  447. # toolbar.
  448. toolitem = Gtk.ToolItem()
  449. self.insert(toolitem, -1)
  450. label = Gtk.Label()
  451. label.set_markup(
  452. '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
  453. toolitem.add(label)
  454. toolitem = Gtk.ToolItem()
  455. self.insert(toolitem, -1)
  456. self.message = Gtk.Label()
  457. toolitem.add(self.message)
  458. self.show_all()
  459. NavigationToolbar2.__init__(self, canvas)
  460. @cbook.deprecated("3.3")
  461. @property
  462. def ctx(self):
  463. return self.canvas.get_property("window").cairo_create()
  464. def set_message(self, s):
  465. escaped = GLib.markup_escape_text(s)
  466. self.message.set_markup(f'<small>{escaped}</small>')
  467. def set_cursor(self, cursor):
  468. window = self.canvas.get_property("window")
  469. if window is not None:
  470. window.set_cursor(cursord[cursor])
  471. Gtk.main_iteration()
  472. def draw_rubberband(self, event, x0, y0, x1, y1):
  473. height = self.canvas.figure.bbox.height
  474. y1 = height - y1
  475. y0 = height - y0
  476. rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
  477. self.canvas._draw_rubberband(rect)
  478. def remove_rubberband(self):
  479. self.canvas._draw_rubberband(None)
  480. def _update_buttons_checked(self):
  481. for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
  482. button = self._gtk_ids.get(name)
  483. if button:
  484. with button.handler_block(button._signal_handler):
  485. button.set_active(self.mode.name == active)
  486. def pan(self, *args):
  487. super().pan(*args)
  488. self._update_buttons_checked()
  489. def zoom(self, *args):
  490. super().zoom(*args)
  491. self._update_buttons_checked()
  492. def save_figure(self, *args):
  493. dialog = Gtk.FileChooserDialog(
  494. title="Save the figure",
  495. parent=self.canvas.get_toplevel(),
  496. action=Gtk.FileChooserAction.SAVE,
  497. buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  498. Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
  499. )
  500. for name, fmts \
  501. in self.canvas.get_supported_filetypes_grouped().items():
  502. ff = Gtk.FileFilter()
  503. ff.set_name(name)
  504. for fmt in fmts:
  505. ff.add_pattern("*." + fmt)
  506. dialog.add_filter(ff)
  507. if self.canvas.get_default_filetype() in fmts:
  508. dialog.set_filter(ff)
  509. @functools.partial(dialog.connect, "notify::filter")
  510. def on_notify_filter(*args):
  511. name = dialog.get_filter().get_name()
  512. fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
  513. dialog.set_current_name(
  514. str(Path(dialog.get_current_name()).with_suffix("." + fmt)))
  515. dialog.set_current_folder(mpl.rcParams["savefig.directory"])
  516. dialog.set_current_name(self.canvas.get_default_filename())
  517. dialog.set_do_overwrite_confirmation(True)
  518. response = dialog.run()
  519. fname = dialog.get_filename()
  520. ff = dialog.get_filter() # Doesn't autoadjust to filename :/
  521. fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
  522. dialog.destroy()
  523. if response != Gtk.ResponseType.OK:
  524. return
  525. # Save dir for next time, unless empty str (which means use cwd).
  526. if mpl.rcParams['savefig.directory']:
  527. mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
  528. try:
  529. self.canvas.figure.savefig(fname, format=fmt)
  530. except Exception as e:
  531. error_msg_gtk(str(e), parent=self)
  532. def configure_subplots(self, button):
  533. toolfig = Figure(figsize=(6, 3))
  534. canvas = type(self.canvas)(toolfig)
  535. toolfig.subplots_adjust(top=0.9)
  536. # Need to keep a reference to the tool.
  537. _tool = SubplotTool(self.canvas.figure, toolfig)
  538. w = int(toolfig.bbox.width)
  539. h = int(toolfig.bbox.height)
  540. window = Gtk.Window()
  541. try:
  542. window.set_icon_from_file(window_icon)
  543. except Exception:
  544. # we presumably already logged a message on the
  545. # failure of the main plot, don't keep reporting
  546. pass
  547. window.set_title("Subplot Configuration Tool")
  548. window.set_default_size(w, h)
  549. vbox = Gtk.Box()
  550. vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  551. window.add(vbox)
  552. vbox.show()
  553. canvas.show()
  554. vbox.pack_start(canvas, True, True, 0)
  555. window.show()
  556. def set_history_buttons(self):
  557. can_backward = self._nav_stack._pos > 0
  558. can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
  559. if 'Back' in self._gtk_ids:
  560. self._gtk_ids['Back'].set_sensitive(can_backward)
  561. if 'Forward' in self._gtk_ids:
  562. self._gtk_ids['Forward'].set_sensitive(can_forward)
  563. class ToolbarGTK3(ToolContainerBase, Gtk.Box):
  564. _icon_extension = '-symbolic.svg'
  565. def __init__(self, toolmanager):
  566. ToolContainerBase.__init__(self, toolmanager)
  567. Gtk.Box.__init__(self)
  568. self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
  569. self._message = Gtk.Label()
  570. self.pack_end(self._message, False, False, 0)
  571. self.show_all()
  572. self._groups = {}
  573. self._toolitems = {}
  574. def add_toolitem(self, name, group, position, image_file, description,
  575. toggle):
  576. if toggle:
  577. tbutton = Gtk.ToggleToolButton()
  578. else:
  579. tbutton = Gtk.ToolButton()
  580. tbutton.set_label(name)
  581. if image_file is not None:
  582. image = Gtk.Image.new_from_gicon(
  583. Gio.Icon.new_for_string(image_file),
  584. Gtk.IconSize.LARGE_TOOLBAR)
  585. tbutton.set_icon_widget(image)
  586. if position is None:
  587. position = -1
  588. self._add_button(tbutton, group, position)
  589. signal = tbutton.connect('clicked', self._call_tool, name)
  590. tbutton.set_tooltip_text(description)
  591. tbutton.show_all()
  592. self._toolitems.setdefault(name, [])
  593. self._toolitems[name].append((tbutton, signal))
  594. def _add_button(self, button, group, position):
  595. if group not in self._groups:
  596. if self._groups:
  597. self._add_separator()
  598. toolbar = Gtk.Toolbar()
  599. toolbar.set_style(Gtk.ToolbarStyle.ICONS)
  600. self.pack_start(toolbar, False, False, 0)
  601. toolbar.show_all()
  602. self._groups[group] = toolbar
  603. self._groups[group].insert(button, position)
  604. def _call_tool(self, btn, name):
  605. self.trigger_tool(name)
  606. def toggle_toolitem(self, name, toggled):
  607. if name not in self._toolitems:
  608. return
  609. for toolitem, signal in self._toolitems[name]:
  610. toolitem.handler_block(signal)
  611. toolitem.set_active(toggled)
  612. toolitem.handler_unblock(signal)
  613. def remove_toolitem(self, name):
  614. if name not in self._toolitems:
  615. self.toolmanager.message_event('%s Not in toolbar' % name, self)
  616. return
  617. for group in self._groups:
  618. for toolitem, _signal in self._toolitems[name]:
  619. if toolitem in self._groups[group]:
  620. self._groups[group].remove(toolitem)
  621. del self._toolitems[name]
  622. def _add_separator(self):
  623. sep = Gtk.Separator()
  624. sep.set_property("orientation", Gtk.Orientation.VERTICAL)
  625. self.pack_start(sep, False, True, 0)
  626. sep.show_all()
  627. def set_message(self, s):
  628. self._message.set_label(s)
  629. @cbook.deprecated("3.3")
  630. class StatusbarGTK3(StatusbarBase, Gtk.Statusbar):
  631. def __init__(self, *args, **kwargs):
  632. StatusbarBase.__init__(self, *args, **kwargs)
  633. Gtk.Statusbar.__init__(self)
  634. self._context = self.get_context_id('message')
  635. def set_message(self, s):
  636. self.pop(self._context)
  637. self.push(self._context, s)
  638. class RubberbandGTK3(backend_tools.RubberbandBase):
  639. def draw_rubberband(self, x0, y0, x1, y1):
  640. NavigationToolbar2GTK3.draw_rubberband(
  641. self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
  642. def remove_rubberband(self):
  643. NavigationToolbar2GTK3.remove_rubberband(
  644. self._make_classic_style_pseudo_toolbar())
  645. class SaveFigureGTK3(backend_tools.SaveFigureBase):
  646. def trigger(self, *args, **kwargs):
  647. class PseudoToolbar:
  648. canvas = self.figure.canvas
  649. return NavigationToolbar2GTK3.save_figure(PseudoToolbar())
  650. class SetCursorGTK3(backend_tools.SetCursorBase):
  651. def set_cursor(self, cursor):
  652. NavigationToolbar2GTK3.set_cursor(
  653. self._make_classic_style_pseudo_toolbar(), cursor)
  654. class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
  655. @cbook.deprecated("3.2")
  656. @property
  657. def window(self):
  658. if not hasattr(self, "_window"):
  659. self._window = None
  660. return self._window
  661. @window.setter
  662. @cbook.deprecated("3.2")
  663. def window(self, window):
  664. self._window = window
  665. @cbook.deprecated("3.2")
  666. def init_window(self):
  667. if self.window:
  668. return
  669. self.window = Gtk.Window(title="Subplot Configuration Tool")
  670. try:
  671. self.window.window.set_icon_from_file(window_icon)
  672. except Exception:
  673. # we presumably already logged a message on the
  674. # failure of the main plot, don't keep reporting
  675. pass
  676. self.vbox = Gtk.Box()
  677. self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  678. self.window.add(self.vbox)
  679. self.vbox.show()
  680. self.window.connect('destroy', self.destroy)
  681. toolfig = Figure(figsize=(6, 3))
  682. canvas = self.figure.canvas.__class__(toolfig)
  683. toolfig.subplots_adjust(top=0.9)
  684. SubplotTool(self.figure, toolfig)
  685. w = int(toolfig.bbox.width)
  686. h = int(toolfig.bbox.height)
  687. self.window.set_default_size(w, h)
  688. canvas.show()
  689. self.vbox.pack_start(canvas, True, True, 0)
  690. self.window.show()
  691. @cbook.deprecated("3.2")
  692. def destroy(self, *args):
  693. self.window.destroy()
  694. self.window = None
  695. def _get_canvas(self, fig):
  696. return self.canvas.__class__(fig)
  697. def trigger(self, *args):
  698. NavigationToolbar2GTK3.configure_subplots(
  699. self._make_classic_style_pseudo_toolbar(), None)
  700. class HelpGTK3(backend_tools.ToolHelpBase):
  701. def _normalize_shortcut(self, key):
  702. """
  703. Convert Matplotlib key presses to GTK+ accelerator identifiers.
  704. Related to `FigureCanvasGTK3._get_key`.
  705. """
  706. special = {
  707. 'backspace': 'BackSpace',
  708. 'pagedown': 'Page_Down',
  709. 'pageup': 'Page_Up',
  710. 'scroll_lock': 'Scroll_Lock',
  711. }
  712. parts = key.split('+')
  713. mods = ['<' + mod + '>' for mod in parts[:-1]]
  714. key = parts[-1]
  715. if key in special:
  716. key = special[key]
  717. elif len(key) > 1:
  718. key = key.capitalize()
  719. elif key.isupper():
  720. mods += ['<shift>']
  721. return ''.join(mods) + key
  722. def _is_valid_shortcut(self, key):
  723. """
  724. Check for a valid shortcut to be displayed.
  725. - GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
  726. - The shortcut window only shows keyboard shortcuts, not mouse buttons.
  727. """
  728. return 'cmd+' not in key and not key.startswith('MouseButton.')
  729. def _show_shortcuts_window(self):
  730. section = Gtk.ShortcutsSection()
  731. for name, tool in sorted(self.toolmanager.tools.items()):
  732. if not tool.description:
  733. continue
  734. # Putting everything in a separate group allows GTK to
  735. # automatically split them into separate columns/pages, which is
  736. # useful because we have lots of shortcuts, some with many keys
  737. # that are very wide.
  738. group = Gtk.ShortcutsGroup()
  739. section.add(group)
  740. # A hack to remove the title since we have no group naming.
  741. group.forall(lambda widget, data: widget.set_visible(False), None)
  742. shortcut = Gtk.ShortcutsShortcut(
  743. accelerator=' '.join(
  744. self._normalize_shortcut(key)
  745. for key in self.toolmanager.get_tool_keymap(name)
  746. if self._is_valid_shortcut(key)),
  747. title=tool.name,
  748. subtitle=tool.description)
  749. group.add(shortcut)
  750. window = Gtk.ShortcutsWindow(
  751. title='Help',
  752. modal=True,
  753. transient_for=self._figure.canvas.get_toplevel())
  754. section.show() # Must be done explicitly before add!
  755. window.add(section)
  756. window.show_all()
  757. def _show_shortcuts_dialog(self):
  758. dialog = Gtk.MessageDialog(
  759. self._figure.canvas.get_toplevel(),
  760. 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
  761. title="Help")
  762. dialog.run()
  763. dialog.destroy()
  764. def trigger(self, *args):
  765. if Gtk.check_version(3, 20, 0) is None:
  766. self._show_shortcuts_window()
  767. else:
  768. self._show_shortcuts_dialog()
  769. class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
  770. def trigger(self, *args, **kwargs):
  771. clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  772. window = self.canvas.get_window()
  773. x, y, width, height = window.get_geometry()
  774. pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
  775. clipboard.set_image(pb)
  776. # Define the file to use as the GTk icon
  777. if sys.platform == 'win32':
  778. icon_filename = 'matplotlib.png'
  779. else:
  780. icon_filename = 'matplotlib.svg'
  781. window_icon = str(cbook._get_data_path('images', icon_filename))
  782. def error_msg_gtk(msg, parent=None):
  783. if parent is not None: # find the toplevel Gtk.Window
  784. parent = parent.get_toplevel()
  785. if not parent.is_toplevel():
  786. parent = None
  787. if not isinstance(msg, str):
  788. msg = ','.join(map(str, msg))
  789. dialog = Gtk.MessageDialog(
  790. parent=parent, type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK,
  791. message_format=msg)
  792. dialog.run()
  793. dialog.destroy()
  794. backend_tools.ToolSaveFigure = SaveFigureGTK3
  795. backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
  796. backend_tools.ToolSetCursor = SetCursorGTK3
  797. backend_tools.ToolRubberband = RubberbandGTK3
  798. backend_tools.ToolHelp = HelpGTK3
  799. backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3
  800. Toolbar = ToolbarGTK3
  801. @_Backend.export
  802. class _BackendGTK3(_Backend):
  803. FigureCanvas = FigureCanvasGTK3
  804. FigureManager = FigureManagerGTK3
  805. @staticmethod
  806. def trigger_manager_draw(manager):
  807. manager.canvas.draw_idle()
  808. @staticmethod
  809. def mainloop():
  810. if Gtk.main_level() == 0:
  811. Gtk.main()