backend_gtk3.py 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. import functools
  2. import logging
  3. import os
  4. from pathlib import Path
  5. import sys
  6. import matplotlib
  7. from matplotlib import backend_tools, cbook, rcParams
  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.backend_managers import ToolManager
  13. from matplotlib.figure import Figure
  14. from matplotlib.widgets import SubplotTool
  15. try:
  16. import gi
  17. except ImportError:
  18. raise ImportError("The GTK3 backends require PyGObject")
  19. try:
  20. # :raises ValueError: If module/version is already loaded, already
  21. # required, or unavailable.
  22. gi.require_version("Gtk", "3.0")
  23. except ValueError as e:
  24. # in this case we want to re-raise as ImportError so the
  25. # auto-backend selection logic correctly skips.
  26. raise ImportError from e
  27. from gi.repository import GLib, GObject, Gtk, Gdk
  28. _log = logging.getLogger(__name__)
  29. backend_version = "%s.%s.%s" % (
  30. Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version())
  31. try:
  32. cursord = {
  33. cursors.MOVE: Gdk.Cursor.new(Gdk.CursorType.FLEUR),
  34. cursors.HAND: Gdk.Cursor.new(Gdk.CursorType.HAND2),
  35. cursors.POINTER: Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR),
  36. cursors.SELECT_REGION: Gdk.Cursor.new(Gdk.CursorType.TCROSS),
  37. cursors.WAIT: Gdk.Cursor.new(Gdk.CursorType.WATCH),
  38. }
  39. except TypeError as exc:
  40. # Happens when running headless. Convert to ImportError to cooperate with
  41. # backend switching.
  42. raise ImportError(exc)
  43. class TimerGTK3(TimerBase):
  44. """
  45. Subclass of `.TimerBase` using GTK3 for timer events.
  46. Attributes
  47. ----------
  48. interval : int
  49. The time between timer events in milliseconds. Default is 1000 ms.
  50. single_shot : bool
  51. Boolean flag indicating whether this timer should operate as single
  52. shot (run once and then stop). Defaults to False.
  53. callbacks : list
  54. Stores list of (func, args) tuples that will be called upon timer
  55. events. This list can be manipulated directly, or the functions
  56. `add_callback` and `remove_callback` can be used.
  57. """
  58. def _timer_start(self):
  59. # Need to stop it, otherwise we potentially leak a timer id that will
  60. # never be stopped.
  61. self._timer_stop()
  62. self._timer = GLib.timeout_add(self._interval, self._on_timer)
  63. def _timer_stop(self):
  64. if self._timer is not None:
  65. GLib.source_remove(self._timer)
  66. self._timer = None
  67. def _timer_set_interval(self):
  68. # Only stop and restart it if the timer has already been started
  69. if self._timer is not None:
  70. self._timer_stop()
  71. self._timer_start()
  72. def _on_timer(self):
  73. TimerBase._on_timer(self)
  74. # Gtk timeout_add() requires that the callback returns True if it
  75. # is to be called again.
  76. if self.callbacks and not self._single:
  77. return True
  78. else:
  79. self._timer = None
  80. return False
  81. class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
  82. required_interactive_framework = "gtk3"
  83. keyvald = {65507: 'control',
  84. 65505: 'shift',
  85. 65513: 'alt',
  86. 65508: 'control',
  87. 65506: 'shift',
  88. 65514: 'alt',
  89. 65361: 'left',
  90. 65362: 'up',
  91. 65363: 'right',
  92. 65364: 'down',
  93. 65307: 'escape',
  94. 65470: 'f1',
  95. 65471: 'f2',
  96. 65472: 'f3',
  97. 65473: 'f4',
  98. 65474: 'f5',
  99. 65475: 'f6',
  100. 65476: 'f7',
  101. 65477: 'f8',
  102. 65478: 'f9',
  103. 65479: 'f10',
  104. 65480: 'f11',
  105. 65481: 'f12',
  106. 65300: 'scroll_lock',
  107. 65299: 'break',
  108. 65288: 'backspace',
  109. 65293: 'enter',
  110. 65379: 'insert',
  111. 65535: 'delete',
  112. 65360: 'home',
  113. 65367: 'end',
  114. 65365: 'pageup',
  115. 65366: 'pagedown',
  116. 65438: '0',
  117. 65436: '1',
  118. 65433: '2',
  119. 65435: '3',
  120. 65430: '4',
  121. 65437: '5',
  122. 65432: '6',
  123. 65429: '7',
  124. 65431: '8',
  125. 65434: '9',
  126. 65451: '+',
  127. 65453: '-',
  128. 65450: '*',
  129. 65455: '/',
  130. 65439: 'dec',
  131. 65421: 'enter',
  132. }
  133. # Setting this as a static constant prevents
  134. # this resulting expression from leaking
  135. event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK
  136. | Gdk.EventMask.BUTTON_RELEASE_MASK
  137. | Gdk.EventMask.EXPOSURE_MASK
  138. | Gdk.EventMask.KEY_PRESS_MASK
  139. | Gdk.EventMask.KEY_RELEASE_MASK
  140. | Gdk.EventMask.ENTER_NOTIFY_MASK
  141. | Gdk.EventMask.LEAVE_NOTIFY_MASK
  142. | Gdk.EventMask.POINTER_MOTION_MASK
  143. | Gdk.EventMask.POINTER_MOTION_HINT_MASK
  144. | Gdk.EventMask.SCROLL_MASK)
  145. def __init__(self, figure):
  146. FigureCanvasBase.__init__(self, figure)
  147. GObject.GObject.__init__(self)
  148. self._idle_draw_id = 0
  149. self._lastCursor = None
  150. self.connect('scroll_event', self.scroll_event)
  151. self.connect('button_press_event', self.button_press_event)
  152. self.connect('button_release_event', self.button_release_event)
  153. self.connect('configure_event', self.configure_event)
  154. self.connect('draw', self.on_draw_event)
  155. self.connect('key_press_event', self.key_press_event)
  156. self.connect('key_release_event', self.key_release_event)
  157. self.connect('motion_notify_event', self.motion_notify_event)
  158. self.connect('leave_notify_event', self.leave_notify_event)
  159. self.connect('enter_notify_event', self.enter_notify_event)
  160. self.connect('size_allocate', self.size_allocate)
  161. self.set_events(self.__class__.event_mask)
  162. self.set_double_buffered(True)
  163. self.set_can_focus(True)
  164. self._renderer_init()
  165. def destroy(self):
  166. #Gtk.DrawingArea.destroy(self)
  167. self.close_event()
  168. if self._idle_draw_id != 0:
  169. GLib.source_remove(self._idle_draw_id)
  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 on_draw_event(self, widget, ctx):
  249. # to be overwritten by GTK3Agg or GTK3Cairo
  250. pass
  251. def draw(self):
  252. # docstring inherited
  253. if self.is_drawable():
  254. self.queue_draw()
  255. def draw_idle(self):
  256. # docstring inherited
  257. if self._idle_draw_id != 0:
  258. return
  259. def idle_draw(*args):
  260. try:
  261. self.draw()
  262. finally:
  263. self._idle_draw_id = 0
  264. return False
  265. self._idle_draw_id = GLib.idle_add(idle_draw)
  266. def new_timer(self, *args, **kwargs):
  267. # docstring inherited
  268. return TimerGTK3(*args, **kwargs)
  269. def flush_events(self):
  270. # docstring inherited
  271. Gdk.threads_enter()
  272. while Gtk.events_pending():
  273. Gtk.main_iteration()
  274. Gdk.flush()
  275. Gdk.threads_leave()
  276. class FigureManagerGTK3(FigureManagerBase):
  277. """
  278. Attributes
  279. ----------
  280. canvas : `FigureCanvas`
  281. The FigureCanvas instance
  282. num : int or str
  283. The Figure number
  284. toolbar : Gtk.Toolbar
  285. The Gtk.Toolbar
  286. vbox : Gtk.VBox
  287. The Gtk.VBox containing the canvas and toolbar
  288. window : Gtk.Window
  289. The Gtk.Window
  290. """
  291. def __init__(self, canvas, num):
  292. FigureManagerBase.__init__(self, canvas, num)
  293. self.window = Gtk.Window()
  294. self.window.set_wmclass("matplotlib", "Matplotlib")
  295. self.set_window_title("Figure %d" % num)
  296. try:
  297. self.window.set_icon_from_file(window_icon)
  298. except Exception:
  299. # Some versions of gtk throw a glib.GError but not all, so I am not
  300. # sure how to catch it. I am unhappy doing a blanket catch here,
  301. # but am not sure what a better way is - JDH
  302. _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1])
  303. self.vbox = Gtk.Box()
  304. self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  305. self.window.add(self.vbox)
  306. self.vbox.show()
  307. self.canvas.show()
  308. self.vbox.pack_start(self.canvas, True, True, 0)
  309. # calculate size for window
  310. w = int(self.canvas.figure.bbox.width)
  311. h = int(self.canvas.figure.bbox.height)
  312. self.toolmanager = self._get_toolmanager()
  313. self.toolbar = self._get_toolbar()
  314. self.statusbar = None
  315. def add_widget(child, expand, fill, padding):
  316. child.show()
  317. self.vbox.pack_end(child, False, False, 0)
  318. size_request = child.size_request()
  319. return size_request.height
  320. if self.toolmanager:
  321. backend_tools.add_tools_to_manager(self.toolmanager)
  322. if self.toolbar:
  323. backend_tools.add_tools_to_container(self.toolbar)
  324. self.statusbar = StatusbarGTK3(self.toolmanager)
  325. h += add_widget(self.statusbar, False, False, 0)
  326. h += add_widget(Gtk.HSeparator(), False, False, 0)
  327. if self.toolbar is not None:
  328. self.toolbar.show()
  329. h += add_widget(self.toolbar, False, False, 0)
  330. self.window.set_default_size(w, h)
  331. def destroy(*args):
  332. Gcf.destroy(num)
  333. self.window.connect("destroy", destroy)
  334. self.window.connect("delete_event", destroy)
  335. if matplotlib.is_interactive():
  336. self.window.show()
  337. self.canvas.draw_idle()
  338. self.canvas.grab_focus()
  339. def destroy(self, *args):
  340. self.vbox.destroy()
  341. self.window.destroy()
  342. self.canvas.destroy()
  343. if self.toolbar:
  344. self.toolbar.destroy()
  345. if (Gcf.get_num_fig_managers() == 0 and
  346. not matplotlib.is_interactive() and
  347. Gtk.main_level() >= 1):
  348. Gtk.main_quit()
  349. def show(self):
  350. # show the figure window
  351. self.window.show()
  352. self.window.present()
  353. def full_screen_toggle(self):
  354. self._full_screen_flag = not self._full_screen_flag
  355. if self._full_screen_flag:
  356. self.window.fullscreen()
  357. else:
  358. self.window.unfullscreen()
  359. _full_screen_flag = False
  360. def _get_toolbar(self):
  361. # must be inited after the window, drawingArea and figure
  362. # attrs are set
  363. if rcParams['toolbar'] == 'toolbar2':
  364. toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
  365. elif rcParams['toolbar'] == 'toolmanager':
  366. toolbar = ToolbarGTK3(self.toolmanager)
  367. else:
  368. toolbar = None
  369. return toolbar
  370. def _get_toolmanager(self):
  371. # must be initialised after toolbar has been set
  372. if rcParams['toolbar'] == 'toolmanager':
  373. toolmanager = ToolManager(self.canvas.figure)
  374. else:
  375. toolmanager = None
  376. return toolmanager
  377. def get_window_title(self):
  378. return self.window.get_title()
  379. def set_window_title(self, title):
  380. self.window.set_title(title)
  381. def resize(self, width, height):
  382. 'set the canvas size in pixels'
  383. #_, _, cw, ch = self.canvas.allocation
  384. #_, _, ww, wh = self.window.allocation
  385. #self.window.resize (width-cw+ww, height-ch+wh)
  386. self.window.resize(width, height)
  387. class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
  388. def __init__(self, canvas, window):
  389. self.win = window
  390. GObject.GObject.__init__(self)
  391. NavigationToolbar2.__init__(self, canvas)
  392. self.ctx = None
  393. def set_message(self, s):
  394. self.message.set_label(s)
  395. def set_cursor(self, cursor):
  396. self.canvas.get_property("window").set_cursor(cursord[cursor])
  397. Gtk.main_iteration()
  398. def draw_rubberband(self, event, x0, y0, x1, y1):
  399. # adapted from
  400. # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744
  401. self.ctx = self.canvas.get_property("window").cairo_create()
  402. # todo: instead of redrawing the entire figure, copy the part of
  403. # the figure that was covered by the previous rubberband rectangle
  404. self.canvas.draw()
  405. height = self.canvas.figure.bbox.height
  406. y1 = height - y1
  407. y0 = height - y0
  408. w = abs(x1 - x0)
  409. h = abs(y1 - y0)
  410. rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)]
  411. self.ctx.new_path()
  412. self.ctx.set_line_width(0.5)
  413. self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
  414. self.ctx.set_source_rgb(0, 0, 0)
  415. self.ctx.stroke()
  416. def _init_toolbar(self):
  417. self.set_style(Gtk.ToolbarStyle.ICONS)
  418. self._gtk_ids = {}
  419. for text, tooltip_text, image_file, callback in self.toolitems:
  420. if text is None:
  421. self.insert(Gtk.SeparatorToolItem(), -1)
  422. continue
  423. image = Gtk.Image()
  424. image.set_from_file(
  425. str(cbook._get_data_path('images', image_file + '.png')))
  426. self._gtk_ids[text] = tbutton = Gtk.ToolButton()
  427. tbutton.set_label(text)
  428. tbutton.set_icon_widget(image)
  429. self.insert(tbutton, -1)
  430. tbutton.connect('clicked', getattr(self, callback))
  431. tbutton.set_tooltip_text(tooltip_text)
  432. toolitem = Gtk.SeparatorToolItem()
  433. self.insert(toolitem, -1)
  434. toolitem.set_draw(False)
  435. toolitem.set_expand(True)
  436. toolitem = Gtk.ToolItem()
  437. self.insert(toolitem, -1)
  438. self.message = Gtk.Label()
  439. toolitem.add(self.message)
  440. self.show_all()
  441. @cbook.deprecated("3.1")
  442. def get_filechooser(self):
  443. fc = FileChooserDialog(
  444. title='Save the figure',
  445. parent=self.win,
  446. path=os.path.expanduser(rcParams['savefig.directory']),
  447. filetypes=self.canvas.get_supported_filetypes(),
  448. default_filetype=self.canvas.get_default_filetype())
  449. fc.set_current_name(self.canvas.get_default_filename())
  450. return fc
  451. def save_figure(self, *args):
  452. dialog = Gtk.FileChooserDialog(
  453. title="Save the figure",
  454. parent=self.canvas.get_toplevel(),
  455. action=Gtk.FileChooserAction.SAVE,
  456. buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  457. Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
  458. )
  459. for name, fmts \
  460. in self.canvas.get_supported_filetypes_grouped().items():
  461. ff = Gtk.FileFilter()
  462. ff.set_name(name)
  463. for fmt in fmts:
  464. ff.add_pattern("*." + fmt)
  465. dialog.add_filter(ff)
  466. if self.canvas.get_default_filetype() in fmts:
  467. dialog.set_filter(ff)
  468. @functools.partial(dialog.connect, "notify::filter")
  469. def on_notify_filter(*args):
  470. name = dialog.get_filter().get_name()
  471. fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
  472. dialog.set_current_name(
  473. str(Path(dialog.get_current_name()).with_suffix("." + fmt)))
  474. dialog.set_current_folder(rcParams["savefig.directory"])
  475. dialog.set_current_name(self.canvas.get_default_filename())
  476. dialog.set_do_overwrite_confirmation(True)
  477. response = dialog.run()
  478. fname = dialog.get_filename()
  479. ff = dialog.get_filter() # Doesn't autoadjust to filename :/
  480. fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
  481. dialog.destroy()
  482. if response == Gtk.ResponseType.CANCEL:
  483. return
  484. # Save dir for next time, unless empty str (which means use cwd).
  485. if rcParams['savefig.directory']:
  486. rcParams['savefig.directory'] = os.path.dirname(fname)
  487. try:
  488. self.canvas.figure.savefig(fname, format=fmt)
  489. except Exception as e:
  490. error_msg_gtk(str(e), parent=self)
  491. def configure_subplots(self, button):
  492. toolfig = Figure(figsize=(6, 3))
  493. canvas = type(self.canvas)(toolfig)
  494. toolfig.subplots_adjust(top=0.9)
  495. # Need to keep a reference to the tool.
  496. _tool = SubplotTool(self.canvas.figure, toolfig)
  497. w = int(toolfig.bbox.width)
  498. h = int(toolfig.bbox.height)
  499. window = Gtk.Window()
  500. try:
  501. window.set_icon_from_file(window_icon)
  502. except Exception:
  503. # we presumably already logged a message on the
  504. # failure of the main plot, don't keep reporting
  505. pass
  506. window.set_title("Subplot Configuration Tool")
  507. window.set_default_size(w, h)
  508. vbox = Gtk.Box()
  509. vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  510. window.add(vbox)
  511. vbox.show()
  512. canvas.show()
  513. vbox.pack_start(canvas, True, True, 0)
  514. window.show()
  515. def set_history_buttons(self):
  516. can_backward = self._nav_stack._pos > 0
  517. can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
  518. if 'Back' in self._gtk_ids:
  519. self._gtk_ids['Back'].set_sensitive(can_backward)
  520. if 'Forward' in self._gtk_ids:
  521. self._gtk_ids['Forward'].set_sensitive(can_forward)
  522. @cbook.deprecated("3.1")
  523. class FileChooserDialog(Gtk.FileChooserDialog):
  524. """GTK+ file selector which remembers the last file/directory
  525. selected and presents the user with a menu of supported image formats
  526. """
  527. def __init__(self,
  528. title='Save file',
  529. parent=None,
  530. action=Gtk.FileChooserAction.SAVE,
  531. buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  532. Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
  533. path=None,
  534. filetypes=[],
  535. default_filetype=None,
  536. ):
  537. super().__init__(title, parent, action, buttons)
  538. self.set_default_response(Gtk.ResponseType.OK)
  539. self.set_do_overwrite_confirmation(True)
  540. if not path:
  541. path = os.getcwd()
  542. # create an extra widget to list supported image formats
  543. self.set_current_folder(path)
  544. self.set_current_name('image.' + default_filetype)
  545. hbox = Gtk.Box(spacing=10)
  546. hbox.pack_start(Gtk.Label(label="File Format:"), False, False, 0)
  547. liststore = Gtk.ListStore(GObject.TYPE_STRING)
  548. cbox = Gtk.ComboBox()
  549. cbox.set_model(liststore)
  550. cell = Gtk.CellRendererText()
  551. cbox.pack_start(cell, True)
  552. cbox.add_attribute(cell, 'text', 0)
  553. hbox.pack_start(cbox, False, False, 0)
  554. self.filetypes = filetypes
  555. sorted_filetypes = sorted(filetypes.items())
  556. default = 0
  557. for i, (ext, name) in enumerate(sorted_filetypes):
  558. liststore.append(["%s (*.%s)" % (name, ext)])
  559. if ext == default_filetype:
  560. default = i
  561. cbox.set_active(default)
  562. self.ext = default_filetype
  563. def cb_cbox_changed(cbox, data=None):
  564. """File extension changed"""
  565. head, filename = os.path.split(self.get_filename())
  566. root, ext = os.path.splitext(filename)
  567. ext = ext[1:]
  568. new_ext = sorted_filetypes[cbox.get_active()][0]
  569. self.ext = new_ext
  570. if ext in self.filetypes:
  571. filename = root + '.' + new_ext
  572. elif ext == '':
  573. filename = filename.rstrip('.') + '.' + new_ext
  574. self.set_current_name(filename)
  575. cbox.connect("changed", cb_cbox_changed)
  576. hbox.show_all()
  577. self.set_extra_widget(hbox)
  578. def get_filename_from_user(self):
  579. if self.run() == int(Gtk.ResponseType.OK):
  580. return self.get_filename(), self.ext
  581. else:
  582. return None, self.ext
  583. class ToolbarGTK3(ToolContainerBase, Gtk.Box):
  584. _icon_extension = '.png'
  585. def __init__(self, toolmanager):
  586. ToolContainerBase.__init__(self, toolmanager)
  587. Gtk.Box.__init__(self)
  588. self.set_property("orientation", Gtk.Orientation.VERTICAL)
  589. self._toolarea = Gtk.Box()
  590. self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL)
  591. self.pack_start(self._toolarea, False, False, 0)
  592. self._toolarea.show_all()
  593. self._groups = {}
  594. self._toolitems = {}
  595. def add_toolitem(self, name, group, position, image_file, description,
  596. toggle):
  597. if toggle:
  598. tbutton = Gtk.ToggleToolButton()
  599. else:
  600. tbutton = Gtk.ToolButton()
  601. tbutton.set_label(name)
  602. if image_file is not None:
  603. image = Gtk.Image()
  604. image.set_from_file(image_file)
  605. tbutton.set_icon_widget(image)
  606. if position is None:
  607. position = -1
  608. self._add_button(tbutton, group, position)
  609. signal = tbutton.connect('clicked', self._call_tool, name)
  610. tbutton.set_tooltip_text(description)
  611. tbutton.show_all()
  612. self._toolitems.setdefault(name, [])
  613. self._toolitems[name].append((tbutton, signal))
  614. def _add_button(self, button, group, position):
  615. if group not in self._groups:
  616. if self._groups:
  617. self._add_separator()
  618. toolbar = Gtk.Toolbar()
  619. toolbar.set_style(Gtk.ToolbarStyle.ICONS)
  620. self._toolarea.pack_start(toolbar, False, False, 0)
  621. toolbar.show_all()
  622. self._groups[group] = toolbar
  623. self._groups[group].insert(button, position)
  624. def _call_tool(self, btn, name):
  625. self.trigger_tool(name)
  626. def toggle_toolitem(self, name, toggled):
  627. if name not in self._toolitems:
  628. return
  629. for toolitem, signal in self._toolitems[name]:
  630. toolitem.handler_block(signal)
  631. toolitem.set_active(toggled)
  632. toolitem.handler_unblock(signal)
  633. def remove_toolitem(self, name):
  634. if name not in self._toolitems:
  635. self.toolmanager.message_event('%s Not in toolbar' % name, self)
  636. return
  637. for group in self._groups:
  638. for toolitem, _signal in self._toolitems[name]:
  639. if toolitem in self._groups[group]:
  640. self._groups[group].remove(toolitem)
  641. del self._toolitems[name]
  642. def _add_separator(self):
  643. sep = Gtk.Separator()
  644. sep.set_property("orientation", Gtk.Orientation.VERTICAL)
  645. self._toolarea.pack_start(sep, False, True, 0)
  646. sep.show_all()
  647. class StatusbarGTK3(StatusbarBase, Gtk.Statusbar):
  648. def __init__(self, *args, **kwargs):
  649. StatusbarBase.__init__(self, *args, **kwargs)
  650. Gtk.Statusbar.__init__(self)
  651. self._context = self.get_context_id('message')
  652. def set_message(self, s):
  653. self.pop(self._context)
  654. self.push(self._context, s)
  655. class RubberbandGTK3(backend_tools.RubberbandBase):
  656. def draw_rubberband(self, x0, y0, x1, y1):
  657. NavigationToolbar2GTK3.draw_rubberband(
  658. self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
  659. class SaveFigureGTK3(backend_tools.SaveFigureBase):
  660. @cbook.deprecated("3.1")
  661. def get_filechooser(self):
  662. fc = FileChooserDialog(
  663. title='Save the figure',
  664. parent=self.figure.canvas.manager.window,
  665. path=os.path.expanduser(rcParams['savefig.directory']),
  666. filetypes=self.figure.canvas.get_supported_filetypes(),
  667. default_filetype=self.figure.canvas.get_default_filetype())
  668. fc.set_current_name(self.figure.canvas.get_default_filename())
  669. return fc
  670. def trigger(self, *args, **kwargs):
  671. class PseudoToolbar:
  672. canvas = self.figure.canvas
  673. return NavigationToolbar2GTK3.save_figure(PseudoToolbar())
  674. class SetCursorGTK3(backend_tools.SetCursorBase):
  675. def set_cursor(self, cursor):
  676. NavigationToolbar2GTK3.set_cursor(
  677. self._make_classic_style_pseudo_toolbar(), cursor)
  678. class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
  679. @cbook.deprecated("3.2")
  680. @property
  681. def window(self):
  682. if not hasattr(self, "_window"):
  683. self._window = None
  684. return self._window
  685. @window.setter
  686. @cbook.deprecated("3.2")
  687. def window(self, window):
  688. self._window = window
  689. @cbook.deprecated("3.2")
  690. def init_window(self):
  691. if self.window:
  692. return
  693. self.window = Gtk.Window(title="Subplot Configuration Tool")
  694. try:
  695. self.window.window.set_icon_from_file(window_icon)
  696. except Exception:
  697. # we presumably already logged a message on the
  698. # failure of the main plot, don't keep reporting
  699. pass
  700. self.vbox = Gtk.Box()
  701. self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
  702. self.window.add(self.vbox)
  703. self.vbox.show()
  704. self.window.connect('destroy', self.destroy)
  705. toolfig = Figure(figsize=(6, 3))
  706. canvas = self.figure.canvas.__class__(toolfig)
  707. toolfig.subplots_adjust(top=0.9)
  708. SubplotTool(self.figure, toolfig)
  709. w = int(toolfig.bbox.width)
  710. h = int(toolfig.bbox.height)
  711. self.window.set_default_size(w, h)
  712. canvas.show()
  713. self.vbox.pack_start(canvas, True, True, 0)
  714. self.window.show()
  715. @cbook.deprecated("3.2")
  716. def destroy(self, *args):
  717. self.window.destroy()
  718. self.window = None
  719. def _get_canvas(self, fig):
  720. return self.canvas.__class__(fig)
  721. def trigger(self, *args):
  722. NavigationToolbar2GTK3.configure_subplots(
  723. self._make_classic_style_pseudo_toolbar(), None)
  724. class HelpGTK3(backend_tools.ToolHelpBase):
  725. def _normalize_shortcut(self, key):
  726. """
  727. Convert Matplotlib key presses to GTK+ accelerator identifiers.
  728. Related to `FigureCanvasGTK3._get_key`.
  729. """
  730. special = {
  731. 'backspace': 'BackSpace',
  732. 'pagedown': 'Page_Down',
  733. 'pageup': 'Page_Up',
  734. 'scroll_lock': 'Scroll_Lock',
  735. }
  736. parts = key.split('+')
  737. mods = ['<' + mod + '>' for mod in parts[:-1]]
  738. key = parts[-1]
  739. if key in special:
  740. key = special[key]
  741. elif len(key) > 1:
  742. key = key.capitalize()
  743. elif key.isupper():
  744. mods += ['<shift>']
  745. return ''.join(mods) + key
  746. def _show_shortcuts_window(self):
  747. section = Gtk.ShortcutsSection()
  748. for name, tool in sorted(self.toolmanager.tools.items()):
  749. if not tool.description:
  750. continue
  751. # Putting everything in a separate group allows GTK to
  752. # automatically split them into separate columns/pages, which is
  753. # useful because we have lots of shortcuts, some with many keys
  754. # that are very wide.
  755. group = Gtk.ShortcutsGroup()
  756. section.add(group)
  757. # A hack to remove the title since we have no group naming.
  758. group.forall(lambda widget, data: widget.set_visible(False), None)
  759. shortcut = Gtk.ShortcutsShortcut(
  760. accelerator=' '.join(
  761. self._normalize_shortcut(key)
  762. for key in self.toolmanager.get_tool_keymap(name)
  763. # Will never be sent:
  764. if 'cmd+' not in key),
  765. title=tool.name,
  766. subtitle=tool.description)
  767. group.add(shortcut)
  768. window = Gtk.ShortcutsWindow(
  769. title='Help',
  770. modal=True,
  771. transient_for=self._figure.canvas.get_toplevel())
  772. section.show() # Must be done explicitly before add!
  773. window.add(section)
  774. window.show_all()
  775. def _show_shortcuts_dialog(self):
  776. dialog = Gtk.MessageDialog(
  777. self._figure.canvas.get_toplevel(),
  778. 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
  779. title="Help")
  780. dialog.run()
  781. dialog.destroy()
  782. def trigger(self, *args):
  783. if Gtk.check_version(3, 20, 0) is None:
  784. self._show_shortcuts_window()
  785. else:
  786. self._show_shortcuts_dialog()
  787. class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
  788. def trigger(self, *args, **kwargs):
  789. clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  790. window = self.canvas.get_window()
  791. x, y, width, height = window.get_geometry()
  792. pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
  793. clipboard.set_image(pb)
  794. # Define the file to use as the GTk icon
  795. if sys.platform == 'win32':
  796. icon_filename = 'matplotlib.png'
  797. else:
  798. icon_filename = 'matplotlib.svg'
  799. window_icon = str(cbook._get_data_path('images', icon_filename))
  800. def error_msg_gtk(msg, parent=None):
  801. if parent is not None: # find the toplevel Gtk.Window
  802. parent = parent.get_toplevel()
  803. if not parent.is_toplevel():
  804. parent = None
  805. if not isinstance(msg, str):
  806. msg = ','.join(map(str, msg))
  807. dialog = Gtk.MessageDialog(
  808. parent=parent, type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK,
  809. message_format=msg)
  810. dialog.run()
  811. dialog.destroy()
  812. backend_tools.ToolSaveFigure = SaveFigureGTK3
  813. backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
  814. backend_tools.ToolSetCursor = SetCursorGTK3
  815. backend_tools.ToolRubberband = RubberbandGTK3
  816. backend_tools.ToolHelp = HelpGTK3
  817. backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3
  818. Toolbar = ToolbarGTK3
  819. @_Backend.export
  820. class _BackendGTK3(_Backend):
  821. FigureCanvas = FigureCanvasGTK3
  822. FigureManager = FigureManagerGTK3
  823. @staticmethod
  824. def trigger_manager_draw(manager):
  825. manager.canvas.draw_idle()
  826. @staticmethod
  827. def mainloop():
  828. if Gtk.main_level() == 0:
  829. Gtk.main()