1from __future__ import (absolute_import, division, print_function, 2 unicode_literals) 3 4import six 5from six.moves import tkinter as Tk 6 7import logging 8import os.path 9import sys 10 11# Paint image to Tk photo blitter extension 12import matplotlib.backends.tkagg as tkagg 13 14from matplotlib.backends.backend_agg import FigureCanvasAgg 15import matplotlib.backends.windowing as windowing 16 17import matplotlib 18from matplotlib import backend_tools, cbook, rcParams 19from matplotlib.backend_bases import ( 20 _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, 21 StatusbarBase, TimerBase, ToolContainerBase, cursors) 22from matplotlib.backend_managers import ToolManager 23from matplotlib._pylab_helpers import Gcf 24from matplotlib.figure import Figure 25from matplotlib.widgets import SubplotTool 26 27 28_log = logging.getLogger(__name__) 29 30backend_version = Tk.TkVersion 31 32# the true dots per inch on the screen; should be display dependent 33# see http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5 for some info about screen dpi 34PIXELS_PER_INCH = 75 35 36cursord = { 37 cursors.MOVE: "fleur", 38 cursors.HAND: "hand2", 39 cursors.POINTER: "arrow", 40 cursors.SELECT_REGION: "tcross", 41 cursors.WAIT: "watch", 42 } 43 44 45def raise_msg_to_str(msg): 46 """msg is a return arg from a raise. Join with new lines""" 47 if not isinstance(msg, six.string_types): 48 msg = '\n'.join(map(str, msg)) 49 return msg 50 51def error_msg_tkpaint(msg, parent=None): 52 from six.moves import tkinter_messagebox as tkMessageBox 53 tkMessageBox.showerror("matplotlib", msg) 54 55 56class TimerTk(TimerBase): 57 ''' 58 Subclass of :class:`backend_bases.TimerBase` that uses Tk's timer events. 59 60 Attributes 61 ---------- 62 interval : int 63 The time between timer events in milliseconds. Default is 1000 ms. 64 single_shot : bool 65 Boolean flag indicating whether this timer should operate as single 66 shot (run once and then stop). Defaults to False. 67 callbacks : list 68 Stores list of (func, args) tuples that will be called upon timer 69 events. This list can be manipulated directly, or the functions 70 `add_callback` and `remove_callback` can be used. 71 72 ''' 73 def __init__(self, parent, *args, **kwargs): 74 TimerBase.__init__(self, *args, **kwargs) 75 self.parent = parent 76 self._timer = None 77 78 def _timer_start(self): 79 self._timer_stop() 80 self._timer = self.parent.after(self._interval, self._on_timer) 81 82 def _timer_stop(self): 83 if self._timer is not None: 84 self.parent.after_cancel(self._timer) 85 self._timer = None 86 87 def _on_timer(self): 88 TimerBase._on_timer(self) 89 90 # Tk after() is only a single shot, so we need to add code here to 91 # reset the timer if we're not operating in single shot mode. However, 92 # if _timer is None, this means that _timer_stop has been called; so 93 # don't recreate the timer in that case. 94 if not self._single and self._timer: 95 self._timer = self.parent.after(self._interval, self._on_timer) 96 else: 97 self._timer = None 98 99 100class FigureCanvasTk(FigureCanvasBase): 101 keyvald = {65507 : 'control', 102 65505 : 'shift', 103 65513 : 'alt', 104 65515 : 'super', 105 65508 : 'control', 106 65506 : 'shift', 107 65514 : 'alt', 108 65361 : 'left', 109 65362 : 'up', 110 65363 : 'right', 111 65364 : 'down', 112 65307 : 'escape', 113 65470 : 'f1', 114 65471 : 'f2', 115 65472 : 'f3', 116 65473 : 'f4', 117 65474 : 'f5', 118 65475 : 'f6', 119 65476 : 'f7', 120 65477 : 'f8', 121 65478 : 'f9', 122 65479 : 'f10', 123 65480 : 'f11', 124 65481 : 'f12', 125 65300 : 'scroll_lock', 126 65299 : 'break', 127 65288 : 'backspace', 128 65293 : 'enter', 129 65379 : 'insert', 130 65535 : 'delete', 131 65360 : 'home', 132 65367 : 'end', 133 65365 : 'pageup', 134 65366 : 'pagedown', 135 65438 : '0', 136 65436 : '1', 137 65433 : '2', 138 65435 : '3', 139 65430 : '4', 140 65437 : '5', 141 65432 : '6', 142 65429 : '7', 143 65431 : '8', 144 65434 : '9', 145 65451 : '+', 146 65453 : '-', 147 65450 : '*', 148 65455 : '/', 149 65439 : 'dec', 150 65421 : 'enter', 151 } 152 153 _keycode_lookup = { 154 262145: 'control', 155 524320: 'alt', 156 524352: 'alt', 157 1048584: 'super', 158 1048592: 'super', 159 131074: 'shift', 160 131076: 'shift', 161 } 162 """_keycode_lookup is used for badly mapped (i.e. no event.key_sym set) 163 keys on apple keyboards.""" 164 165 def __init__(self, figure, master=None, resize_callback=None): 166 super(FigureCanvasTk, self).__init__(figure) 167 self._idle = True 168 self._idle_callback = None 169 t1,t2,w,h = self.figure.bbox.bounds 170 w, h = int(w), int(h) 171 self._tkcanvas = Tk.Canvas( 172 master=master, background="white", 173 width=w, height=h, borderwidth=0, highlightthickness=0) 174 self._tkphoto = Tk.PhotoImage( 175 master=self._tkcanvas, width=w, height=h) 176 self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) 177 self._resize_callback = resize_callback 178 self._tkcanvas.bind("<Configure>", self.resize) 179 self._tkcanvas.bind("<Key>", self.key_press) 180 self._tkcanvas.bind("<Motion>", self.motion_notify_event) 181 self._tkcanvas.bind("<KeyRelease>", self.key_release) 182 for name in "<Button-1>", "<Button-2>", "<Button-3>": 183 self._tkcanvas.bind(name, self.button_press_event) 184 for name in "<Double-Button-1>", "<Double-Button-2>", "<Double-Button-3>": 185 self._tkcanvas.bind(name, self.button_dblclick_event) 186 for name in "<ButtonRelease-1>", "<ButtonRelease-2>", "<ButtonRelease-3>": 187 self._tkcanvas.bind(name, self.button_release_event) 188 189 # Mouse wheel on Linux generates button 4/5 events 190 for name in "<Button-4>", "<Button-5>": 191 self._tkcanvas.bind(name, self.scroll_event) 192 # Mouse wheel for windows goes to the window with the focus. 193 # Since the canvas won't usually have the focus, bind the 194 # event to the window containing the canvas instead. 195 # See http://wiki.tcl.tk/3893 (mousewheel) for details 196 root = self._tkcanvas.winfo_toplevel() 197 root.bind("<MouseWheel>", self.scroll_event_windows, "+") 198 199 # Can't get destroy events by binding to _tkcanvas. Therefore, bind 200 # to the window and filter. 201 def filter_destroy(evt): 202 if evt.widget is self._tkcanvas: 203 self._master.update_idletasks() 204 self.close_event() 205 root.bind("<Destroy>", filter_destroy, "+") 206 207 self._master = master 208 self._tkcanvas.focus_set() 209 210 def resize(self, event): 211 width, height = event.width, event.height 212 if self._resize_callback is not None: 213 self._resize_callback(event) 214 215 # compute desired figure size in inches 216 dpival = self.figure.dpi 217 winch = width/dpival 218 hinch = height/dpival 219 self.figure.set_size_inches(winch, hinch, forward=False) 220 221 222 self._tkcanvas.delete(self._tkphoto) 223 self._tkphoto = Tk.PhotoImage( 224 master=self._tkcanvas, width=int(width), height=int(height)) 225 self._tkcanvas.create_image(int(width/2),int(height/2),image=self._tkphoto) 226 self.resize_event() 227 self.draw() 228 229 # a resizing will in general move the pointer position 230 # relative to the canvas, so process it as a motion notify 231 # event. An intended side effect of this call is to allow 232 # window raises (which trigger a resize) to get the cursor 233 # position to the mpl event framework so key presses which are 234 # over the axes will work w/o clicks or explicit motion 235 self._update_pointer_position(event) 236 237 def _update_pointer_position(self, guiEvent=None): 238 """ 239 Figure out if we are inside the canvas or not and update the 240 canvas enter/leave events 241 """ 242 # if the pointer if over the canvas, set the lastx and lasty 243 # attrs of the canvas so it can process event w/o mouse click 244 # or move 245 246 # the window's upper, left coords in screen coords 247 xw = self._tkcanvas.winfo_rootx() 248 yw = self._tkcanvas.winfo_rooty() 249 # the pointer's location in screen coords 250 xp, yp = self._tkcanvas.winfo_pointerxy() 251 252 # not figure out the canvas coordinates of the pointer 253 xc = xp - xw 254 yc = yp - yw 255 256 # flip top/bottom 257 yc = self.figure.bbox.height - yc 258 259 # JDH: this method was written originally to get the pointer 260 # location to the backend lastx and lasty attrs so that events 261 # like KeyEvent can be handled without mouse events. e.g., if 262 # the cursor is already above the axes, then key presses like 263 # 'g' should toggle the grid. In order for this to work in 264 # backend_bases, the canvas needs to know _lastx and _lasty. 265 # There are three ways to get this info the canvas: 266 # 267 # 1) set it explicitly 268 # 269 # 2) call enter/leave events explicitly. The downside of this 270 # in the impl below is that enter could be repeatedly 271 # triggered if the mouse is over the axes and one is 272 # resizing with the keyboard. This is not entirely bad, 273 # because the mouse position relative to the canvas is 274 # changing, but it may be surprising to get repeated entries 275 # without leaves 276 # 277 # 3) process it as a motion notify event. This also has pros 278 # and cons. The mouse is moving relative to the window, but 279 # this may surpise an event handler writer who is getting 280 # motion_notify_events even if the mouse has not moved 281 282 # here are the three scenarios 283 if 1: 284 # just manually set it 285 self._lastx, self._lasty = xc, yc 286 elif 0: 287 # alternate implementation: process it as a motion 288 FigureCanvasBase.motion_notify_event(self, xc, yc, guiEvent) 289 elif 0: 290 # alternate implementation -- process enter/leave events 291 # instead of motion/notify 292 if self.figure.bbox.contains(xc, yc): 293 self.enter_notify_event(guiEvent, xy=(xc,yc)) 294 else: 295 self.leave_notify_event(guiEvent) 296 297 show = cbook.deprecated("2.2", name="FigureCanvasTk.show", 298 alternative="FigureCanvasTk.draw")( 299 lambda self: self.draw()) 300 301 def draw_idle(self): 302 'update drawing area only if idle' 303 if self._idle is False: 304 return 305 306 self._idle = False 307 308 def idle_draw(*args): 309 try: 310 self.draw() 311 finally: 312 self._idle = True 313 314 self._idle_callback = self._tkcanvas.after_idle(idle_draw) 315 316 def get_tk_widget(self): 317 """returns the Tk widget used to implement FigureCanvasTkAgg. 318 Although the initial implementation uses a Tk canvas, this routine 319 is intended to hide that fact. 320 """ 321 return self._tkcanvas 322 323 def motion_notify_event(self, event): 324 x = event.x 325 # flipy so y=0 is bottom of canvas 326 y = self.figure.bbox.height - event.y 327 FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) 328 329 330 def button_press_event(self, event, dblclick=False): 331 x = event.x 332 # flipy so y=0 is bottom of canvas 333 y = self.figure.bbox.height - event.y 334 num = getattr(event, 'num', None) 335 336 if sys.platform=='darwin': 337 # 2 and 3 were reversed on the OSX platform I 338 # tested under tkagg 339 if num==2: num=3 340 elif num==3: num=2 341 342 FigureCanvasBase.button_press_event(self, x, y, num, dblclick=dblclick, guiEvent=event) 343 344 def button_dblclick_event(self,event): 345 self.button_press_event(event,dblclick=True) 346 347 def button_release_event(self, event): 348 x = event.x 349 # flipy so y=0 is bottom of canvas 350 y = self.figure.bbox.height - event.y 351 352 num = getattr(event, 'num', None) 353 354 if sys.platform=='darwin': 355 # 2 and 3 were reversed on the OSX platform I 356 # tested under tkagg 357 if num==2: num=3 358 elif num==3: num=2 359 360 FigureCanvasBase.button_release_event(self, x, y, num, guiEvent=event) 361 362 def scroll_event(self, event): 363 x = event.x 364 y = self.figure.bbox.height - event.y 365 num = getattr(event, 'num', None) 366 if num==4: step = +1 367 elif num==5: step = -1 368 else: step = 0 369 370 FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) 371 372 def scroll_event_windows(self, event): 373 """MouseWheel event processor""" 374 # need to find the window that contains the mouse 375 w = event.widget.winfo_containing(event.x_root, event.y_root) 376 if w == self._tkcanvas: 377 x = event.x_root - w.winfo_rootx() 378 y = event.y_root - w.winfo_rooty() 379 y = self.figure.bbox.height - y 380 step = event.delta/120. 381 FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) 382 383 def _get_key(self, event): 384 val = event.keysym_num 385 if val in self.keyvald: 386 key = self.keyvald[val] 387 elif val == 0 and sys.platform == 'darwin' and \ 388 event.keycode in self._keycode_lookup: 389 key = self._keycode_lookup[event.keycode] 390 elif val < 256: 391 key = chr(val) 392 else: 393 key = None 394 395 # add modifier keys to the key string. Bit details originate from 396 # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm 397 # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; 398 # BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080; 399 # BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400; 400 # In general, the modifier key is excluded from the modifier flag, 401 # however this is not the case on "darwin", so double check that 402 # we aren't adding repeat modifier flags to a modifier key. 403 if sys.platform == 'win32': 404 modifiers = [(17, 'alt', 'alt'), 405 (2, 'ctrl', 'control'), 406 ] 407 elif sys.platform == 'darwin': 408 modifiers = [(3, 'super', 'super'), 409 (4, 'alt', 'alt'), 410 (2, 'ctrl', 'control'), 411 ] 412 else: 413 modifiers = [(6, 'super', 'super'), 414 (3, 'alt', 'alt'), 415 (2, 'ctrl', 'control'), 416 ] 417 418 if key is not None: 419 # note, shift is not added to the keys as this is already accounted for 420 for bitmask, prefix, key_name in modifiers: 421 if event.state & (1 << bitmask) and key_name not in key: 422 key = '{0}+{1}'.format(prefix, key) 423 424 return key 425 426 def key_press(self, event): 427 key = self._get_key(event) 428 FigureCanvasBase.key_press_event(self, key, guiEvent=event) 429 430 def key_release(self, event): 431 key = self._get_key(event) 432 FigureCanvasBase.key_release_event(self, key, guiEvent=event) 433 434 def new_timer(self, *args, **kwargs): 435 """ 436 Creates a new backend-specific subclass of :class:`backend_bases.Timer`. 437 This is useful for getting periodic events through the backend's native 438 event loop. Implemented only for backends with GUIs. 439 440 Other Parameters 441 ---------------- 442 interval : scalar 443 Timer interval in milliseconds 444 callbacks : list 445 Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` 446 will be executed by the timer every *interval*. 447 448 """ 449 return TimerTk(self._tkcanvas, *args, **kwargs) 450 451 def flush_events(self): 452 self._master.update() 453 454 455class FigureManagerTk(FigureManagerBase): 456 """ 457 Attributes 458 ---------- 459 canvas : `FigureCanvas` 460 The FigureCanvas instance 461 num : int or str 462 The Figure number 463 toolbar : tk.Toolbar 464 The tk.Toolbar 465 window : tk.Window 466 The tk.Window 467 468 """ 469 def __init__(self, canvas, num, window): 470 FigureManagerBase.__init__(self, canvas, num) 471 self.window = window 472 self.window.withdraw() 473 self.set_window_title("Figure %d" % num) 474 self.canvas = canvas 475 # If using toolmanager it has to be present when initializing the toolbar 476 self.toolmanager = self._get_toolmanager() 477 # packing toolbar first, because if space is getting low, last packed widget is getting shrunk first (-> the canvas) 478 self.toolbar = self._get_toolbar() 479 self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) 480 self._num = num 481 482 self.statusbar = None 483 484 if self.toolmanager: 485 backend_tools.add_tools_to_manager(self.toolmanager) 486 if self.toolbar: 487 backend_tools.add_tools_to_container(self.toolbar) 488 self.statusbar = StatusbarTk(self.window, self.toolmanager) 489 490 self._shown = False 491 492 def notify_axes_change(fig): 493 'this will be called whenever the current axes is changed' 494 if self.toolmanager is not None: 495 pass 496 elif self.toolbar is not None: 497 self.toolbar.update() 498 self.canvas.figure.add_axobserver(notify_axes_change) 499 500 def _get_toolbar(self): 501 if matplotlib.rcParams['toolbar'] == 'toolbar2': 502 toolbar = NavigationToolbar2Tk(self.canvas, self.window) 503 elif matplotlib.rcParams['toolbar'] == 'toolmanager': 504 toolbar = ToolbarTk(self.toolmanager, self.window) 505 else: 506 toolbar = None 507 return toolbar 508 509 def _get_toolmanager(self): 510 if rcParams['toolbar'] == 'toolmanager': 511 toolmanager = ToolManager(self.canvas.figure) 512 else: 513 toolmanager = None 514 return toolmanager 515 516 def resize(self, width, height=None): 517 # before 09-12-22, the resize method takes a single *event* 518 # parameter. On the other hand, the resize method of other 519 # FigureManager class takes *width* and *height* parameter, 520 # which is used to change the size of the window. For the 521 # Figure.set_size_inches with forward=True work with Tk 522 # backend, I changed the function signature but tried to keep 523 # it backward compatible. -JJL 524 525 # when a single parameter is given, consider it as a event 526 if height is None: 527 cbook.warn_deprecated("2.2", "FigureManagerTkAgg.resize now takes " 528 "width and height as separate arguments") 529 width = width.width 530 else: 531 self.canvas._tkcanvas.master.geometry("%dx%d" % (width, height)) 532 533 if self.toolbar is not None: 534 self.toolbar.configure(width=width) 535 536 def show(self): 537 """ 538 this function doesn't segfault but causes the 539 PyEval_RestoreThread: NULL state bug on win32 540 """ 541 _focus = windowing.FocusManager() 542 if not self._shown: 543 def destroy(*args): 544 self.window = None 545 Gcf.destroy(self._num) 546 self.canvas._tkcanvas.bind("<Destroy>", destroy) 547 self.window.deiconify() 548 else: 549 self.canvas.draw_idle() 550 # Raise the new window. 551 self.canvas.manager.window.attributes('-topmost', 1) 552 self.canvas.manager.window.attributes('-topmost', 0) 553 self._shown = True 554 555 def destroy(self, *args): 556 if self.window is not None: 557 #self.toolbar.destroy() 558 if self.canvas._idle_callback: 559 self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) 560 self.window.destroy() 561 if Gcf.get_num_fig_managers()==0: 562 if self.window is not None: 563 self.window.quit() 564 self.window = None 565 566 def get_window_title(self): 567 return self.window.wm_title() 568 569 def set_window_title(self, title): 570 self.window.wm_title(title) 571 572 def full_screen_toggle(self): 573 is_fullscreen = bool(self.window.attributes('-fullscreen')) 574 self.window.attributes('-fullscreen', not is_fullscreen) 575 576 577@cbook.deprecated("2.2") 578class AxisMenu(object): 579 def __init__(self, master, naxes): 580 self._master = master 581 self._naxes = naxes 582 self._mbar = Tk.Frame(master=master, relief=Tk.RAISED, borderwidth=2) 583 self._mbar.pack(side=Tk.LEFT) 584 self._mbutton = Tk.Menubutton( 585 master=self._mbar, text="Axes", underline=0) 586 self._mbutton.pack(side=Tk.LEFT, padx="2m") 587 self._mbutton.menu = Tk.Menu(self._mbutton) 588 self._mbutton.menu.add_command( 589 label="Select All", command=self.select_all) 590 self._mbutton.menu.add_command( 591 label="Invert All", command=self.invert_all) 592 self._axis_var = [] 593 self._checkbutton = [] 594 for i in range(naxes): 595 self._axis_var.append(Tk.IntVar()) 596 self._axis_var[i].set(1) 597 self._checkbutton.append(self._mbutton.menu.add_checkbutton( 598 label = "Axis %d" % (i+1), 599 variable=self._axis_var[i], 600 command=self.set_active)) 601 self._mbutton.menu.invoke(self._mbutton.menu.index("Select All")) 602 self._mbutton['menu'] = self._mbutton.menu 603 self._mbar.tk_menuBar(self._mbutton) 604 self.set_active() 605 606 def adjust(self, naxes): 607 if self._naxes < naxes: 608 for i in range(self._naxes, naxes): 609 self._axis_var.append(Tk.IntVar()) 610 self._axis_var[i].set(1) 611 self._checkbutton.append( self._mbutton.menu.add_checkbutton( 612 label = "Axis %d" % (i+1), 613 variable=self._axis_var[i], 614 command=self.set_active)) 615 elif self._naxes > naxes: 616 for i in range(self._naxes-1, naxes-1, -1): 617 del self._axis_var[i] 618 self._mbutton.menu.forget(self._checkbutton[i]) 619 del self._checkbutton[i] 620 self._naxes = naxes 621 self.set_active() 622 623 def get_indices(self): 624 a = [i for i in range(len(self._axis_var)) if self._axis_var[i].get()] 625 return a 626 627 def set_active(self): 628 self._master.set_active(self.get_indices()) 629 630 def invert_all(self): 631 for a in self._axis_var: 632 a.set(not a.get()) 633 self.set_active() 634 635 def select_all(self): 636 for a in self._axis_var: 637 a.set(1) 638 self.set_active() 639 640 641class NavigationToolbar2Tk(NavigationToolbar2, Tk.Frame): 642 """ 643 Attributes 644 ---------- 645 canvas : `FigureCanvas` 646 the figure canvas on which to operate 647 win : tk.Window 648 the tk.Window which owns this toolbar 649 650 """ 651 def __init__(self, canvas, window): 652 self.canvas = canvas 653 self.window = window 654 NavigationToolbar2.__init__(self, canvas) 655 656 def destroy(self, *args): 657 del self.message 658 Tk.Frame.destroy(self, *args) 659 660 def set_message(self, s): 661 self.message.set(s) 662 663 def draw_rubberband(self, event, x0, y0, x1, y1): 664 height = self.canvas.figure.bbox.height 665 y0 = height - y0 666 y1 = height - y1 667 if hasattr(self, "lastrect"): 668 self.canvas._tkcanvas.delete(self.lastrect) 669 self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) 670 671 #self.canvas.draw() 672 673 def release(self, event): 674 try: self.lastrect 675 except AttributeError: pass 676 else: 677 self.canvas._tkcanvas.delete(self.lastrect) 678 del self.lastrect 679 680 def set_cursor(self, cursor): 681 self.window.configure(cursor=cursord[cursor]) 682 self.window.update_idletasks() 683 684 def _Button(self, text, file, command, extension='.gif'): 685 img_file = os.path.join( 686 rcParams['datapath'], 'images', file + extension) 687 im = Tk.PhotoImage(master=self, file=img_file) 688 b = Tk.Button( 689 master=self, text=text, padx=2, pady=2, image=im, command=command) 690 b._ntimage = im 691 b.pack(side=Tk.LEFT) 692 return b 693 694 def _Spacer(self): 695 # Buttons are 30px high, so make this 26px tall with padding to center it 696 s = Tk.Frame( 697 master=self, height=26, relief=Tk.RIDGE, pady=2, bg="DarkGray") 698 s.pack(side=Tk.LEFT, padx=5) 699 return s 700 701 def _init_toolbar(self): 702 xmin, xmax = self.canvas.figure.bbox.intervalx 703 height, width = 50, xmax-xmin 704 Tk.Frame.__init__(self, master=self.window, 705 width=int(width), height=int(height), 706 borderwidth=2) 707 708 self.update() # Make axes menu 709 710 for text, tooltip_text, image_file, callback in self.toolitems: 711 if text is None: 712 # Add a spacer; return value is unused. 713 self._Spacer() 714 else: 715 button = self._Button(text=text, file=image_file, 716 command=getattr(self, callback)) 717 if tooltip_text is not None: 718 ToolTip.createToolTip(button, tooltip_text) 719 720 self.message = Tk.StringVar(master=self) 721 self._message_label = Tk.Label(master=self, textvariable=self.message) 722 self._message_label.pack(side=Tk.RIGHT) 723 self.pack(side=Tk.BOTTOM, fill=Tk.X) 724 725 def configure_subplots(self): 726 toolfig = Figure(figsize=(6,3)) 727 window = Tk.Toplevel() 728 canvas = type(self.canvas)(toolfig, master=window) 729 toolfig.subplots_adjust(top=0.9) 730 canvas.tool = SubplotTool(self.canvas.figure, toolfig) 731 canvas.draw() 732 canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) 733 window.grab_set() 734 735 def save_figure(self, *args): 736 from six.moves import tkinter_tkfiledialog, tkinter_messagebox 737 filetypes = self.canvas.get_supported_filetypes().copy() 738 default_filetype = self.canvas.get_default_filetype() 739 740 # Tk doesn't provide a way to choose a default filetype, 741 # so we just have to put it first 742 default_filetype_name = filetypes.pop(default_filetype) 743 sorted_filetypes = ([(default_filetype, default_filetype_name)] 744 + sorted(six.iteritems(filetypes))) 745 tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] 746 747 # adding a default extension seems to break the 748 # asksaveasfilename dialog when you choose various save types 749 # from the dropdown. Passing in the empty string seems to 750 # work - JDH! 751 #defaultextension = self.canvas.get_default_filetype() 752 defaultextension = '' 753 initialdir = os.path.expanduser(rcParams['savefig.directory']) 754 initialfile = self.canvas.get_default_filename() 755 fname = tkinter_tkfiledialog.asksaveasfilename( 756 master=self.window, 757 title='Save the figure', 758 filetypes=tk_filetypes, 759 defaultextension=defaultextension, 760 initialdir=initialdir, 761 initialfile=initialfile, 762 ) 763 764 if fname in ["", ()]: 765 return 766 # Save dir for next time, unless empty str (i.e., use cwd). 767 if initialdir != "": 768 rcParams['savefig.directory'] = ( 769 os.path.dirname(six.text_type(fname))) 770 try: 771 # This method will handle the delegation to the correct type 772 self.canvas.figure.savefig(fname) 773 except Exception as e: 774 tkinter_messagebox.showerror("Error saving file", str(e)) 775 776 def set_active(self, ind): 777 self._ind = ind 778 self._active = [self._axes[i] for i in self._ind] 779 780 def update(self): 781 _focus = windowing.FocusManager() 782 self._axes = self.canvas.figure.axes 783 NavigationToolbar2.update(self) 784 785 786class ToolTip(object): 787 """ 788 Tooltip recipe from 789 http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387 790 """ 791 @staticmethod 792 def createToolTip(widget, text): 793 toolTip = ToolTip(widget) 794 def enter(event): 795 toolTip.showtip(text) 796 def leave(event): 797 toolTip.hidetip() 798 widget.bind('<Enter>', enter) 799 widget.bind('<Leave>', leave) 800 801 def __init__(self, widget): 802 self.widget = widget 803 self.tipwindow = None 804 self.id = None 805 self.x = self.y = 0 806 807 def showtip(self, text): 808 "Display text in tooltip window" 809 self.text = text 810 if self.tipwindow or not self.text: 811 return 812 x, y, _, _ = self.widget.bbox("insert") 813 x = x + self.widget.winfo_rootx() + 27 814 y = y + self.widget.winfo_rooty() 815 self.tipwindow = tw = Tk.Toplevel(self.widget) 816 tw.wm_overrideredirect(1) 817 tw.wm_geometry("+%d+%d" % (x, y)) 818 try: 819 # For Mac OS 820 tw.tk.call("::tk::unsupported::MacWindowStyle", 821 "style", tw._w, 822 "help", "noActivates") 823 except Tk.TclError: 824 pass 825 label = Tk.Label(tw, text=self.text, justify=Tk.LEFT, 826 background="#ffffe0", relief=Tk.SOLID, borderwidth=1) 827 label.pack(ipadx=1) 828 829 def hidetip(self): 830 tw = self.tipwindow 831 self.tipwindow = None 832 if tw: 833 tw.destroy() 834 835 836class RubberbandTk(backend_tools.RubberbandBase): 837 def __init__(self, *args, **kwargs): 838 backend_tools.RubberbandBase.__init__(self, *args, **kwargs) 839 840 def draw_rubberband(self, x0, y0, x1, y1): 841 height = self.figure.canvas.figure.bbox.height 842 y0 = height - y0 843 y1 = height - y1 844 if hasattr(self, "lastrect"): 845 self.figure.canvas._tkcanvas.delete(self.lastrect) 846 self.lastrect = self.figure.canvas._tkcanvas.create_rectangle( 847 x0, y0, x1, y1) 848 849 def remove_rubberband(self): 850 if hasattr(self, "lastrect"): 851 self.figure.canvas._tkcanvas.delete(self.lastrect) 852 del self.lastrect 853 854 855class SetCursorTk(backend_tools.SetCursorBase): 856 def set_cursor(self, cursor): 857 self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) 858 859 860class ToolbarTk(ToolContainerBase, Tk.Frame): 861 _icon_extension = '.gif' 862 def __init__(self, toolmanager, window): 863 ToolContainerBase.__init__(self, toolmanager) 864 xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx 865 height, width = 50, xmax - xmin 866 Tk.Frame.__init__(self, master=window, 867 width=int(width), height=int(height), 868 borderwidth=2) 869 self._toolitems = {} 870 self.pack(side=Tk.TOP, fill=Tk.X) 871 self._groups = {} 872 873 def add_toolitem( 874 self, name, group, position, image_file, description, toggle): 875 frame = self._get_groupframe(group) 876 button = self._Button(name, image_file, toggle, frame) 877 if description is not None: 878 ToolTip.createToolTip(button, description) 879 self._toolitems.setdefault(name, []) 880 self._toolitems[name].append(button) 881 882 def _get_groupframe(self, group): 883 if group not in self._groups: 884 if self._groups: 885 self._add_separator() 886 frame = Tk.Frame(master=self, borderwidth=0) 887 frame.pack(side=Tk.LEFT, fill=Tk.Y) 888 self._groups[group] = frame 889 return self._groups[group] 890 891 def _add_separator(self): 892 separator = Tk.Frame(master=self, bd=5, width=1, bg='black') 893 separator.pack(side=Tk.LEFT, fill=Tk.Y, padx=2) 894 895 def _Button(self, text, image_file, toggle, frame): 896 if image_file is not None: 897 im = Tk.PhotoImage(master=self, file=image_file) 898 else: 899 im = None 900 901 if not toggle: 902 b = Tk.Button(master=frame, text=text, padx=2, pady=2, image=im, 903 command=lambda: self._button_click(text)) 904 else: 905 # There is a bug in tkinter included in some python 3.6 versions 906 # that without this variable, produces a "visual" toggling of 907 # other near checkbuttons 908 # https://bugs.python.org/issue29402 909 # https://bugs.python.org/issue25684 910 var = Tk.IntVar() 911 b = Tk.Checkbutton(master=frame, text=text, padx=2, pady=2, 912 image=im, indicatoron=False, 913 command=lambda: self._button_click(text), 914 variable=var) 915 b._ntimage = im 916 b.pack(side=Tk.LEFT) 917 return b 918 919 def _button_click(self, name): 920 self.trigger_tool(name) 921 922 def toggle_toolitem(self, name, toggled): 923 if name not in self._toolitems: 924 return 925 for toolitem in self._toolitems[name]: 926 if toggled: 927 toolitem.select() 928 else: 929 toolitem.deselect() 930 931 def remove_toolitem(self, name): 932 for toolitem in self._toolitems[name]: 933 toolitem.pack_forget() 934 del self._toolitems[name] 935 936 937class StatusbarTk(StatusbarBase, Tk.Frame): 938 def __init__(self, window, *args, **kwargs): 939 StatusbarBase.__init__(self, *args, **kwargs) 940 xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx 941 height, width = 50, xmax - xmin 942 Tk.Frame.__init__(self, master=window, 943 width=int(width), height=int(height), 944 borderwidth=2) 945 self._message = Tk.StringVar(master=self) 946 self._message_label = Tk.Label(master=self, textvariable=self._message) 947 self._message_label.pack(side=Tk.RIGHT) 948 self.pack(side=Tk.TOP, fill=Tk.X) 949 950 def set_message(self, s): 951 self._message.set(s) 952 953 954class SaveFigureTk(backend_tools.SaveFigureBase): 955 def trigger(self, *args): 956 from six.moves import tkinter_tkfiledialog, tkinter_messagebox 957 filetypes = self.figure.canvas.get_supported_filetypes().copy() 958 default_filetype = self.figure.canvas.get_default_filetype() 959 960 # Tk doesn't provide a way to choose a default filetype, 961 # so we just have to put it first 962 default_filetype_name = filetypes.pop(default_filetype) 963 sorted_filetypes = ([(default_filetype, default_filetype_name)] 964 + sorted(six.iteritems(filetypes))) 965 tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] 966 967 # adding a default extension seems to break the 968 # asksaveasfilename dialog when you choose various save types 969 # from the dropdown. Passing in the empty string seems to 970 # work - JDH! 971 # defaultextension = self.figure.canvas.get_default_filetype() 972 defaultextension = '' 973 initialdir = os.path.expanduser(rcParams['savefig.directory']) 974 initialfile = self.figure.canvas.get_default_filename() 975 fname = tkinter_tkfiledialog.asksaveasfilename( 976 master=self.figure.canvas.manager.window, 977 title='Save the figure', 978 filetypes=tk_filetypes, 979 defaultextension=defaultextension, 980 initialdir=initialdir, 981 initialfile=initialfile, 982 ) 983 984 if fname == "" or fname == (): 985 return 986 else: 987 if initialdir == '': 988 # explicitly missing key or empty str signals to use cwd 989 rcParams['savefig.directory'] = initialdir 990 else: 991 # save dir for next time 992 rcParams['savefig.directory'] = os.path.dirname( 993 six.text_type(fname)) 994 try: 995 # This method will handle the delegation to the correct type 996 self.figure.savefig(fname) 997 except Exception as e: 998 tkinter_messagebox.showerror("Error saving file", str(e)) 999 1000 1001class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): 1002 def __init__(self, *args, **kwargs): 1003 backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) 1004 self.window = None 1005 1006 def trigger(self, *args): 1007 self.init_window() 1008 self.window.lift() 1009 1010 def init_window(self): 1011 if self.window: 1012 return 1013 1014 toolfig = Figure(figsize=(6, 3)) 1015 self.window = Tk.Tk() 1016 1017 canvas = type(self.canvas)(toolfig, master=self.window) 1018 toolfig.subplots_adjust(top=0.9) 1019 _tool = SubplotTool(self.figure, toolfig) 1020 canvas.draw() 1021 canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) 1022 self.window.protocol("WM_DELETE_WINDOW", self.destroy) 1023 1024 def destroy(self, *args, **kwargs): 1025 self.window.destroy() 1026 self.window = None 1027 1028 1029backend_tools.ToolSaveFigure = SaveFigureTk 1030backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk 1031backend_tools.ToolSetCursor = SetCursorTk 1032backend_tools.ToolRubberband = RubberbandTk 1033Toolbar = ToolbarTk 1034 1035 1036@_Backend.export 1037class _BackendTk(_Backend): 1038 FigureManager = FigureManagerTk 1039 1040 @classmethod 1041 def new_figure_manager_given_figure(cls, num, figure): 1042 """ 1043 Create a new figure manager instance for the given figure. 1044 """ 1045 _focus = windowing.FocusManager() 1046 window = Tk.Tk(className="matplotlib") 1047 window.withdraw() 1048 1049 # Put a mpl icon on the window rather than the default tk icon. 1050 # Tkinter doesn't allow colour icons on linux systems, but tk>=8.5 has 1051 # a iconphoto command which we call directly. Source: 1052 # http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html 1053 icon_fname = os.path.join( 1054 rcParams['datapath'], 'images', 'matplotlib.ppm') 1055 icon_img = Tk.PhotoImage(file=icon_fname) 1056 try: 1057 window.tk.call('wm', 'iconphoto', window._w, icon_img) 1058 except Exception as exc: 1059 # log the failure (due e.g. to Tk version), but carry on 1060 _log.info('Could not load matplotlib icon: %s', exc) 1061 1062 canvas = cls.FigureCanvas(figure, master=window) 1063 manager = cls.FigureManager(canvas, num, window) 1064 if matplotlib.is_interactive(): 1065 manager.show() 1066 canvas.draw_idle() 1067 return manager 1068 1069 @staticmethod 1070 def trigger_manager_draw(manager): 1071 manager.show() 1072 1073 @staticmethod 1074 def mainloop(): 1075 Tk.mainloop() 1076