diff --git a/doc/users/next_whats_new/more-cairo.rst b/doc/users/next_whats_new/more-cairo.rst new file mode 100644 index 000000000000..2bbf19345325 --- /dev/null +++ b/doc/users/next_whats_new/more-cairo.rst @@ -0,0 +1,5 @@ +Cairo rendering for Tk canvases +------------------------------- + +The new ``TkCairo`` backend allows Tk canvases to use Cairo rendering instead +of Agg. diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py new file mode 100644 index 000000000000..e5ef40caf74f --- /dev/null +++ b/lib/matplotlib/backends/_backend_tk.py @@ -0,0 +1,1073 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import tkinter as Tk + +import logging +import os.path +import sys + +# Paint image to Tk photo blitter extension +import matplotlib.backends.tkagg as tkagg + +from matplotlib.backends.backend_agg import FigureCanvasAgg +import matplotlib.backends.windowing as windowing + +import matplotlib +from matplotlib import backend_tools, cbook, rcParams +from matplotlib.backend_bases import ( + _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, + StatusbarBase, TimerBase, ToolContainerBase, cursors) +from matplotlib.backend_managers import ToolManager +from matplotlib._pylab_helpers import Gcf +from matplotlib.figure import Figure +from matplotlib.widgets import SubplotTool + + +_log = logging.getLogger(__name__) + +backend_version = Tk.TkVersion + +# the true dots per inch on the screen; should be display dependent +# 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 +PIXELS_PER_INCH = 75 + +cursord = { + cursors.MOVE: "fleur", + cursors.HAND: "hand2", + cursors.POINTER: "arrow", + cursors.SELECT_REGION: "tcross", + cursors.WAIT: "watch", + } + + +def raise_msg_to_str(msg): + """msg is a return arg from a raise. Join with new lines""" + if not isinstance(msg, six.string_types): + msg = '\n'.join(map(str, msg)) + return msg + +def error_msg_tkpaint(msg, parent=None): + from six.moves import tkinter_messagebox as tkMessageBox + tkMessageBox.showerror("matplotlib", msg) + + +class TimerTk(TimerBase): + ''' + Subclass of :class:`backend_bases.TimerBase` that uses Tk's timer events. + + Attributes + ---------- + interval : int + The time between timer events in milliseconds. Default is 1000 ms. + single_shot : bool + Boolean flag indicating whether this timer should operate as single + shot (run once and then stop). Defaults to False. + callbacks : list + Stores list of (func, args) tuples that will be called upon timer + events. This list can be manipulated directly, or the functions + `add_callback` and `remove_callback` can be used. + + ''' + def __init__(self, parent, *args, **kwargs): + TimerBase.__init__(self, *args, **kwargs) + self.parent = parent + self._timer = None + + def _timer_start(self): + self._timer_stop() + self._timer = self.parent.after(self._interval, self._on_timer) + + def _timer_stop(self): + if self._timer is not None: + self.parent.after_cancel(self._timer) + self._timer = None + + def _on_timer(self): + TimerBase._on_timer(self) + + # Tk after() is only a single shot, so we need to add code here to + # reset the timer if we're not operating in single shot mode. However, + # if _timer is None, this means that _timer_stop has been called; so + # don't recreate the timer in that case. + if not self._single and self._timer: + self._timer = self.parent.after(self._interval, self._on_timer) + else: + self._timer = None + + +class FigureCanvasTk(FigureCanvasBase): + keyvald = {65507 : 'control', + 65505 : 'shift', + 65513 : 'alt', + 65515 : 'super', + 65508 : 'control', + 65506 : 'shift', + 65514 : 'alt', + 65361 : 'left', + 65362 : 'up', + 65363 : 'right', + 65364 : 'down', + 65307 : 'escape', + 65470 : 'f1', + 65471 : 'f2', + 65472 : 'f3', + 65473 : 'f4', + 65474 : 'f5', + 65475 : 'f6', + 65476 : 'f7', + 65477 : 'f8', + 65478 : 'f9', + 65479 : 'f10', + 65480 : 'f11', + 65481 : 'f12', + 65300 : 'scroll_lock', + 65299 : 'break', + 65288 : 'backspace', + 65293 : 'enter', + 65379 : 'insert', + 65535 : 'delete', + 65360 : 'home', + 65367 : 'end', + 65365 : 'pageup', + 65366 : 'pagedown', + 65438 : '0', + 65436 : '1', + 65433 : '2', + 65435 : '3', + 65430 : '4', + 65437 : '5', + 65432 : '6', + 65429 : '7', + 65431 : '8', + 65434 : '9', + 65451 : '+', + 65453 : '-', + 65450 : '*', + 65455 : '/', + 65439 : 'dec', + 65421 : 'enter', + } + + _keycode_lookup = { + 262145: 'control', + 524320: 'alt', + 524352: 'alt', + 1048584: 'super', + 1048592: 'super', + 131074: 'shift', + 131076: 'shift', + } + """_keycode_lookup is used for badly mapped (i.e. no event.key_sym set) + keys on apple keyboards.""" + + def __init__(self, figure, master=None, resize_callback=None): + super(FigureCanvasTk, self).__init__(figure) + self._idle = True + self._idle_callback = None + t1,t2,w,h = self.figure.bbox.bounds + w, h = int(w), int(h) + self._tkcanvas = Tk.Canvas( + master=master, background="white", + width=w, height=h, borderwidth=0, highlightthickness=0) + self._tkphoto = Tk.PhotoImage( + master=self._tkcanvas, width=w, height=h) + self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) + self._resize_callback = resize_callback + self._tkcanvas.bind("", self.resize) + self._tkcanvas.bind("", self.key_press) + self._tkcanvas.bind("", self.motion_notify_event) + self._tkcanvas.bind("", self.key_release) + for name in "", "", "": + self._tkcanvas.bind(name, self.button_press_event) + for name in "", "", "": + self._tkcanvas.bind(name, self.button_dblclick_event) + for name in "", "", "": + self._tkcanvas.bind(name, self.button_release_event) + + # Mouse wheel on Linux generates button 4/5 events + for name in "", "": + self._tkcanvas.bind(name, self.scroll_event) + # Mouse wheel for windows goes to the window with the focus. + # Since the canvas won't usually have the focus, bind the + # event to the window containing the canvas instead. + # See http://wiki.tcl.tk/3893 (mousewheel) for details + root = self._tkcanvas.winfo_toplevel() + root.bind("", self.scroll_event_windows, "+") + + # Can't get destroy events by binding to _tkcanvas. Therefore, bind + # to the window and filter. + def filter_destroy(evt): + if evt.widget is self._tkcanvas: + self._master.update_idletasks() + self.close_event() + root.bind("", filter_destroy, "+") + + self._master = master + self._tkcanvas.focus_set() + + def resize(self, event): + width, height = event.width, event.height + if self._resize_callback is not None: + self._resize_callback(event) + + # compute desired figure size in inches + dpival = self.figure.dpi + winch = width/dpival + hinch = height/dpival + self.figure.set_size_inches(winch, hinch, forward=False) + + + self._tkcanvas.delete(self._tkphoto) + self._tkphoto = Tk.PhotoImage( + master=self._tkcanvas, width=int(width), height=int(height)) + self._tkcanvas.create_image(int(width/2),int(height/2),image=self._tkphoto) + self.resize_event() + self.draw() + + # a resizing will in general move the pointer position + # relative to the canvas, so process it as a motion notify + # event. An intended side effect of this call is to allow + # window raises (which trigger a resize) to get the cursor + # position to the mpl event framework so key presses which are + # over the axes will work w/o clicks or explicit motion + self._update_pointer_position(event) + + def _update_pointer_position(self, guiEvent=None): + """ + Figure out if we are inside the canvas or not and update the + canvas enter/leave events + """ + # if the pointer if over the canvas, set the lastx and lasty + # attrs of the canvas so it can process event w/o mouse click + # or move + + # the window's upper, left coords in screen coords + xw = self._tkcanvas.winfo_rootx() + yw = self._tkcanvas.winfo_rooty() + # the pointer's location in screen coords + xp, yp = self._tkcanvas.winfo_pointerxy() + + # not figure out the canvas coordinates of the pointer + xc = xp - xw + yc = yp - yw + + # flip top/bottom + yc = self.figure.bbox.height - yc + + # JDH: this method was written originally to get the pointer + # location to the backend lastx and lasty attrs so that events + # like KeyEvent can be handled without mouse events. e.g., if + # the cursor is already above the axes, then key presses like + # 'g' should toggle the grid. In order for this to work in + # backend_bases, the canvas needs to know _lastx and _lasty. + # There are three ways to get this info the canvas: + # + # 1) set it explicitly + # + # 2) call enter/leave events explicitly. The downside of this + # in the impl below is that enter could be repeatedly + # triggered if the mouse is over the axes and one is + # resizing with the keyboard. This is not entirely bad, + # because the mouse position relative to the canvas is + # changing, but it may be surprising to get repeated entries + # without leaves + # + # 3) process it as a motion notify event. This also has pros + # and cons. The mouse is moving relative to the window, but + # this may surpise an event handler writer who is getting + # motion_notify_events even if the mouse has not moved + + # here are the three scenarios + if 1: + # just manually set it + self._lastx, self._lasty = xc, yc + elif 0: + # alternate implementation: process it as a motion + FigureCanvasBase.motion_notify_event(self, xc, yc, guiEvent) + elif 0: + # alternate implementation -- process enter/leave events + # instead of motion/notify + if self.figure.bbox.contains(xc, yc): + self.enter_notify_event(guiEvent, xy=(xc,yc)) + else: + self.leave_notify_event(guiEvent) + + show = cbook.deprecated("2.2", name="FigureCanvasTk.show", + alternative="FigureCanvasTk.draw")( + lambda self: self.draw()) + + def draw_idle(self): + 'update drawing area only if idle' + if self._idle is False: + return + + self._idle = False + + def idle_draw(*args): + try: + self.draw() + finally: + self._idle = True + + self._idle_callback = self._tkcanvas.after_idle(idle_draw) + + def get_tk_widget(self): + """returns the Tk widget used to implement FigureCanvasTkAgg. + Although the initial implementation uses a Tk canvas, this routine + is intended to hide that fact. + """ + return self._tkcanvas + + def motion_notify_event(self, event): + x = event.x + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y + FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) + + + def button_press_event(self, event, dblclick=False): + x = event.x + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y + num = getattr(event, 'num', None) + + if sys.platform=='darwin': + # 2 and 3 were reversed on the OSX platform I + # tested under tkagg + if num==2: num=3 + elif num==3: num=2 + + FigureCanvasBase.button_press_event(self, x, y, num, dblclick=dblclick, guiEvent=event) + + def button_dblclick_event(self,event): + self.button_press_event(event,dblclick=True) + + def button_release_event(self, event): + x = event.x + # flipy so y=0 is bottom of canvas + y = self.figure.bbox.height - event.y + + num = getattr(event, 'num', None) + + if sys.platform=='darwin': + # 2 and 3 were reversed on the OSX platform I + # tested under tkagg + if num==2: num=3 + elif num==3: num=2 + + FigureCanvasBase.button_release_event(self, x, y, num, guiEvent=event) + + def scroll_event(self, event): + x = event.x + y = self.figure.bbox.height - event.y + num = getattr(event, 'num', None) + if num==4: step = +1 + elif num==5: step = -1 + else: step = 0 + + FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + + def scroll_event_windows(self, event): + """MouseWheel event processor""" + # need to find the window that contains the mouse + w = event.widget.winfo_containing(event.x_root, event.y_root) + if w == self._tkcanvas: + x = event.x_root - w.winfo_rootx() + y = event.y_root - w.winfo_rooty() + y = self.figure.bbox.height - y + step = event.delta/120. + FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + + def _get_key(self, event): + val = event.keysym_num + if val in self.keyvald: + key = self.keyvald[val] + elif val == 0 and sys.platform == 'darwin' and \ + event.keycode in self._keycode_lookup: + key = self._keycode_lookup[event.keycode] + elif val < 256: + key = chr(val) + else: + key = None + + # add modifier keys to the key string. Bit details originate from + # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm + # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; + # BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080; + # BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400; + # In general, the modifier key is excluded from the modifier flag, + # however this is not the case on "darwin", so double check that + # we aren't adding repeat modifier flags to a modifier key. + if sys.platform == 'win32': + modifiers = [(17, 'alt', 'alt'), + (2, 'ctrl', 'control'), + ] + elif sys.platform == 'darwin': + modifiers = [(3, 'super', 'super'), + (4, 'alt', 'alt'), + (2, 'ctrl', 'control'), + ] + else: + modifiers = [(6, 'super', 'super'), + (3, 'alt', 'alt'), + (2, 'ctrl', 'control'), + ] + + if key is not None: + # note, shift is not added to the keys as this is already accounted for + for bitmask, prefix, key_name in modifiers: + if event.state & (1 << bitmask) and key_name not in key: + key = '{0}+{1}'.format(prefix, key) + + return key + + def key_press(self, event): + key = self._get_key(event) + FigureCanvasBase.key_press_event(self, key, guiEvent=event) + + def key_release(self, event): + key = self._get_key(event) + FigureCanvasBase.key_release_event(self, key, guiEvent=event) + + def new_timer(self, *args, **kwargs): + """ + Creates a new backend-specific subclass of :class:`backend_bases.Timer`. + This is useful for getting periodic events through the backend's native + event loop. Implemented only for backends with GUIs. + + Other Parameters + ---------------- + interval : scalar + Timer interval in milliseconds + callbacks : list + Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` + will be executed by the timer every *interval*. + + """ + return TimerTk(self._tkcanvas, *args, **kwargs) + + def flush_events(self): + self._master.update() + + +class FigureManagerTk(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : tk.Toolbar + The tk.Toolbar + window : tk.Window + The tk.Window + + """ + def __init__(self, canvas, num, window): + FigureManagerBase.__init__(self, canvas, num) + self.window = window + self.window.withdraw() + self.set_window_title("Figure %d" % num) + self.canvas = canvas + self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) + self._num = num + + self.toolmanager = self._get_toolmanager() + self.toolbar = self._get_toolbar() + self.statusbar = None + + if self.toolmanager: + backend_tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + self.statusbar = StatusbarTk(self.window, self.toolmanager) + + self._shown = False + + def notify_axes_change(fig): + 'this will be called whenever the current axes is changed' + if self.toolmanager is not None: + pass + elif self.toolbar is not None: + self.toolbar.update() + self.canvas.figure.add_axobserver(notify_axes_change) + + def _get_toolbar(self): + if matplotlib.rcParams['toolbar'] == 'toolbar2': + toolbar = NavigationToolbar2Tk(self.canvas, self.window) + elif matplotlib.rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarTk(self.toolmanager, self.window) + else: + toolbar = None + return toolbar + + def _get_toolmanager(self): + if rcParams['toolbar'] == 'toolmanager': + toolmanager = ToolManager(self.canvas.figure) + else: + toolmanager = None + return toolmanager + + def resize(self, width, height=None): + # before 09-12-22, the resize method takes a single *event* + # parameter. On the other hand, the resize method of other + # FigureManager class takes *width* and *height* parameter, + # which is used to change the size of the window. For the + # Figure.set_size_inches with forward=True work with Tk + # backend, I changed the function signature but tried to keep + # it backward compatible. -JJL + + # when a single parameter is given, consider it as a event + if height is None: + cbook.warn_deprecated("2.2", "FigureManagerTkAgg.resize now takes " + "width and height as separate arguments") + width = width.width + else: + self.canvas._tkcanvas.master.geometry("%dx%d" % (width, height)) + + if self.toolbar is not None: + self.toolbar.configure(width=width) + + def show(self): + """ + this function doesn't segfault but causes the + PyEval_RestoreThread: NULL state bug on win32 + """ + _focus = windowing.FocusManager() + if not self._shown: + def destroy(*args): + self.window = None + Gcf.destroy(self._num) + self.canvas._tkcanvas.bind("", destroy) + self.window.deiconify() + else: + self.canvas.draw_idle() + # Raise the new window. + self.canvas.manager.window.attributes('-topmost', 1) + self.canvas.manager.window.attributes('-topmost', 0) + self._shown = True + + def destroy(self, *args): + if self.window is not None: + #self.toolbar.destroy() + if self.canvas._idle_callback: + self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) + self.window.destroy() + if Gcf.get_num_fig_managers()==0: + if self.window is not None: + self.window.quit() + self.window = None + + def get_window_title(self): + return self.window.wm_title() + + def set_window_title(self, title): + self.window.wm_title(title) + + def full_screen_toggle(self): + is_fullscreen = bool(self.window.attributes('-fullscreen')) + self.window.attributes('-fullscreen', not is_fullscreen) + + +@cbook.deprecated("2.2") +class AxisMenu(object): + def __init__(self, master, naxes): + self._master = master + self._naxes = naxes + self._mbar = Tk.Frame(master=master, relief=Tk.RAISED, borderwidth=2) + self._mbar.pack(side=Tk.LEFT) + self._mbutton = Tk.Menubutton( + master=self._mbar, text="Axes", underline=0) + self._mbutton.pack(side=Tk.LEFT, padx="2m") + self._mbutton.menu = Tk.Menu(self._mbutton) + self._mbutton.menu.add_command( + label="Select All", command=self.select_all) + self._mbutton.menu.add_command( + label="Invert All", command=self.invert_all) + self._axis_var = [] + self._checkbutton = [] + for i in range(naxes): + self._axis_var.append(Tk.IntVar()) + self._axis_var[i].set(1) + self._checkbutton.append(self._mbutton.menu.add_checkbutton( + label = "Axis %d" % (i+1), + variable=self._axis_var[i], + command=self.set_active)) + self._mbutton.menu.invoke(self._mbutton.menu.index("Select All")) + self._mbutton['menu'] = self._mbutton.menu + self._mbar.tk_menuBar(self._mbutton) + self.set_active() + + def adjust(self, naxes): + if self._naxes < naxes: + for i in range(self._naxes, naxes): + self._axis_var.append(Tk.IntVar()) + self._axis_var[i].set(1) + self._checkbutton.append( self._mbutton.menu.add_checkbutton( + label = "Axis %d" % (i+1), + variable=self._axis_var[i], + command=self.set_active)) + elif self._naxes > naxes: + for i in range(self._naxes-1, naxes-1, -1): + del self._axis_var[i] + self._mbutton.menu.forget(self._checkbutton[i]) + del self._checkbutton[i] + self._naxes = naxes + self.set_active() + + def get_indices(self): + a = [i for i in range(len(self._axis_var)) if self._axis_var[i].get()] + return a + + def set_active(self): + self._master.set_active(self.get_indices()) + + def invert_all(self): + for a in self._axis_var: + a.set(not a.get()) + self.set_active() + + def select_all(self): + for a in self._axis_var: + a.set(1) + self.set_active() + + +class NavigationToolbar2Tk(NavigationToolbar2, Tk.Frame): + """ + Attributes + ---------- + canvas : `FigureCanvas` + the figure canvas on which to operate + win : tk.Window + the tk.Window which owns this toolbar + + """ + def __init__(self, canvas, window): + self.canvas = canvas + self.window = window + NavigationToolbar2.__init__(self, canvas) + + def destroy(self, *args): + del self.message + Tk.Frame.destroy(self, *args) + + def set_message(self, s): + self.message.set(s) + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y0 = height - y0 + y1 = height - y1 + if hasattr(self, "lastrect"): + self.canvas._tkcanvas.delete(self.lastrect) + self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) + + #self.canvas.draw() + + def release(self, event): + try: self.lastrect + except AttributeError: pass + else: + self.canvas._tkcanvas.delete(self.lastrect) + del self.lastrect + + def set_cursor(self, cursor): + self.window.configure(cursor=cursord[cursor]) + self.window.update_idletasks() + + def _Button(self, text, file, command, extension='.gif'): + img_file = os.path.join( + rcParams['datapath'], 'images', file + extension) + im = Tk.PhotoImage(master=self, file=img_file) + b = Tk.Button( + master=self, text=text, padx=2, pady=2, image=im, command=command) + b._ntimage = im + b.pack(side=Tk.LEFT) + return b + + def _Spacer(self): + # Buttons are 30px high, so make this 26px tall with padding to center it + s = Tk.Frame( + master=self, height=26, relief=Tk.RIDGE, pady=2, bg="DarkGray") + s.pack(side=Tk.LEFT, padx=5) + return s + + def _init_toolbar(self): + xmin, xmax = self.canvas.figure.bbox.intervalx + height, width = 50, xmax-xmin + Tk.Frame.__init__(self, master=self.window, + width=int(width), height=int(height), + borderwidth=2) + + self.update() # Make axes menu + + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + # Add a spacer; return value is unused. + self._Spacer() + else: + button = self._Button(text=text, file=image_file, + command=getattr(self, callback)) + if tooltip_text is not None: + ToolTip.createToolTip(button, tooltip_text) + + self.message = Tk.StringVar(master=self) + self._message_label = Tk.Label(master=self, textvariable=self.message) + self._message_label.pack(side=Tk.RIGHT) + self.pack(side=Tk.BOTTOM, fill=Tk.X) + + def configure_subplots(self): + toolfig = Figure(figsize=(6,3)) + window = Tk.Toplevel() + canvas = type(self.canvas)(toolfig, master=window) + toolfig.subplots_adjust(top=0.9) + canvas.tool = SubplotTool(self.canvas.figure, toolfig) + canvas.draw() + canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) + window.grab_set() + + def save_figure(self, *args): + from six.moves import tkinter_tkfiledialog, tkinter_messagebox + filetypes = self.canvas.get_supported_filetypes().copy() + default_filetype = self.canvas.get_default_filetype() + + # Tk doesn't provide a way to choose a default filetype, + # so we just have to put it first + default_filetype_name = filetypes.pop(default_filetype) + sorted_filetypes = ([(default_filetype, default_filetype_name)] + + sorted(six.iteritems(filetypes))) + tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] + + # adding a default extension seems to break the + # asksaveasfilename dialog when you choose various save types + # from the dropdown. Passing in the empty string seems to + # work - JDH! + #defaultextension = self.canvas.get_default_filetype() + defaultextension = '' + initialdir = os.path.expanduser(rcParams['savefig.directory']) + initialfile = self.canvas.get_default_filename() + fname = tkinter_tkfiledialog.asksaveasfilename( + master=self.window, + title='Save the figure', + filetypes=tk_filetypes, + defaultextension=defaultextension, + initialdir=initialdir, + initialfile=initialfile, + ) + + if fname in ["", ()]: + return + # Save dir for next time, unless empty str (i.e., use cwd). + if initialdir != "": + rcParams['savefig.directory'] = ( + os.path.dirname(six.text_type(fname))) + try: + # This method will handle the delegation to the correct type + self.canvas.figure.savefig(fname) + except Exception as e: + tkinter_messagebox.showerror("Error saving file", str(e)) + + def set_active(self, ind): + self._ind = ind + self._active = [self._axes[i] for i in self._ind] + + def update(self): + _focus = windowing.FocusManager() + self._axes = self.canvas.figure.axes + NavigationToolbar2.update(self) + + +class ToolTip(object): + """ + Tooltip recipe from + http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387 + """ + @staticmethod + def createToolTip(widget, text): + toolTip = ToolTip(widget) + def enter(event): + toolTip.showtip(text) + def leave(event): + toolTip.hidetip() + widget.bind('', enter) + widget.bind('', leave) + + def __init__(self, widget): + self.widget = widget + self.tipwindow = None + self.id = None + self.x = self.y = 0 + + def showtip(self, text): + "Display text in tooltip window" + self.text = text + if self.tipwindow or not self.text: + return + x, y, _, _ = self.widget.bbox("insert") + x = x + self.widget.winfo_rootx() + 27 + y = y + self.widget.winfo_rooty() + self.tipwindow = tw = Tk.Toplevel(self.widget) + tw.wm_overrideredirect(1) + tw.wm_geometry("+%d+%d" % (x, y)) + try: + # For Mac OS + tw.tk.call("::tk::unsupported::MacWindowStyle", + "style", tw._w, + "help", "noActivates") + except Tk.TclError: + pass + label = Tk.Label(tw, text=self.text, justify=Tk.LEFT, + background="#ffffe0", relief=Tk.SOLID, borderwidth=1) + label.pack(ipadx=1) + + def hidetip(self): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + + +class RubberbandTk(backend_tools.RubberbandBase): + def __init__(self, *args, **kwargs): + backend_tools.RubberbandBase.__init__(self, *args, **kwargs) + + def draw_rubberband(self, x0, y0, x1, y1): + height = self.figure.canvas.figure.bbox.height + y0 = height - y0 + y1 = height - y1 + if hasattr(self, "lastrect"): + self.figure.canvas._tkcanvas.delete(self.lastrect) + self.lastrect = self.figure.canvas._tkcanvas.create_rectangle( + x0, y0, x1, y1) + + def remove_rubberband(self): + if hasattr(self, "lastrect"): + self.figure.canvas._tkcanvas.delete(self.lastrect) + del self.lastrect + + +class SetCursorTk(backend_tools.SetCursorBase): + def set_cursor(self, cursor): + self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) + + +class ToolbarTk(ToolContainerBase, Tk.Frame): + _icon_extension = '.gif' + def __init__(self, toolmanager, window): + ToolContainerBase.__init__(self, toolmanager) + xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx + height, width = 50, xmax - xmin + Tk.Frame.__init__(self, master=window, + width=int(width), height=int(height), + borderwidth=2) + self._toolitems = {} + self.pack(side=Tk.TOP, fill=Tk.X) + self._groups = {} + + def add_toolitem( + self, name, group, position, image_file, description, toggle): + frame = self._get_groupframe(group) + button = self._Button(name, image_file, toggle, frame) + if description is not None: + ToolTip.createToolTip(button, description) + self._toolitems.setdefault(name, []) + self._toolitems[name].append(button) + + def _get_groupframe(self, group): + if group not in self._groups: + if self._groups: + self._add_separator() + frame = Tk.Frame(master=self, borderwidth=0) + frame.pack(side=Tk.LEFT, fill=Tk.Y) + self._groups[group] = frame + return self._groups[group] + + def _add_separator(self): + separator = Tk.Frame(master=self, bd=5, width=1, bg='black') + separator.pack(side=Tk.LEFT, fill=Tk.Y, padx=2) + + def _Button(self, text, image_file, toggle, frame): + if image_file is not None: + im = Tk.PhotoImage(master=self, file=image_file) + else: + im = None + + if not toggle: + b = Tk.Button(master=frame, text=text, padx=2, pady=2, image=im, + command=lambda: self._button_click(text)) + else: + # There is a bug in tkinter included in some python 3.6 versions + # that without this variable, produces a "visual" toggling of + # other near checkbuttons + # https://bugs.python.org/issue29402 + # https://bugs.python.org/issue25684 + var = Tk.IntVar() + b = Tk.Checkbutton(master=frame, text=text, padx=2, pady=2, + image=im, indicatoron=False, + command=lambda: self._button_click(text), + variable=var) + b._ntimage = im + b.pack(side=Tk.LEFT) + return b + + def _button_click(self, name): + self.trigger_tool(name) + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for toolitem in self._toolitems[name]: + if toggled: + toolitem.select() + else: + toolitem.deselect() + + def remove_toolitem(self, name): + for toolitem in self._toolitems[name]: + toolitem.pack_forget() + del self._toolitems[name] + + +class StatusbarTk(StatusbarBase, Tk.Frame): + def __init__(self, window, *args, **kwargs): + StatusbarBase.__init__(self, *args, **kwargs) + xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx + height, width = 50, xmax - xmin + Tk.Frame.__init__(self, master=window, + width=int(width), height=int(height), + borderwidth=2) + self._message = Tk.StringVar(master=self) + self._message_label = Tk.Label(master=self, textvariable=self._message) + self._message_label.pack(side=Tk.RIGHT) + self.pack(side=Tk.TOP, fill=Tk.X) + + def set_message(self, s): + self._message.set(s) + + +class SaveFigureTk(backend_tools.SaveFigureBase): + def trigger(self, *args): + from six.moves import tkinter_tkfiledialog, tkinter_messagebox + filetypes = self.figure.canvas.get_supported_filetypes().copy() + default_filetype = self.figure.canvas.get_default_filetype() + + # Tk doesn't provide a way to choose a default filetype, + # so we just have to put it first + default_filetype_name = filetypes.pop(default_filetype) + sorted_filetypes = ([(default_filetype, default_filetype_name)] + + sorted(six.iteritems(filetypes))) + tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] + + # adding a default extension seems to break the + # asksaveasfilename dialog when you choose various save types + # from the dropdown. Passing in the empty string seems to + # work - JDH! + # defaultextension = self.figure.canvas.get_default_filetype() + defaultextension = '' + initialdir = os.path.expanduser(rcParams['savefig.directory']) + initialfile = self.figure.canvas.get_default_filename() + fname = tkinter_tkfiledialog.asksaveasfilename( + master=self.figure.canvas.manager.window, + title='Save the figure', + filetypes=tk_filetypes, + defaultextension=defaultextension, + initialdir=initialdir, + initialfile=initialfile, + ) + + if fname == "" or fname == (): + return + else: + if initialdir == '': + # explicitly missing key or empty str signals to use cwd + rcParams['savefig.directory'] = initialdir + else: + # save dir for next time + rcParams['savefig.directory'] = os.path.dirname( + six.text_type(fname)) + try: + # This method will handle the delegation to the correct type + self.figure.savefig(fname) + except Exception as e: + tkinter_messagebox.showerror("Error saving file", str(e)) + + +class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): + def __init__(self, *args, **kwargs): + backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) + self.window = None + + def trigger(self, *args): + self.init_window() + self.window.lift() + + def init_window(self): + if self.window: + return + + toolfig = Figure(figsize=(6, 3)) + self.window = Tk.Tk() + + canvas = type(self.canvas)(toolfig, master=self.window) + toolfig.subplots_adjust(top=0.9) + _tool = SubplotTool(self.figure, toolfig) + canvas.draw() + canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) + self.window.protocol("WM_DELETE_WINDOW", self.destroy) + + def destroy(self, *args, **kwargs): + self.window.destroy() + self.window = None + + +backend_tools.ToolSaveFigure = SaveFigureTk +backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk +backend_tools.ToolSetCursor = SetCursorTk +backend_tools.ToolRubberband = RubberbandTk +Toolbar = ToolbarTk + + +@_Backend.export +class _BackendTk(_Backend): + FigureManager = FigureManagerTk + + @classmethod + def new_figure_manager_given_figure(cls, num, figure): + """ + Create a new figure manager instance for the given figure. + """ + _focus = windowing.FocusManager() + window = Tk.Tk(className="matplotlib") + window.withdraw() + + # Put a mpl icon on the window rather than the default tk icon. + # Tkinter doesn't allow colour icons on linux systems, but tk>=8.5 has + # a iconphoto command which we call directly. Source: + # http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html + icon_fname = os.path.join( + rcParams['datapath'], 'images', 'matplotlib.ppm') + icon_img = Tk.PhotoImage(file=icon_fname) + try: + window.tk.call('wm', 'foobar', window._w, icon_img) + except Exception as exc: + # log the failure (due e.g. to Tk version), but carry on + _log.info('Could not load matplotlib icon: %s', exc) + + canvas = cls.FigureCanvas(figure, master=window) + manager = cls.FigureManager(canvas, num, window) + if matplotlib.is_interactive(): + manager.show() + canvas.draw_idle() + return manager + + @staticmethod + def trigger_manager_draw(manager): + manager.show() + + @staticmethod + def mainloop(): + Tk.mainloop() diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 8fbc88691271..9511326e4a5a 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1,302 +1,15 @@ -# Todd Miller jmiller@stsci.edu -from __future__ import (absolute_import, division, print_function, - unicode_literals) +from __future__ import absolute_import, division, print_function -import six -from six.moves import tkinter as Tk +from .. import cbook +from . import tkagg # Paint image to Tk photo blitter extension. +from .backend_agg import FigureCanvasAgg +from ._backend_tk import ( + _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk) -import logging -import os.path -import sys - -# Paint image to Tk photo blitter extension -import matplotlib.backends.tkagg as tkagg - -from matplotlib.backends.backend_agg import FigureCanvasAgg -import matplotlib.backends.windowing as windowing - -import matplotlib -from matplotlib import backend_tools, cbook, rcParams -from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - StatusbarBase, TimerBase, ToolContainerBase, cursors) -from matplotlib.backend_managers import ToolManager -from matplotlib._pylab_helpers import Gcf -from matplotlib.figure import Figure -from matplotlib.widgets import SubplotTool - - -_log = logging.getLogger(__name__) - -backend_version = Tk.TkVersion - -# the true dots per inch on the screen; should be display dependent -# 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 -PIXELS_PER_INCH = 75 - -cursord = { - cursors.MOVE: "fleur", - cursors.HAND: "hand2", - cursors.POINTER: "arrow", - cursors.SELECT_REGION: "tcross", - cursors.WAIT: "watch", - } - - -def raise_msg_to_str(msg): - """msg is a return arg from a raise. Join with new lines""" - if not isinstance(msg, six.string_types): - msg = '\n'.join(map(str, msg)) - return msg - -def error_msg_tkpaint(msg, parent=None): - from six.moves import tkinter_messagebox as tkMessageBox - tkMessageBox.showerror("matplotlib", msg) - - -class TimerTk(TimerBase): - ''' - Subclass of :class:`backend_bases.TimerBase` that uses Tk's timer events. - - Attributes - ---------- - interval : int - The time between timer events in milliseconds. Default is 1000 ms. - single_shot : bool - Boolean flag indicating whether this timer should operate as single - shot (run once and then stop). Defaults to False. - callbacks : list - Stores list of (func, args) tuples that will be called upon timer - events. This list can be manipulated directly, or the functions - `add_callback` and `remove_callback` can be used. - - ''' - def __init__(self, parent, *args, **kwargs): - TimerBase.__init__(self, *args, **kwargs) - self.parent = parent - self._timer = None - - def _timer_start(self): - self._timer_stop() - self._timer = self.parent.after(self._interval, self._on_timer) - - def _timer_stop(self): - if self._timer is not None: - self.parent.after_cancel(self._timer) - self._timer = None - - def _on_timer(self): - TimerBase._on_timer(self) - - # Tk after() is only a single shot, so we need to add code here to - # reset the timer if we're not operating in single shot mode. However, - # if _timer is None, this means that _timer_stop has been called; so - # don't recreate the timer in that case. - if not self._single and self._timer: - self._timer = self.parent.after(self._interval, self._on_timer) - else: - self._timer = None - - -class FigureCanvasTkAgg(FigureCanvasAgg): - keyvald = {65507 : 'control', - 65505 : 'shift', - 65513 : 'alt', - 65515 : 'super', - 65508 : 'control', - 65506 : 'shift', - 65514 : 'alt', - 65361 : 'left', - 65362 : 'up', - 65363 : 'right', - 65364 : 'down', - 65307 : 'escape', - 65470 : 'f1', - 65471 : 'f2', - 65472 : 'f3', - 65473 : 'f4', - 65474 : 'f5', - 65475 : 'f6', - 65476 : 'f7', - 65477 : 'f8', - 65478 : 'f9', - 65479 : 'f10', - 65480 : 'f11', - 65481 : 'f12', - 65300 : 'scroll_lock', - 65299 : 'break', - 65288 : 'backspace', - 65293 : 'enter', - 65379 : 'insert', - 65535 : 'delete', - 65360 : 'home', - 65367 : 'end', - 65365 : 'pageup', - 65366 : 'pagedown', - 65438 : '0', - 65436 : '1', - 65433 : '2', - 65435 : '3', - 65430 : '4', - 65437 : '5', - 65432 : '6', - 65429 : '7', - 65431 : '8', - 65434 : '9', - 65451 : '+', - 65453 : '-', - 65450 : '*', - 65455 : '/', - 65439 : 'dec', - 65421 : 'enter', - } - - _keycode_lookup = { - 262145: 'control', - 524320: 'alt', - 524352: 'alt', - 1048584: 'super', - 1048592: 'super', - 131074: 'shift', - 131076: 'shift', - } - """_keycode_lookup is used for badly mapped (i.e. no event.key_sym set) - keys on apple keyboards.""" - - def __init__(self, figure, master=None, resize_callback=None): - FigureCanvasAgg.__init__(self, figure) - self._idle = True - self._idle_callback = None - t1,t2,w,h = self.figure.bbox.bounds - w, h = int(w), int(h) - self._tkcanvas = Tk.Canvas( - master=master, background="white", - width=w, height=h, borderwidth=0, highlightthickness=0) - self._tkphoto = Tk.PhotoImage( - master=self._tkcanvas, width=w, height=h) - self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) - self._resize_callback = resize_callback - self._tkcanvas.bind("", self.resize) - self._tkcanvas.bind("", self.key_press) - self._tkcanvas.bind("", self.motion_notify_event) - self._tkcanvas.bind("", self.key_release) - for name in "", "", "": - self._tkcanvas.bind(name, self.button_press_event) - for name in "", "", "": - self._tkcanvas.bind(name, self.button_dblclick_event) - for name in "", "", "": - self._tkcanvas.bind(name, self.button_release_event) - - # Mouse wheel on Linux generates button 4/5 events - for name in "", "": - self._tkcanvas.bind(name, self.scroll_event) - # Mouse wheel for windows goes to the window with the focus. - # Since the canvas won't usually have the focus, bind the - # event to the window containing the canvas instead. - # See http://wiki.tcl.tk/3893 (mousewheel) for details - root = self._tkcanvas.winfo_toplevel() - root.bind("", self.scroll_event_windows, "+") - - # Can't get destroy events by binding to _tkcanvas. Therefore, bind - # to the window and filter. - def filter_destroy(evt): - if evt.widget is self._tkcanvas: - self._master.update_idletasks() - self.close_event() - root.bind("", filter_destroy, "+") - - self._master = master - self._tkcanvas.focus_set() - - def resize(self, event): - width, height = event.width, event.height - if self._resize_callback is not None: - self._resize_callback(event) - - # compute desired figure size in inches - dpival = self.figure.dpi - winch = width/dpival - hinch = height/dpival - self.figure.set_size_inches(winch, hinch, forward=False) - - - self._tkcanvas.delete(self._tkphoto) - self._tkphoto = Tk.PhotoImage( - master=self._tkcanvas, width=int(width), height=int(height)) - self._tkcanvas.create_image(int(width/2),int(height/2),image=self._tkphoto) - self.resize_event() - self.draw() - - # a resizing will in general move the pointer position - # relative to the canvas, so process it as a motion notify - # event. An intended side effect of this call is to allow - # window raises (which trigger a resize) to get the cursor - # position to the mpl event framework so key presses which are - # over the axes will work w/o clicks or explicit motion - self._update_pointer_position(event) - - def _update_pointer_position(self, guiEvent=None): - """ - Figure out if we are inside the canvas or not and update the - canvas enter/leave events - """ - # if the pointer if over the canvas, set the lastx and lasty - # attrs of the canvas so it can process event w/o mouse click - # or move - - # the window's upper, left coords in screen coords - xw = self._tkcanvas.winfo_rootx() - yw = self._tkcanvas.winfo_rooty() - # the pointer's location in screen coords - xp, yp = self._tkcanvas.winfo_pointerxy() - - # not figure out the canvas coordinates of the pointer - xc = xp - xw - yc = yp - yw - - # flip top/bottom - yc = self.figure.bbox.height - yc - - # JDH: this method was written originally to get the pointer - # location to the backend lastx and lasty attrs so that events - # like KeyEvent can be handled without mouse events. e.g., if - # the cursor is already above the axes, then key presses like - # 'g' should toggle the grid. In order for this to work in - # backend_bases, the canvas needs to know _lastx and _lasty. - # There are three ways to get this info the canvas: - # - # 1) set it explicitly - # - # 2) call enter/leave events explicitly. The downside of this - # in the impl below is that enter could be repeatedly - # triggered if the mouse is over the axes and one is - # resizing with the keyboard. This is not entirely bad, - # because the mouse position relative to the canvas is - # changing, but it may be surprising to get repeated entries - # without leaves - # - # 3) process it as a motion notify event. This also has pros - # and cons. The mouse is moving relative to the window, but - # this may surpise an event handler writer who is getting - # motion_notify_events even if the mouse has not moved - - # here are the three scenarios - if 1: - # just manually set it - self._lastx, self._lasty = xc, yc - elif 0: - # alternate implementation: process it as a motion - FigureCanvasBase.motion_notify_event(self, xc, yc, guiEvent) - elif 0: - # alternate implementation -- process enter/leave events - # instead of motion/notify - if self.figure.bbox.contains(xc, yc): - self.enter_notify_event(guiEvent, xy=(xc,yc)) - else: - self.leave_notify_event(guiEvent) +class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk): def draw(self): - FigureCanvasAgg.draw(self) + super(FigureCanvasTkAgg, self).draw() tkagg.blit(self._tkphoto, self.renderer._renderer, colormode=2) self._master.update_idletasks() @@ -305,780 +18,17 @@ def blit(self, bbox=None): self._tkphoto, self.renderer._renderer, bbox=bbox, colormode=2) self._master.update_idletasks() - show = cbook.deprecated("2.2", name="FigureCanvasTkAgg.show", - alternative="FigureCanvasTkAgg.draw")(draw) - - def draw_idle(self): - 'update drawing area only if idle' - if self._idle is False: - return - - self._idle = False - - def idle_draw(*args): - try: - self.draw() - finally: - self._idle = True - - self._idle_callback = self._tkcanvas.after_idle(idle_draw) - - def get_tk_widget(self): - """returns the Tk widget used to implement FigureCanvasTkAgg. - Although the initial implementation uses a Tk canvas, this routine - is intended to hide that fact. - """ - return self._tkcanvas - - def motion_notify_event(self, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) - - - def button_press_event(self, event, dblclick=False): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - num = getattr(event, 'num', None) - - if sys.platform=='darwin': - # 2 and 3 were reversed on the OSX platform I - # tested under tkagg - if num==2: num=3 - elif num==3: num=2 - - FigureCanvasBase.button_press_event(self, x, y, num, dblclick=dblclick, guiEvent=event) - - def button_dblclick_event(self,event): - self.button_press_event(event,dblclick=True) - - def button_release_event(self, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - - num = getattr(event, 'num', None) - - if sys.platform=='darwin': - # 2 and 3 were reversed on the OSX platform I - # tested under tkagg - if num==2: num=3 - elif num==3: num=2 - - FigureCanvasBase.button_release_event(self, x, y, num, guiEvent=event) - - def scroll_event(self, event): - x = event.x - y = self.figure.bbox.height - event.y - num = getattr(event, 'num', None) - if num==4: step = +1 - elif num==5: step = -1 - else: step = 0 - - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) - - def scroll_event_windows(self, event): - """MouseWheel event processor""" - # need to find the window that contains the mouse - w = event.widget.winfo_containing(event.x_root, event.y_root) - if w == self._tkcanvas: - x = event.x_root - w.winfo_rootx() - y = event.y_root - w.winfo_rooty() - y = self.figure.bbox.height - y - step = event.delta/120. - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) - - def _get_key(self, event): - val = event.keysym_num - if val in self.keyvald: - key = self.keyvald[val] - elif val == 0 and sys.platform == 'darwin' and \ - event.keycode in self._keycode_lookup: - key = self._keycode_lookup[event.keycode] - elif val < 256: - key = chr(val) - else: - key = None - - # add modifier keys to the key string. Bit details originate from - # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm - # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; - # BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080; - # BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400; - # In general, the modifier key is excluded from the modifier flag, - # however this is not the case on "darwin", so double check that - # we aren't adding repeat modifier flags to a modifier key. - if sys.platform == 'win32': - modifiers = [(17, 'alt', 'alt'), - (2, 'ctrl', 'control'), - ] - elif sys.platform == 'darwin': - modifiers = [(3, 'super', 'super'), - (4, 'alt', 'alt'), - (2, 'ctrl', 'control'), - ] - else: - modifiers = [(6, 'super', 'super'), - (3, 'alt', 'alt'), - (2, 'ctrl', 'control'), - ] - - if key is not None: - # note, shift is not added to the keys as this is already accounted for - for bitmask, prefix, key_name in modifiers: - if event.state & (1 << bitmask) and key_name not in key: - key = '{0}+{1}'.format(prefix, key) - - return key - - def key_press(self, event): - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) - - def key_release(self, event): - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) - - def new_timer(self, *args, **kwargs): - """ - Creates a new backend-specific subclass of :class:`backend_bases.Timer`. - This is useful for getting periodic events through the backend's native - event loop. Implemented only for backends with GUIs. - - Other Parameters - ---------------- - interval : scalar - Timer interval in milliseconds - callbacks : list - Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` - will be executed by the timer every *interval*. - - """ - return TimerTk(self._tkcanvas, *args, **kwargs) - - def flush_events(self): - self._master.update() - - -class FigureManagerTkAgg(FigureManagerBase): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The FigureCanvas instance - num : int or str - The Figure number - toolbar : tk.Toolbar - The tk.Toolbar - window : tk.Window - The tk.Window - - """ - def __init__(self, canvas, num, window): - FigureManagerBase.__init__(self, canvas, num) - self.window = window - self.window.withdraw() - self.set_window_title("Figure %d" % num) - self.canvas = canvas - self.canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) - self._num = num - - self.toolmanager = self._get_toolmanager() - self.toolbar = self._get_toolbar() - self.statusbar = None - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - self.statusbar = StatusbarTk(self.window, self.toolmanager) - - self._shown = False - - def notify_axes_change(fig): - 'this will be called whenever the current axes is changed' - if self.toolmanager is not None: - pass - elif self.toolbar is not None: - self.toolbar.update() - self.canvas.figure.add_axobserver(notify_axes_change) - - def _get_toolbar(self): - if matplotlib.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2TkAgg(self.canvas, self.window) - elif matplotlib.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarTk(self.toolmanager, self.window) - else: - toolbar = None - return toolbar - - def _get_toolmanager(self): - if rcParams['toolbar'] == 'toolmanager': - toolmanager = ToolManager(self.canvas.figure) - else: - toolmanager = None - return toolmanager - - def resize(self, width, height=None): - # before 09-12-22, the resize method takes a single *event* - # parameter. On the other hand, the resize method of other - # FigureManager class takes *width* and *height* parameter, - # which is used to change the size of the window. For the - # Figure.set_size_inches with forward=True work with Tk - # backend, I changed the function signature but tried to keep - # it backward compatible. -JJL - - # when a single parameter is given, consider it as a event - if height is None: - cbook.warn_deprecated("2.2", "FigureManagerTkAgg.resize now takes " - "width and height as separate arguments") - width = width.width - else: - self.canvas._tkcanvas.master.geometry("%dx%d" % (width, height)) - - if self.toolbar is not None: - self.toolbar.configure(width=width) - - def show(self): - """ - this function doesn't segfault but causes the - PyEval_RestoreThread: NULL state bug on win32 - """ - _focus = windowing.FocusManager() - if not self._shown: - def destroy(*args): - self.window = None - Gcf.destroy(self._num) - self.canvas._tkcanvas.bind("", destroy) - self.window.deiconify() - else: - self.canvas.draw_idle() - # Raise the new window. - self.canvas.manager.window.attributes('-topmost', 1) - self.canvas.manager.window.attributes('-topmost', 0) - self._shown = True - - def destroy(self, *args): - if self.window is not None: - #self.toolbar.destroy() - if self.canvas._idle_callback: - self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) - self.window.destroy() - if Gcf.get_num_fig_managers()==0: - if self.window is not None: - self.window.quit() - self.window = None - - def get_window_title(self): - return self.window.wm_title() - - def set_window_title(self, title): - self.window.wm_title(title) - - def full_screen_toggle(self): - is_fullscreen = bool(self.window.attributes('-fullscreen')) - self.window.attributes('-fullscreen', not is_fullscreen) - @cbook.deprecated("2.2") -class AxisMenu(object): - def __init__(self, master, naxes): - self._master = master - self._naxes = naxes - self._mbar = Tk.Frame(master=master, relief=Tk.RAISED, borderwidth=2) - self._mbar.pack(side=Tk.LEFT) - self._mbutton = Tk.Menubutton( - master=self._mbar, text="Axes", underline=0) - self._mbutton.pack(side=Tk.LEFT, padx="2m") - self._mbutton.menu = Tk.Menu(self._mbutton) - self._mbutton.menu.add_command( - label="Select All", command=self.select_all) - self._mbutton.menu.add_command( - label="Invert All", command=self.invert_all) - self._axis_var = [] - self._checkbutton = [] - for i in range(naxes): - self._axis_var.append(Tk.IntVar()) - self._axis_var[i].set(1) - self._checkbutton.append(self._mbutton.menu.add_checkbutton( - label = "Axis %d" % (i+1), - variable=self._axis_var[i], - command=self.set_active)) - self._mbutton.menu.invoke(self._mbutton.menu.index("Select All")) - self._mbutton['menu'] = self._mbutton.menu - self._mbar.tk_menuBar(self._mbutton) - self.set_active() - - def adjust(self, naxes): - if self._naxes < naxes: - for i in range(self._naxes, naxes): - self._axis_var.append(Tk.IntVar()) - self._axis_var[i].set(1) - self._checkbutton.append( self._mbutton.menu.add_checkbutton( - label = "Axis %d" % (i+1), - variable=self._axis_var[i], - command=self.set_active)) - elif self._naxes > naxes: - for i in range(self._naxes-1, naxes-1, -1): - del self._axis_var[i] - self._mbutton.menu.forget(self._checkbutton[i]) - del self._checkbutton[i] - self._naxes = naxes - self.set_active() - - def get_indices(self): - a = [i for i in range(len(self._axis_var)) if self._axis_var[i].get()] - return a - - def set_active(self): - self._master.set_active(self.get_indices()) - - def invert_all(self): - for a in self._axis_var: - a.set(not a.get()) - self.set_active() - - def select_all(self): - for a in self._axis_var: - a.set(1) - self.set_active() - - -class NavigationToolbar2TkAgg(NavigationToolbar2, Tk.Frame): - """ - Attributes - ---------- - canvas : `FigureCanvas` - the figure canvas on which to operate - win : tk.Window - the tk.Window which owns this toolbar - - """ - def __init__(self, canvas, window): - self.canvas = canvas - self.window = window - NavigationToolbar2.__init__(self, canvas) - - def destroy(self, *args): - del self.message - Tk.Frame.destroy(self, *args) - - def set_message(self, s): - self.message.set(s) - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y0 = height - y0 - y1 = height - y1 - if hasattr(self, "lastrect"): - self.canvas._tkcanvas.delete(self.lastrect) - self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) - - #self.canvas.draw() - - def release(self, event): - try: self.lastrect - except AttributeError: pass - else: - self.canvas._tkcanvas.delete(self.lastrect) - del self.lastrect - - def set_cursor(self, cursor): - self.window.configure(cursor=cursord[cursor]) - self.window.update_idletasks() - - def _Button(self, text, file, command, extension='.gif'): - img_file = os.path.join( - rcParams['datapath'], 'images', file + extension) - im = Tk.PhotoImage(master=self, file=img_file) - b = Tk.Button( - master=self, text=text, padx=2, pady=2, image=im, command=command) - b._ntimage = im - b.pack(side=Tk.LEFT) - return b - - def _Spacer(self): - # Buttons are 30px high, so make this 26px tall with padding to center it - s = Tk.Frame( - master=self, height=26, relief=Tk.RIDGE, pady=2, bg="DarkGray") - s.pack(side=Tk.LEFT, padx=5) - return s - - def _init_toolbar(self): - xmin, xmax = self.canvas.figure.bbox.intervalx - height, width = 50, xmax-xmin - Tk.Frame.__init__(self, master=self.window, - width=int(width), height=int(height), - borderwidth=2) - - self.update() # Make axes menu - - for text, tooltip_text, image_file, callback in self.toolitems: - if text is None: - # Add a spacer; return value is unused. - self._Spacer() - else: - button = self._Button(text=text, file=image_file, - command=getattr(self, callback)) - if tooltip_text is not None: - ToolTip.createToolTip(button, tooltip_text) - - self.message = Tk.StringVar(master=self) - self._message_label = Tk.Label(master=self, textvariable=self.message) - self._message_label.pack(side=Tk.RIGHT) - self.pack(side=Tk.BOTTOM, fill=Tk.X) - - def configure_subplots(self): - toolfig = Figure(figsize=(6,3)) - window = Tk.Toplevel() - canvas = FigureCanvasTkAgg(toolfig, master=window) - toolfig.subplots_adjust(top=0.9) - canvas.tool = SubplotTool(self.canvas.figure, toolfig) - canvas.draw() - canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) - window.grab_set() - - def save_figure(self, *args): - from six.moves import tkinter_tkfiledialog, tkinter_messagebox - filetypes = self.canvas.get_supported_filetypes().copy() - default_filetype = self.canvas.get_default_filetype() - - # Tk doesn't provide a way to choose a default filetype, - # so we just have to put it first - default_filetype_name = filetypes.pop(default_filetype) - sorted_filetypes = ([(default_filetype, default_filetype_name)] - + sorted(six.iteritems(filetypes))) - tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] - - # adding a default extension seems to break the - # asksaveasfilename dialog when you choose various save types - # from the dropdown. Passing in the empty string seems to - # work - JDH! - #defaultextension = self.canvas.get_default_filetype() - defaultextension = '' - initialdir = os.path.expanduser(rcParams['savefig.directory']) - initialfile = self.canvas.get_default_filename() - fname = tkinter_tkfiledialog.asksaveasfilename( - master=self.window, - title='Save the figure', - filetypes=tk_filetypes, - defaultextension=defaultextension, - initialdir=initialdir, - initialfile=initialfile, - ) - - if fname in ["", ()]: - return - # Save dir for next time, unless empty str (i.e., use cwd). - if initialdir != "": - rcParams['savefig.directory'] = ( - os.path.dirname(six.text_type(fname))) - try: - # This method will handle the delegation to the correct type - self.canvas.figure.savefig(fname) - except Exception as e: - tkinter_messagebox.showerror("Error saving file", str(e)) - - def set_active(self, ind): - self._ind = ind - self._active = [self._axes[i] for i in self._ind] - - def update(self): - _focus = windowing.FocusManager() - self._axes = self.canvas.figure.axes - NavigationToolbar2.update(self) - - -class ToolTip(object): - """ - Tooltip recipe from - http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387 - """ - @staticmethod - def createToolTip(widget, text): - toolTip = ToolTip(widget) - def enter(event): - toolTip.showtip(text) - def leave(event): - toolTip.hidetip() - widget.bind('', enter) - widget.bind('', leave) - - def __init__(self, widget): - self.widget = widget - self.tipwindow = None - self.id = None - self.x = self.y = 0 - - def showtip(self, text): - "Display text in tooltip window" - self.text = text - if self.tipwindow or not self.text: - return - x, y, _, _ = self.widget.bbox("insert") - x = x + self.widget.winfo_rootx() + 27 - y = y + self.widget.winfo_rooty() - self.tipwindow = tw = Tk.Toplevel(self.widget) - tw.wm_overrideredirect(1) - tw.wm_geometry("+%d+%d" % (x, y)) - try: - # For Mac OS - tw.tk.call("::tk::unsupported::MacWindowStyle", - "style", tw._w, - "help", "noActivates") - except Tk.TclError: - pass - label = Tk.Label(tw, text=self.text, justify=Tk.LEFT, - background="#ffffe0", relief=Tk.SOLID, borderwidth=1) - label.pack(ipadx=1) +class FigureManagerTkAgg(FigureManagerTk): + pass - def hidetip(self): - tw = self.tipwindow - self.tipwindow = None - if tw: - tw.destroy() - -class RubberbandTk(backend_tools.RubberbandBase): - def __init__(self, *args, **kwargs): - backend_tools.RubberbandBase.__init__(self, *args, **kwargs) - - def draw_rubberband(self, x0, y0, x1, y1): - height = self.figure.canvas.figure.bbox.height - y0 = height - y0 - y1 = height - y1 - if hasattr(self, "lastrect"): - self.figure.canvas._tkcanvas.delete(self.lastrect) - self.lastrect = self.figure.canvas._tkcanvas.create_rectangle( - x0, y0, x1, y1) - - def remove_rubberband(self): - if hasattr(self, "lastrect"): - self.figure.canvas._tkcanvas.delete(self.lastrect) - del self.lastrect - - -class SetCursorTk(backend_tools.SetCursorBase): - def set_cursor(self, cursor): - self.figure.canvas.manager.window.configure(cursor=cursord[cursor]) - - -class ToolbarTk(ToolContainerBase, Tk.Frame): - _icon_extension = '.gif' - def __init__(self, toolmanager, window): - ToolContainerBase.__init__(self, toolmanager) - xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx - height, width = 50, xmax - xmin - Tk.Frame.__init__(self, master=window, - width=int(width), height=int(height), - borderwidth=2) - self._toolitems = {} - self.pack(side=Tk.TOP, fill=Tk.X) - self._groups = {} - - def add_toolitem( - self, name, group, position, image_file, description, toggle): - frame = self._get_groupframe(group) - button = self._Button(name, image_file, toggle, frame) - if description is not None: - ToolTip.createToolTip(button, description) - self._toolitems.setdefault(name, []) - self._toolitems[name].append(button) - - def _get_groupframe(self, group): - if group not in self._groups: - if self._groups: - self._add_separator() - frame = Tk.Frame(master=self, borderwidth=0) - frame.pack(side=Tk.LEFT, fill=Tk.Y) - self._groups[group] = frame - return self._groups[group] - - def _add_separator(self): - separator = Tk.Frame(master=self, bd=5, width=1, bg='black') - separator.pack(side=Tk.LEFT, fill=Tk.Y, padx=2) - - def _Button(self, text, image_file, toggle, frame): - if image_file is not None: - im = Tk.PhotoImage(master=self, file=image_file) - else: - im = None - - if not toggle: - b = Tk.Button(master=frame, text=text, padx=2, pady=2, image=im, - command=lambda: self._button_click(text)) - else: - # There is a bug in tkinter included in some python 3.6 versions - # that without this variable, produces a "visual" toggling of - # other near checkbuttons - # https://bugs.python.org/issue29402 - # https://bugs.python.org/issue25684 - var = Tk.IntVar() - b = Tk.Checkbutton(master=frame, text=text, padx=2, pady=2, - image=im, indicatoron=False, - command=lambda: self._button_click(text), - variable=var) - b._ntimage = im - b.pack(side=Tk.LEFT) - return b - - def _button_click(self, name): - self.trigger_tool(name) - - def toggle_toolitem(self, name, toggled): - if name not in self._toolitems: - return - for toolitem in self._toolitems[name]: - if toggled: - toolitem.select() - else: - toolitem.deselect() - - def remove_toolitem(self, name): - for toolitem in self._toolitems[name]: - toolitem.pack_forget() - del self._toolitems[name] - - -class StatusbarTk(StatusbarBase, Tk.Frame): - def __init__(self, window, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx - height, width = 50, xmax - xmin - Tk.Frame.__init__(self, master=window, - width=int(width), height=int(height), - borderwidth=2) - self._message = Tk.StringVar(master=self) - self._message_label = Tk.Label(master=self, textvariable=self._message) - self._message_label.pack(side=Tk.RIGHT) - self.pack(side=Tk.TOP, fill=Tk.X) - - def set_message(self, s): - self._message.set(s) - - -class SaveFigureTk(backend_tools.SaveFigureBase): - def trigger(self, *args): - from six.moves import tkinter_tkfiledialog, tkinter_messagebox - filetypes = self.figure.canvas.get_supported_filetypes().copy() - default_filetype = self.figure.canvas.get_default_filetype() - - # Tk doesn't provide a way to choose a default filetype, - # so we just have to put it first - default_filetype_name = filetypes.pop(default_filetype) - sorted_filetypes = ([(default_filetype, default_filetype_name)] - + sorted(six.iteritems(filetypes))) - tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] - - # adding a default extension seems to break the - # asksaveasfilename dialog when you choose various save types - # from the dropdown. Passing in the empty string seems to - # work - JDH! - # defaultextension = self.figure.canvas.get_default_filetype() - defaultextension = '' - initialdir = os.path.expanduser(rcParams['savefig.directory']) - initialfile = self.figure.canvas.get_default_filename() - fname = tkinter_tkfiledialog.asksaveasfilename( - master=self.figure.canvas.manager.window, - title='Save the figure', - filetypes=tk_filetypes, - defaultextension=defaultextension, - initialdir=initialdir, - initialfile=initialfile, - ) - - if fname == "" or fname == (): - return - else: - if initialdir == '': - # explicitly missing key or empty str signals to use cwd - rcParams['savefig.directory'] = initialdir - else: - # save dir for next time - rcParams['savefig.directory'] = os.path.dirname( - six.text_type(fname)) - try: - # This method will handle the delegation to the correct type - self.figure.savefig(fname) - except Exception as e: - tkinter_messagebox.showerror("Error saving file", str(e)) - - -class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): - def __init__(self, *args, **kwargs): - backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs) - self.window = None - - def trigger(self, *args): - self.init_window() - self.window.lift() - - def init_window(self): - if self.window: - return - - toolfig = Figure(figsize=(6, 3)) - self.window = Tk.Tk() - - canvas = FigureCanvasTkAgg(toolfig, master=self.window) - toolfig.subplots_adjust(top=0.9) - _tool = SubplotTool(self.figure, toolfig) - canvas.draw() - canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) - self.window.protocol("WM_DELETE_WINDOW", self.destroy) - - def destroy(self, *args, **kwargs): - self.window.destroy() - self.window = None - - -backend_tools.ToolSaveFigure = SaveFigureTk -backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk -backend_tools.ToolSetCursor = SetCursorTk -backend_tools.ToolRubberband = RubberbandTk -Toolbar = ToolbarTk +@cbook.deprecated("2.2") +class NavigationToolbar2TkAgg(NavigationToolbar2Tk): + pass -@_Backend.export -class _BackendTkAgg(_Backend): +@_BackendTk.export +class _BackendTkAgg(_BackendTk): FigureCanvas = FigureCanvasTkAgg - FigureManager = FigureManagerTkAgg - - @staticmethod - def new_figure_manager_given_figure(num, figure): - """ - Create a new figure manager instance for the given figure. - """ - _focus = windowing.FocusManager() - window = Tk.Tk(className="matplotlib") - window.withdraw() - - # Put a mpl icon on the window rather than the default tk icon. - # Tkinter doesn't allow colour icons on linux systems, but tk>=8.5 has - # a iconphoto command which we call directly. Source: - # http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html - icon_fname = os.path.join( - rcParams['datapath'], 'images', 'matplotlib.ppm') - icon_img = Tk.PhotoImage(file=icon_fname) - try: - window.tk.call('wm', 'foobar', window._w, icon_img) - except Exception as exc: - # log the failure (due e.g. to Tk version), but carry on - _log.info('Could not load matplotlib icon: %s', exc) - - canvas = FigureCanvasTkAgg(figure, master=window) - manager = FigureManagerTkAgg(canvas, num, window) - if matplotlib.is_interactive(): - manager.show() - canvas.draw_idle() - return manager - - @staticmethod - def trigger_manager_draw(manager): - manager.show() - - @staticmethod - def mainloop(): - Tk.mainloop() diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py new file mode 100644 index 000000000000..c4edfb97ed1a --- /dev/null +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, division, print_function + +import sys + +import numpy as np + +from . import tkagg # Paint image to Tk photo blitter extension. +from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo +from ._backend_tk import _BackendTk, FigureCanvasTk + + +class FigureCanvasTkCairo(FigureCanvasCairo, FigureCanvasTk): + def __init__(self, *args, **kwargs): + super(FigureCanvasTkCairo, self).__init__(*args, **kwargs) + self._renderer = RendererCairo(self.figure.dpi) + + def draw(self): + width = int(self.figure.bbox.width) + height = int(self.figure.bbox.height) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + self._renderer.set_ctx_from_surface(surface) + self._renderer.set_width_height(width, height) + self.figure.draw(self._renderer) + buf = np.reshape(surface.get_data(), (height, width, 4)) + # Convert from ARGB32 to RGBA8888. Using .take() instead of directly + # indexing ensures C-contiguity of the result, which is needed by + # tkagg. + buf = buf.take( + [2, 1, 0, 3] if sys.byteorder == "little" else [1, 2, 3, 0], + axis=2) + tkagg.blit(self._tkphoto, buf, colormode=2) + self._master.update_idletasks() + + +@_BackendTk.export +class _BackendTkCairo(_BackendTk): + FigureCanvas = FigureCanvasTkCairo diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 20a45f4d0f13..a4434f1ba5d4 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -42,7 +42,7 @@ 'MacOSX', 'nbAgg', 'Qt4Agg', 'Qt4Cairo', 'Qt5Agg', 'Qt5Cairo', - 'TkAgg', + 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg', 'WXCairo'] non_interactive_bk = ['agg', 'cairo', 'gdk', diff --git a/pytest.ini b/pytest.ini index e21f8ef0d4d5..c3495651769b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -35,7 +35,7 @@ pep8ignore = matplotlib/backends/backend_ps.py E202 E203 E225 E228 E231 E261 E262 E271 E301 E302 E303 E401 E402 E501 E701 matplotlib/backends/backend_svg.py E203 E221 E225 E228 E231 E261 E302 E401 E501 matplotlib/backends/backend_template.py E302 E303 - matplotlib/backends/backend_tkagg.py E201 E202 E203 E222 E225 E231 E251 E271 E301 E302 E303 E401 E501 E701 W293 + matplotlib/backends/_backend_tk.py E201 E202 E203 E222 E225 E231 E251 E271 E301 E302 E303 E401 E501 E701 W293 matplotlib/backends/tkagg.py E231 E302 E701 matplotlib/backends/windowing.py E301 E302 matplotlib/backend_bases.py E225 E712 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy