_backend_tk.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. from contextlib import contextmanager
  2. import logging
  3. import math
  4. import os.path
  5. import sys
  6. import tkinter as tk
  7. from tkinter.simpledialog import SimpleDialog
  8. import tkinter.filedialog
  9. import tkinter.messagebox
  10. import numpy as np
  11. import matplotlib
  12. from matplotlib import backend_tools, cbook, rcParams
  13. from matplotlib.backend_bases import (
  14. _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
  15. StatusbarBase, TimerBase, ToolContainerBase, cursors)
  16. from matplotlib.backend_managers import ToolManager
  17. from matplotlib._pylab_helpers import Gcf
  18. from matplotlib.figure import Figure
  19. from matplotlib.widgets import SubplotTool
  20. from . import _tkagg
  21. try:
  22. from ._tkagg import Win32_GetForegroundWindow, Win32_SetForegroundWindow
  23. except ImportError:
  24. @contextmanager
  25. def _restore_foreground_window_at_end():
  26. yield
  27. else:
  28. @contextmanager
  29. def _restore_foreground_window_at_end():
  30. foreground = Win32_GetForegroundWindow()
  31. try:
  32. yield
  33. finally:
  34. if rcParams['tk.window_focus']:
  35. Win32_SetForegroundWindow(foreground)
  36. _log = logging.getLogger(__name__)
  37. backend_version = tk.TkVersion
  38. cursord = {
  39. cursors.MOVE: "fleur",
  40. cursors.HAND: "hand2",
  41. cursors.POINTER: "arrow",
  42. cursors.SELECT_REGION: "tcross",
  43. cursors.WAIT: "watch",
  44. }
  45. def blit(photoimage, aggimage, offsets, bbox=None):
  46. """
  47. Blit *aggimage* to *photoimage*.
  48. *offsets* is a tuple describing how to fill the ``offset`` field of the
  49. ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data,
  50. (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0)
  51. for big-endian ARGB32 (i.e. ARGB8888) data.
  52. If *bbox* is passed, it defines the region that gets blitted.
  53. """
  54. data = np.asarray(aggimage)
  55. height, width = data.shape[:2]
  56. dataptr = (height, width, data.ctypes.data)
  57. if bbox is not None:
  58. (x1, y1), (x2, y2) = bbox.__array__()
  59. x1 = max(math.floor(x1), 0)
  60. x2 = min(math.ceil(x2), width)
  61. y1 = max(math.floor(y1), 0)
  62. y2 = min(math.ceil(y2), height)
  63. bboxptr = (x1, x2, y1, y2)
  64. else:
  65. photoimage.blank()
  66. bboxptr = (0, width, 0, height)
  67. _tkagg.blit(
  68. photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr)
  69. class TimerTk(TimerBase):
  70. '''
  71. Subclass of :class:`backend_bases.TimerBase` that uses Tk's timer events.
  72. Attributes
  73. ----------
  74. interval : int
  75. The time between timer events in milliseconds. Default is 1000 ms.
  76. single_shot : bool
  77. Boolean flag indicating whether this timer should operate as single
  78. shot (run once and then stop). Defaults to False.
  79. callbacks : list
  80. Stores list of (func, args) tuples that will be called upon timer
  81. events. This list can be manipulated directly, or the functions
  82. `add_callback` and `remove_callback` can be used.
  83. '''
  84. def __init__(self, parent, *args, **kwargs):
  85. TimerBase.__init__(self, *args, **kwargs)
  86. self.parent = parent
  87. self._timer = None
  88. def _timer_start(self):
  89. self._timer_stop()
  90. self._timer = self.parent.after(self._interval, self._on_timer)
  91. def _timer_stop(self):
  92. if self._timer is not None:
  93. self.parent.after_cancel(self._timer)
  94. self._timer = None
  95. def _on_timer(self):
  96. TimerBase._on_timer(self)
  97. # Tk after() is only a single shot, so we need to add code here to
  98. # reset the timer if we're not operating in single shot mode. However,
  99. # if _timer is None, this means that _timer_stop has been called; so
  100. # don't recreate the timer in that case.
  101. if not self._single and self._timer:
  102. self._timer = self.parent.after(self._interval, self._on_timer)
  103. else:
  104. self._timer = None
  105. class FigureCanvasTk(FigureCanvasBase):
  106. required_interactive_framework = "tk"
  107. keyvald = {65507: 'control',
  108. 65505: 'shift',
  109. 65513: 'alt',
  110. 65515: 'super',
  111. 65508: 'control',
  112. 65506: 'shift',
  113. 65514: 'alt',
  114. 65361: 'left',
  115. 65362: 'up',
  116. 65363: 'right',
  117. 65364: 'down',
  118. 65307: 'escape',
  119. 65470: 'f1',
  120. 65471: 'f2',
  121. 65472: 'f3',
  122. 65473: 'f4',
  123. 65474: 'f5',
  124. 65475: 'f6',
  125. 65476: 'f7',
  126. 65477: 'f8',
  127. 65478: 'f9',
  128. 65479: 'f10',
  129. 65480: 'f11',
  130. 65481: 'f12',
  131. 65300: 'scroll_lock',
  132. 65299: 'break',
  133. 65288: 'backspace',
  134. 65293: 'enter',
  135. 65379: 'insert',
  136. 65535: 'delete',
  137. 65360: 'home',
  138. 65367: 'end',
  139. 65365: 'pageup',
  140. 65366: 'pagedown',
  141. 65438: '0',
  142. 65436: '1',
  143. 65433: '2',
  144. 65435: '3',
  145. 65430: '4',
  146. 65437: '5',
  147. 65432: '6',
  148. 65429: '7',
  149. 65431: '8',
  150. 65434: '9',
  151. 65451: '+',
  152. 65453: '-',
  153. 65450: '*',
  154. 65455: '/',
  155. 65439: 'dec',
  156. 65421: 'enter',
  157. }
  158. _keycode_lookup = {
  159. 262145: 'control',
  160. 524320: 'alt',
  161. 524352: 'alt',
  162. 1048584: 'super',
  163. 1048592: 'super',
  164. 131074: 'shift',
  165. 131076: 'shift',
  166. }
  167. """_keycode_lookup is used for badly mapped (i.e. no event.key_sym set)
  168. keys on apple keyboards."""
  169. def __init__(self, figure, master=None, resize_callback=None):
  170. super(FigureCanvasTk, self).__init__(figure)
  171. self._idle = True
  172. self._idle_callback = None
  173. t1, t2, w, h = self.figure.bbox.bounds
  174. w, h = int(w), int(h)
  175. self._tkcanvas = tk.Canvas(
  176. master=master, background="white",
  177. width=w, height=h, borderwidth=0, highlightthickness=0)
  178. self._tkphoto = tk.PhotoImage(
  179. master=self._tkcanvas, width=w, height=h)
  180. self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto)
  181. self._resize_callback = resize_callback
  182. self._tkcanvas.bind("<Configure>", self.resize)
  183. self._tkcanvas.bind("<Key>", self.key_press)
  184. self._tkcanvas.bind("<Motion>", self.motion_notify_event)
  185. self._tkcanvas.bind("<Enter>", self.enter_notify_event)
  186. self._tkcanvas.bind("<Leave>", self.leave_notify_event)
  187. self._tkcanvas.bind("<KeyRelease>", self.key_release)
  188. for name in ["<Button-1>", "<Button-2>", "<Button-3>"]:
  189. self._tkcanvas.bind(name, self.button_press_event)
  190. for name in [
  191. "<Double-Button-1>", "<Double-Button-2>", "<Double-Button-3>"]:
  192. self._tkcanvas.bind(name, self.button_dblclick_event)
  193. for name in [
  194. "<ButtonRelease-1>", "<ButtonRelease-2>", "<ButtonRelease-3>"]:
  195. self._tkcanvas.bind(name, self.button_release_event)
  196. # Mouse wheel on Linux generates button 4/5 events
  197. for name in "<Button-4>", "<Button-5>":
  198. self._tkcanvas.bind(name, self.scroll_event)
  199. # Mouse wheel for windows goes to the window with the focus.
  200. # Since the canvas won't usually have the focus, bind the
  201. # event to the window containing the canvas instead.
  202. # See http://wiki.tcl.tk/3893 (mousewheel) for details
  203. root = self._tkcanvas.winfo_toplevel()
  204. root.bind("<MouseWheel>", self.scroll_event_windows, "+")
  205. # Can't get destroy events by binding to _tkcanvas. Therefore, bind
  206. # to the window and filter.
  207. def filter_destroy(evt):
  208. if evt.widget is self._tkcanvas:
  209. self._master.update_idletasks()
  210. self.close_event()
  211. root.bind("<Destroy>", filter_destroy, "+")
  212. self._master = master
  213. self._tkcanvas.focus_set()
  214. def resize(self, event):
  215. width, height = event.width, event.height
  216. if self._resize_callback is not None:
  217. self._resize_callback(event)
  218. # compute desired figure size in inches
  219. dpival = self.figure.dpi
  220. winch = width / dpival
  221. hinch = height / dpival
  222. self.figure.set_size_inches(winch, hinch, forward=False)
  223. self._tkcanvas.delete(self._tkphoto)
  224. self._tkphoto = tk.PhotoImage(
  225. master=self._tkcanvas, width=int(width), height=int(height))
  226. self._tkcanvas.create_image(
  227. int(width / 2), int(height / 2), image=self._tkphoto)
  228. self.resize_event()
  229. self.draw()
  230. def draw_idle(self):
  231. # docstring inherited
  232. if not self._idle:
  233. return
  234. self._idle = False
  235. def idle_draw(*args):
  236. try:
  237. self.draw()
  238. finally:
  239. self._idle = True
  240. self._idle_callback = self._tkcanvas.after_idle(idle_draw)
  241. def get_tk_widget(self):
  242. """Return the Tk widget used to implement FigureCanvasTkAgg.
  243. Although the initial implementation uses a Tk canvas, this routine
  244. is intended to hide that fact.
  245. """
  246. return self._tkcanvas
  247. def motion_notify_event(self, event):
  248. x = event.x
  249. # flipy so y=0 is bottom of canvas
  250. y = self.figure.bbox.height - event.y
  251. FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
  252. def enter_notify_event(self, event):
  253. x = event.x
  254. # flipy so y=0 is bottom of canvas
  255. y = self.figure.bbox.height - event.y
  256. FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
  257. def button_press_event(self, event, dblclick=False):
  258. x = event.x
  259. # flipy so y=0 is bottom of canvas
  260. y = self.figure.bbox.height - event.y
  261. num = getattr(event, 'num', None)
  262. if sys.platform == 'darwin':
  263. # 2 and 3 were reversed on the OSX platform I tested under tkagg.
  264. if num == 2:
  265. num = 3
  266. elif num == 3:
  267. num = 2
  268. FigureCanvasBase.button_press_event(
  269. self, x, y, num, dblclick=dblclick, guiEvent=event)
  270. def button_dblclick_event(self, event):
  271. self.button_press_event(event, dblclick=True)
  272. def button_release_event(self, event):
  273. x = event.x
  274. # flipy so y=0 is bottom of canvas
  275. y = self.figure.bbox.height - event.y
  276. num = getattr(event, 'num', None)
  277. if sys.platform == 'darwin':
  278. # 2 and 3 were reversed on the OSX platform I tested under tkagg.
  279. if num == 2:
  280. num = 3
  281. elif num == 3:
  282. num = 2
  283. FigureCanvasBase.button_release_event(self, x, y, num, guiEvent=event)
  284. def scroll_event(self, event):
  285. x = event.x
  286. y = self.figure.bbox.height - event.y
  287. num = getattr(event, 'num', None)
  288. step = 1 if num == 4 else -1 if num == 5 else 0
  289. FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
  290. def scroll_event_windows(self, event):
  291. """MouseWheel event processor"""
  292. # need to find the window that contains the mouse
  293. w = event.widget.winfo_containing(event.x_root, event.y_root)
  294. if w == self._tkcanvas:
  295. x = event.x_root - w.winfo_rootx()
  296. y = event.y_root - w.winfo_rooty()
  297. y = self.figure.bbox.height - y
  298. step = event.delta/120.
  299. FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
  300. def _get_key(self, event):
  301. val = event.keysym_num
  302. if val in self.keyvald:
  303. key = self.keyvald[val]
  304. elif (val == 0 and sys.platform == 'darwin'
  305. and event.keycode in self._keycode_lookup):
  306. key = self._keycode_lookup[event.keycode]
  307. elif val < 256:
  308. key = chr(val)
  309. else:
  310. key = None
  311. # add modifier keys to the key string. Bit details originate from
  312. # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
  313. # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
  314. # BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080;
  315. # BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400;
  316. # In general, the modifier key is excluded from the modifier flag,
  317. # however this is not the case on "darwin", so double check that
  318. # we aren't adding repeat modifier flags to a modifier key.
  319. if sys.platform == 'win32':
  320. modifiers = [(17, 'alt', 'alt'),
  321. (2, 'ctrl', 'control'),
  322. ]
  323. elif sys.platform == 'darwin':
  324. modifiers = [(3, 'super', 'super'),
  325. (4, 'alt', 'alt'),
  326. (2, 'ctrl', 'control'),
  327. ]
  328. else:
  329. modifiers = [(6, 'super', 'super'),
  330. (3, 'alt', 'alt'),
  331. (2, 'ctrl', 'control'),
  332. ]
  333. if key is not None:
  334. # shift is not added to the keys as this is already accounted for
  335. for bitmask, prefix, key_name in modifiers:
  336. if event.state & (1 << bitmask) and key_name not in key:
  337. key = '{0}+{1}'.format(prefix, key)
  338. return key
  339. def key_press(self, event):
  340. key = self._get_key(event)
  341. FigureCanvasBase.key_press_event(self, key, guiEvent=event)
  342. def key_release(self, event):
  343. key = self._get_key(event)
  344. FigureCanvasBase.key_release_event(self, key, guiEvent=event)
  345. def new_timer(self, *args, **kwargs):
  346. # docstring inherited
  347. return TimerTk(self._tkcanvas, *args, **kwargs)
  348. def flush_events(self):
  349. # docstring inherited
  350. self._master.update()
  351. class FigureManagerTk(FigureManagerBase):
  352. """
  353. Attributes
  354. ----------
  355. canvas : `FigureCanvas`
  356. The FigureCanvas instance
  357. num : int or str
  358. The Figure number
  359. toolbar : tk.Toolbar
  360. The tk.Toolbar
  361. window : tk.Window
  362. The tk.Window
  363. """
  364. def __init__(self, canvas, num, window):
  365. FigureManagerBase.__init__(self, canvas, num)
  366. self.window = window
  367. self.window.withdraw()
  368. self.set_window_title("Figure %d" % num)
  369. self.canvas = canvas
  370. # If using toolmanager it has to be present when initializing the
  371. # toolbar
  372. self.toolmanager = self._get_toolmanager()
  373. # packing toolbar first, because if space is getting low, last packed
  374. # widget is getting shrunk first (-> the canvas)
  375. self.toolbar = self._get_toolbar()
  376. self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
  377. self.statusbar = None
  378. if self.toolmanager:
  379. backend_tools.add_tools_to_manager(self.toolmanager)
  380. if self.toolbar:
  381. backend_tools.add_tools_to_container(self.toolbar)
  382. self.statusbar = StatusbarTk(self.window, self.toolmanager)
  383. self._shown = False
  384. def _get_toolbar(self):
  385. if matplotlib.rcParams['toolbar'] == 'toolbar2':
  386. toolbar = NavigationToolbar2Tk(self.canvas, self.window)
  387. elif matplotlib.rcParams['toolbar'] == 'toolmanager':
  388. toolbar = ToolbarTk(self.toolmanager, self.window)
  389. else:
  390. toolbar = None
  391. return toolbar
  392. def _get_toolmanager(self):
  393. if rcParams['toolbar'] == 'toolmanager':
  394. toolmanager = ToolManager(self.canvas.figure)
  395. else:
  396. toolmanager = None
  397. return toolmanager
  398. def resize(self, width, height):
  399. self.canvas._tkcanvas.master.geometry("%dx%d" % (width, height))
  400. if self.toolbar is not None:
  401. self.toolbar.configure(width=width)
  402. def show(self):
  403. with _restore_foreground_window_at_end():
  404. if not self._shown:
  405. def destroy(*args):
  406. self.window = None
  407. Gcf.destroy(self.num)
  408. self.canvas._tkcanvas.bind("<Destroy>", destroy)
  409. self.window.deiconify()
  410. else:
  411. self.canvas.draw_idle()
  412. # Raise the new window.
  413. self.canvas.manager.window.attributes('-topmost', 1)
  414. self.canvas.manager.window.attributes('-topmost', 0)
  415. self._shown = True
  416. def destroy(self, *args):
  417. if self.window is not None:
  418. #self.toolbar.destroy()
  419. if self.canvas._idle_callback:
  420. self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback)
  421. self.window.destroy()
  422. if Gcf.get_num_fig_managers() == 0:
  423. if self.window is not None:
  424. self.window.quit()
  425. self.window = None
  426. def get_window_title(self):
  427. return self.window.wm_title()
  428. def set_window_title(self, title):
  429. self.window.wm_title(title)
  430. def full_screen_toggle(self):
  431. is_fullscreen = bool(self.window.attributes('-fullscreen'))
  432. self.window.attributes('-fullscreen', not is_fullscreen)
  433. class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame):
  434. """
  435. Attributes
  436. ----------
  437. canvas : `FigureCanvas`
  438. the figure canvas on which to operate
  439. win : tk.Window
  440. the tk.Window which owns this toolbar
  441. """
  442. def __init__(self, canvas, window):
  443. self.canvas = canvas
  444. # Avoid using self.window (prefer self.canvas.get_tk_widget().master),
  445. # so that Tool implementations can reuse the methods.
  446. self.window = window
  447. NavigationToolbar2.__init__(self, canvas)
  448. def destroy(self, *args):
  449. del self.message
  450. tk.Frame.destroy(self, *args)
  451. def set_message(self, s):
  452. self.message.set(s)
  453. def draw_rubberband(self, event, x0, y0, x1, y1):
  454. height = self.canvas.figure.bbox.height
  455. y0 = height - y0
  456. y1 = height - y1
  457. if hasattr(self, "lastrect"):
  458. self.canvas._tkcanvas.delete(self.lastrect)
  459. self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1)
  460. def release(self, event):
  461. if hasattr(self, "lastrect"):
  462. self.canvas._tkcanvas.delete(self.lastrect)
  463. del self.lastrect
  464. def set_cursor(self, cursor):
  465. window = self.canvas.get_tk_widget().master
  466. window.configure(cursor=cursord[cursor])
  467. window.update_idletasks()
  468. def _Button(self, text, file, command, extension='.gif'):
  469. img_file = str(cbook._get_data_path('images', file + extension))
  470. im = tk.PhotoImage(master=self, file=img_file)
  471. b = tk.Button(
  472. master=self, text=text, padx=2, pady=2, image=im, command=command)
  473. b._ntimage = im
  474. b.pack(side=tk.LEFT)
  475. return b
  476. def _Spacer(self):
  477. # Buttons are 30px high. Make this 26px tall +2px padding to center it.
  478. s = tk.Frame(
  479. master=self, height=26, relief=tk.RIDGE, pady=2, bg="DarkGray")
  480. s.pack(side=tk.LEFT, padx=5)
  481. return s
  482. def _init_toolbar(self):
  483. xmin, xmax = self.canvas.figure.bbox.intervalx
  484. height, width = 50, xmax-xmin
  485. tk.Frame.__init__(self, master=self.window,
  486. width=int(width), height=int(height),
  487. borderwidth=2)
  488. self.update() # Make axes menu
  489. for text, tooltip_text, image_file, callback in self.toolitems:
  490. if text is None:
  491. # Add a spacer; return value is unused.
  492. self._Spacer()
  493. else:
  494. button = self._Button(text=text, file=image_file,
  495. command=getattr(self, callback))
  496. if tooltip_text is not None:
  497. ToolTip.createToolTip(button, tooltip_text)
  498. self.message = tk.StringVar(master=self)
  499. self._message_label = tk.Label(master=self, textvariable=self.message)
  500. self._message_label.pack(side=tk.RIGHT)
  501. self.pack(side=tk.BOTTOM, fill=tk.X)
  502. def configure_subplots(self):
  503. toolfig = Figure(figsize=(6, 3))
  504. window = tk.Toplevel()
  505. canvas = type(self.canvas)(toolfig, master=window)
  506. toolfig.subplots_adjust(top=0.9)
  507. canvas.tool = SubplotTool(self.canvas.figure, toolfig)
  508. canvas.draw()
  509. canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
  510. window.grab_set()
  511. def save_figure(self, *args):
  512. filetypes = self.canvas.get_supported_filetypes().copy()
  513. default_filetype = self.canvas.get_default_filetype()
  514. # Tk doesn't provide a way to choose a default filetype,
  515. # so we just have to put it first
  516. default_filetype_name = filetypes.pop(default_filetype)
  517. sorted_filetypes = ([(default_filetype, default_filetype_name)]
  518. + sorted(filetypes.items()))
  519. tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes]
  520. # adding a default extension seems to break the
  521. # asksaveasfilename dialog when you choose various save types
  522. # from the dropdown. Passing in the empty string seems to
  523. # work - JDH!
  524. #defaultextension = self.canvas.get_default_filetype()
  525. defaultextension = ''
  526. initialdir = os.path.expanduser(rcParams['savefig.directory'])
  527. initialfile = self.canvas.get_default_filename()
  528. fname = tkinter.filedialog.asksaveasfilename(
  529. master=self.canvas.get_tk_widget().master,
  530. title='Save the figure',
  531. filetypes=tk_filetypes,
  532. defaultextension=defaultextension,
  533. initialdir=initialdir,
  534. initialfile=initialfile,
  535. )
  536. if fname in ["", ()]:
  537. return
  538. # Save dir for next time, unless empty str (i.e., use cwd).
  539. if initialdir != "":
  540. rcParams['savefig.directory'] = (
  541. os.path.dirname(str(fname)))
  542. try:
  543. # This method will handle the delegation to the correct type
  544. self.canvas.figure.savefig(fname)
  545. except Exception as e:
  546. tkinter.messagebox.showerror("Error saving file", str(e))
  547. @cbook.deprecated("3.1")
  548. def set_active(self, ind):
  549. self._ind = ind
  550. self._active = [self._axes[i] for i in self._ind]
  551. def update(self):
  552. self._axes = self.canvas.figure.axes
  553. with _restore_foreground_window_at_end():
  554. NavigationToolbar2.update(self)
  555. class ToolTip:
  556. """
  557. Tooltip recipe from
  558. http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387
  559. """
  560. @staticmethod
  561. def createToolTip(widget, text):
  562. toolTip = ToolTip(widget)
  563. def enter(event):
  564. toolTip.showtip(text)
  565. def leave(event):
  566. toolTip.hidetip()
  567. widget.bind('<Enter>', enter)
  568. widget.bind('<Leave>', leave)
  569. def __init__(self, widget):
  570. self.widget = widget
  571. self.tipwindow = None
  572. self.id = None
  573. self.x = self.y = 0
  574. def showtip(self, text):
  575. "Display text in tooltip window"
  576. self.text = text
  577. if self.tipwindow or not self.text:
  578. return
  579. x, y, _, _ = self.widget.bbox("insert")
  580. x = x + self.widget.winfo_rootx() + 27
  581. y = y + self.widget.winfo_rooty()
  582. self.tipwindow = tw = tk.Toplevel(self.widget)
  583. tw.wm_overrideredirect(1)
  584. tw.wm_geometry("+%d+%d" % (x, y))
  585. try:
  586. # For Mac OS
  587. tw.tk.call("::tk::unsupported::MacWindowStyle",
  588. "style", tw._w,
  589. "help", "noActivates")
  590. except tk.TclError:
  591. pass
  592. label = tk.Label(tw, text=self.text, justify=tk.LEFT,
  593. background="#ffffe0", relief=tk.SOLID, borderwidth=1)
  594. label.pack(ipadx=1)
  595. def hidetip(self):
  596. tw = self.tipwindow
  597. self.tipwindow = None
  598. if tw:
  599. tw.destroy()
  600. class RubberbandTk(backend_tools.RubberbandBase):
  601. def draw_rubberband(self, x0, y0, x1, y1):
  602. height = self.figure.canvas.figure.bbox.height
  603. y0 = height - y0
  604. y1 = height - y1
  605. if hasattr(self, "lastrect"):
  606. self.figure.canvas._tkcanvas.delete(self.lastrect)
  607. self.lastrect = self.figure.canvas._tkcanvas.create_rectangle(
  608. x0, y0, x1, y1)
  609. def remove_rubberband(self):
  610. if hasattr(self, "lastrect"):
  611. self.figure.canvas._tkcanvas.delete(self.lastrect)
  612. del self.lastrect
  613. class SetCursorTk(backend_tools.SetCursorBase):
  614. def set_cursor(self, cursor):
  615. NavigationToolbar2Tk.set_cursor(
  616. self._make_classic_style_pseudo_toolbar(), cursor)
  617. class ToolbarTk(ToolContainerBase, tk.Frame):
  618. _icon_extension = '.gif'
  619. def __init__(self, toolmanager, window):
  620. ToolContainerBase.__init__(self, toolmanager)
  621. xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx
  622. height, width = 50, xmax - xmin
  623. tk.Frame.__init__(self, master=window,
  624. width=int(width), height=int(height),
  625. borderwidth=2)
  626. self._toolitems = {}
  627. self.pack(side=tk.TOP, fill=tk.X)
  628. self._groups = {}
  629. def add_toolitem(
  630. self, name, group, position, image_file, description, toggle):
  631. frame = self._get_groupframe(group)
  632. button = self._Button(name, image_file, toggle, frame)
  633. if description is not None:
  634. ToolTip.createToolTip(button, description)
  635. self._toolitems.setdefault(name, [])
  636. self._toolitems[name].append(button)
  637. def _get_groupframe(self, group):
  638. if group not in self._groups:
  639. if self._groups:
  640. self._add_separator()
  641. frame = tk.Frame(master=self, borderwidth=0)
  642. frame.pack(side=tk.LEFT, fill=tk.Y)
  643. self._groups[group] = frame
  644. return self._groups[group]
  645. def _add_separator(self):
  646. separator = tk.Frame(master=self, bd=5, width=1, bg='black')
  647. separator.pack(side=tk.LEFT, fill=tk.Y, padx=2)
  648. def _Button(self, text, image_file, toggle, frame):
  649. if image_file is not None:
  650. im = tk.PhotoImage(master=self, file=image_file)
  651. else:
  652. im = None
  653. if not toggle:
  654. b = tk.Button(master=frame, text=text, padx=2, pady=2, image=im,
  655. command=lambda: self._button_click(text))
  656. else:
  657. # There is a bug in tkinter included in some python 3.6 versions
  658. # that without this variable, produces a "visual" toggling of
  659. # other near checkbuttons
  660. # https://bugs.python.org/issue29402
  661. # https://bugs.python.org/issue25684
  662. var = tk.IntVar()
  663. b = tk.Checkbutton(master=frame, text=text, padx=2, pady=2,
  664. image=im, indicatoron=False,
  665. command=lambda: self._button_click(text),
  666. variable=var)
  667. b._ntimage = im
  668. b.pack(side=tk.LEFT)
  669. return b
  670. def _button_click(self, name):
  671. self.trigger_tool(name)
  672. def toggle_toolitem(self, name, toggled):
  673. if name not in self._toolitems:
  674. return
  675. for toolitem in self._toolitems[name]:
  676. if toggled:
  677. toolitem.select()
  678. else:
  679. toolitem.deselect()
  680. def remove_toolitem(self, name):
  681. for toolitem in self._toolitems[name]:
  682. toolitem.pack_forget()
  683. del self._toolitems[name]
  684. class StatusbarTk(StatusbarBase, tk.Frame):
  685. def __init__(self, window, *args, **kwargs):
  686. StatusbarBase.__init__(self, *args, **kwargs)
  687. xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx
  688. height, width = 50, xmax - xmin
  689. tk.Frame.__init__(self, master=window,
  690. width=int(width), height=int(height),
  691. borderwidth=2)
  692. self._message = tk.StringVar(master=self)
  693. self._message_label = tk.Label(master=self, textvariable=self._message)
  694. self._message_label.pack(side=tk.RIGHT)
  695. self.pack(side=tk.TOP, fill=tk.X)
  696. def set_message(self, s):
  697. self._message.set(s)
  698. class SaveFigureTk(backend_tools.SaveFigureBase):
  699. def trigger(self, *args):
  700. NavigationToolbar2Tk.save_figure(
  701. self._make_classic_style_pseudo_toolbar())
  702. class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase):
  703. def __init__(self, *args, **kwargs):
  704. backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
  705. self.window = None
  706. def trigger(self, *args):
  707. self.init_window()
  708. self.window.lift()
  709. def init_window(self):
  710. if self.window:
  711. return
  712. toolfig = Figure(figsize=(6, 3))
  713. self.window = tk.Tk()
  714. canvas = type(self.canvas)(toolfig, master=self.window)
  715. toolfig.subplots_adjust(top=0.9)
  716. SubplotTool(self.figure, toolfig)
  717. canvas.draw()
  718. canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
  719. self.window.protocol("WM_DELETE_WINDOW", self.destroy)
  720. def destroy(self, *args, **kwargs):
  721. if self.window is not None:
  722. self.window.destroy()
  723. self.window = None
  724. class HelpTk(backend_tools.ToolHelpBase):
  725. def trigger(self, *args):
  726. dialog = SimpleDialog(
  727. self.figure.canvas._tkcanvas, self._get_help_text(), ["OK"])
  728. dialog.done = lambda num: dialog.frame.master.withdraw()
  729. backend_tools.ToolSaveFigure = SaveFigureTk
  730. backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk
  731. backend_tools.ToolSetCursor = SetCursorTk
  732. backend_tools.ToolRubberband = RubberbandTk
  733. backend_tools.ToolHelp = HelpTk
  734. backend_tools.ToolCopyToClipboard = backend_tools.ToolCopyToClipboardBase
  735. Toolbar = ToolbarTk
  736. @_Backend.export
  737. class _BackendTk(_Backend):
  738. FigureManager = FigureManagerTk
  739. @classmethod
  740. def new_figure_manager_given_figure(cls, num, figure):
  741. """
  742. Create a new figure manager instance for the given figure.
  743. """
  744. with _restore_foreground_window_at_end():
  745. window = tk.Tk(className="matplotlib")
  746. window.withdraw()
  747. # Put a Matplotlib icon on the window rather than the default tk
  748. # icon. Tkinter doesn't allow colour icons on linux systems, but
  749. # tk>=8.5 has a iconphoto command which we call directly. See
  750. # http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html
  751. icon_fname = str(cbook._get_data_path(
  752. 'images/matplotlib_128.ppm'))
  753. icon_img = tk.PhotoImage(file=icon_fname, master=window)
  754. try:
  755. window.iconphoto(False, icon_img)
  756. except Exception as exc:
  757. # log the failure (due e.g. to Tk version), but carry on
  758. _log.info('Could not load matplotlib icon: %s', exc)
  759. canvas = cls.FigureCanvas(figure, master=window)
  760. manager = cls.FigureManager(canvas, num, window)
  761. if matplotlib.is_interactive():
  762. manager.show()
  763. canvas.draw_idle()
  764. return manager
  765. @staticmethod
  766. def trigger_manager_draw(manager):
  767. manager.show()
  768. @staticmethod
  769. def mainloop():
  770. managers = Gcf.get_all_fig_managers()
  771. if managers:
  772. managers[0].window.mainloop()