Download lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py from parallel-reasoner/env-full: direct link, hf CLI and curl.
- Browser
- Download file 43.3 kB
-
https://huggingface.co/parallel-reasoner/env-full/resolve/a2585bd5ca1cf640fa361bd20177ded829caaa8d/lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py
- Command line
-
hf download hf://parallel-reasoner/env-full@a2585bd5ca1cf640fa361bd20177ded829caaa8d/lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py
-
curl -L -o _backend_tk.py https://huggingface.co/parallel-reasoner/env-full/resolve/a2585bd5ca1cf640fa361bd20177ded829caaa8d/lib/python3.12/site-packages/matplotlib/backends/_backend_tk.py
43.3 kB
| import uuid | |
| import weakref | |
| from contextlib import contextmanager | |
| import logging | |
| import math | |
| import os.path | |
| import pathlib | |
| import sys | |
| import tkinter as tk | |
| import tkinter.filedialog | |
| import tkinter.font | |
| import tkinter.messagebox | |
| from tkinter.simpledialog import SimpleDialog | |
| import numpy as np | |
| from PIL import Image, ImageTk | |
| import matplotlib as mpl | |
| from matplotlib import _api, backend_tools, cbook, _c_internal_utils | |
| from matplotlib.backend_bases import ( | |
| _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, | |
| TimerBase, ToolContainerBase, cursors, _Mode, MouseButton, | |
| CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) | |
| from matplotlib._pylab_helpers import Gcf | |
| from . import _tkagg | |
| from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET | |
| _log = logging.getLogger(__name__) | |
| cursord = { | |
| cursors.MOVE: "fleur", | |
| cursors.HAND: "hand2", | |
| cursors.POINTER: "arrow", | |
| cursors.SELECT_REGION: "crosshair", | |
| cursors.WAIT: "watch", | |
| cursors.RESIZE_HORIZONTAL: "sb_h_double_arrow", | |
| cursors.RESIZE_VERTICAL: "sb_v_double_arrow", | |
| } | |
| def _restore_foreground_window_at_end(): | |
| foreground = _c_internal_utils.Win32_GetForegroundWindow() | |
| try: | |
| yield | |
| finally: | |
| if foreground and mpl.rcParams['tk.window_focus']: | |
| _c_internal_utils.Win32_SetForegroundWindow(foreground) | |
| _blit_args = {} | |
| # Initialize to a non-empty string that is not a Tcl command | |
| _blit_tcl_name = "mpl_blit_" + uuid.uuid4().hex | |
| def _blit(argsid): | |
| """ | |
| Thin wrapper to blit called via tkapp.call. | |
| *argsid* is a unique string identifier to fetch the correct arguments from | |
| the ``_blit_args`` dict, since arguments cannot be passed directly. | |
| """ | |
| photoimage, data, offsets, bbox, comp_rule = _blit_args.pop(argsid) | |
| if not photoimage.tk.call("info", "commands", photoimage): | |
| return | |
| _tkagg.blit(photoimage.tk.interpaddr(), str(photoimage), data, comp_rule, offsets, | |
| bbox) | |
| def blit(photoimage, aggimage, offsets, bbox=None): | |
| """ | |
| Blit *aggimage* to *photoimage*. | |
| *offsets* is a tuple describing how to fill the ``offset`` field of the | |
| ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data, | |
| (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) | |
| for big-endian ARGB32 (i.e. ARGB8888) data. | |
| If *bbox* is passed, it defines the region that gets blitted. That region | |
| will be composed with the previous data according to the alpha channel. | |
| Blitting will be clipped to pixels inside the canvas, including silently | |
| doing nothing if the *bbox* region is entirely outside the canvas. | |
| Tcl events must be dispatched to trigger a blit from a non-Tcl thread. | |
| """ | |
| data = np.asarray(aggimage) | |
| height, width = data.shape[:2] | |
| if bbox is not None: | |
| (x1, y1), (x2, y2) = bbox.__array__() | |
| x1 = max(math.floor(x1), 0) | |
| x2 = min(math.ceil(x2), width) | |
| y1 = max(math.floor(y1), 0) | |
| y2 = min(math.ceil(y2), height) | |
| if (x1 > x2) or (y1 > y2): | |
| return | |
| bboxptr = (x1, x2, y1, y2) | |
| comp_rule = TK_PHOTO_COMPOSITE_OVERLAY | |
| else: | |
| bboxptr = (0, width, 0, height) | |
| comp_rule = TK_PHOTO_COMPOSITE_SET | |
| # NOTE: _tkagg.blit is thread unsafe and will crash the process if called | |
| # from a thread (GH#13293). Instead of blanking and blitting here, | |
| # use tkapp.call to post a cross-thread event if this function is called | |
| # from a non-Tcl thread. | |
| # tkapp.call coerces all arguments to strings, so to avoid string parsing | |
| # within _blit, pack up the arguments into a global data structure. | |
| args = photoimage, data, offsets, bboxptr, comp_rule | |
| # Need a unique key to avoid thread races. | |
| # Again, make the key a string to avoid string parsing in _blit. | |
| argsid = str(id(args)) | |
| _blit_args[argsid] = args | |
| try: | |
| photoimage.tk.call(_blit_tcl_name, argsid) | |
| except tk.TclError as e: | |
| if "invalid command name" not in str(e): | |
| raise | |
| photoimage.tk.createcommand(_blit_tcl_name, _blit) | |
| photoimage.tk.call(_blit_tcl_name, argsid) | |
| class TimerTk(TimerBase): | |
| """Subclass of `backend_bases.TimerBase` using Tk timer events.""" | |
| def __init__(self, parent, *args, **kwargs): | |
| self._timer = None | |
| super().__init__(*args, **kwargs) | |
| self.parent = parent | |
| 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): | |
| super()._on_timer() | |
| # 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: | |
| if self._interval > 0: | |
| self._timer = self.parent.after(self._interval, self._on_timer) | |
| else: | |
| # Edge case: Tcl after 0 *prepends* events to the queue | |
| # so a 0 interval does not allow any other events to run. | |
| # This incantation is cancellable and runs as fast as possible | |
| # while also allowing events and drawing every frame. GH#18236 | |
| self._timer = self.parent.after_idle( | |
| lambda: self.parent.after(self._interval, self._on_timer) | |
| ) | |
| else: | |
| self._timer = None | |
| class FigureCanvasTk(FigureCanvasBase): | |
| required_interactive_framework = "tk" | |
| manager_class = _api.classproperty(lambda cls: FigureManagerTk) | |
| def __init__(self, figure=None, master=None): | |
| super().__init__(figure) | |
| self._idle_draw_id = None | |
| self._event_loop_id = None | |
| w, h = self.get_width_height(physical=True) | |
| 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_image_region = self._tkcanvas.create_image( | |
| w//2, h//2, image=self._tkphoto) | |
| self._tkcanvas.bind("<Configure>", self.resize) | |
| self._tkcanvas.bind("<Map>", self._update_device_pixel_ratio) | |
| self._tkcanvas.bind("<Key>", self.key_press) | |
| self._tkcanvas.bind("<Motion>", self.motion_notify_event) | |
| self._tkcanvas.bind("<Enter>", self.enter_notify_event) | |
| self._tkcanvas.bind("<Leave>", self.leave_notify_event) | |
| self._tkcanvas.bind("<KeyRelease>", self.key_release) | |
| for name in ["<Button-1>", "<Button-2>", "<Button-3>"]: | |
| self._tkcanvas.bind(name, self.button_press_event) | |
| for name in [ | |
| "<Double-Button-1>", "<Double-Button-2>", "<Double-Button-3>"]: | |
| self._tkcanvas.bind(name, self.button_dblclick_event) | |
| for name in [ | |
| "<ButtonRelease-1>", "<ButtonRelease-2>", "<ButtonRelease-3>"]: | |
| self._tkcanvas.bind(name, self.button_release_event) | |
| # Mouse wheel on Linux generates button 4/5 events | |
| for name in "<Button-4>", "<Button-5>": | |
| 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 https://wiki.tcl-lang.org/3893 (mousewheel) for details | |
| root = self._tkcanvas.winfo_toplevel() | |
| # Prevent long-lived references via tkinter callback structure GH-24820 | |
| weakself = weakref.ref(self) | |
| weakroot = weakref.ref(root) | |
| def scroll_event_windows(event): | |
| self = weakself() | |
| if self is None: | |
| root = weakroot() | |
| if root is not None: | |
| root.unbind("<MouseWheel>", scroll_event_windows_id) | |
| return | |
| return self.scroll_event_windows(event) | |
| scroll_event_windows_id = root.bind("<MouseWheel>", scroll_event_windows, "+") | |
| # Can't get destroy events by binding to _tkcanvas. Therefore, bind | |
| # to the window and filter. | |
| def filter_destroy(event): | |
| self = weakself() | |
| if self is None: | |
| root = weakroot() | |
| if root is not None: | |
| root.unbind("<Destroy>", filter_destroy_id) | |
| return | |
| if event.widget is self._tkcanvas: | |
| CloseEvent("close_event", self)._process() | |
| filter_destroy_id = root.bind("<Destroy>", filter_destroy, "+") | |
| self._tkcanvas.focus_set() | |
| self._rubberband_rect_black = None | |
| self._rubberband_rect_white = None | |
| def _update_device_pixel_ratio(self, event=None): | |
| ratio = None | |
| if sys.platform == 'win32': | |
| # Tk gives scaling with respect to 72 DPI, but Windows screens are | |
| # scaled vs 96 dpi, and pixel ratio settings are given in whole | |
| # percentages, so round to 2 digits. | |
| ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) | |
| elif sys.platform == "linux": | |
| ratio = self._tkcanvas.winfo_fpixels('1i') / 96 | |
| if ratio is not None and self._set_device_pixel_ratio(ratio): | |
| # The easiest way to resize the canvas is to resize the canvas | |
| # widget itself, since we implement all the logic for resizing the | |
| # canvas backing store on that event. | |
| w, h = self.get_width_height(physical=True) | |
| self._tkcanvas.configure(width=w, height=h) | |
| def resize(self, event): | |
| width, height = event.width, event.height | |
| # 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._tkcanvas_image_region) | |
| self._tkphoto.configure(width=int(width), height=int(height)) | |
| self._tkcanvas_image_region = self._tkcanvas.create_image( | |
| int(width / 2), int(height / 2), image=self._tkphoto) | |
| ResizeEvent("resize_event", self)._process() | |
| self.draw_idle() | |
| def draw_idle(self): | |
| # docstring inherited | |
| if self._idle_draw_id: | |
| return | |
| def idle_draw(*args): | |
| try: | |
| self.draw() | |
| finally: | |
| self._idle_draw_id = None | |
| self._idle_draw_id = self._tkcanvas.after_idle(idle_draw) | |
| def get_tk_widget(self): | |
| """ | |
| Return 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 _event_mpl_coords(self, event): | |
| # calling canvasx/canvasy allows taking scrollbars into account (i.e. | |
| # the top of the widget may have been scrolled out of view). | |
| return (self._tkcanvas.canvasx(event.x), | |
| # flipy so y=0 is bottom of canvas | |
| self.figure.bbox.height - self._tkcanvas.canvasy(event.y)) | |
| def motion_notify_event(self, event): | |
| MouseEvent("motion_notify_event", self, | |
| *self._event_mpl_coords(event), | |
| buttons=self._mpl_buttons(event), | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def enter_notify_event(self, event): | |
| LocationEvent("figure_enter_event", self, | |
| *self._event_mpl_coords(event), | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def leave_notify_event(self, event): | |
| LocationEvent("figure_leave_event", self, | |
| *self._event_mpl_coords(event), | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def button_press_event(self, event, dblclick=False): | |
| # set focus to the canvas so that it can receive keyboard events | |
| self._tkcanvas.focus_set() | |
| num = getattr(event, 'num', None) | |
| if sys.platform == 'darwin': # 2 and 3 are reversed. | |
| num = {2: 3, 3: 2}.get(num, num) | |
| MouseEvent("button_press_event", self, | |
| *self._event_mpl_coords(event), num, dblclick=dblclick, | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def button_dblclick_event(self, event): | |
| self.button_press_event(event, dblclick=True) | |
| def button_release_event(self, event): | |
| num = getattr(event, 'num', None) | |
| if sys.platform == 'darwin': # 2 and 3 are reversed. | |
| num = {2: 3, 3: 2}.get(num, num) | |
| MouseEvent("button_release_event", self, | |
| *self._event_mpl_coords(event), num, | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def scroll_event(self, event): | |
| num = getattr(event, 'num', None) | |
| step = 1 if num == 4 else -1 if num == 5 else 0 | |
| MouseEvent("scroll_event", self, | |
| *self._event_mpl_coords(event), step=step, | |
| modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| 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: | |
| return | |
| x = self._tkcanvas.canvasx(event.x_root - w.winfo_rootx()) | |
| y = (self.figure.bbox.height | |
| - self._tkcanvas.canvasy(event.y_root - w.winfo_rooty())) | |
| step = event.delta / 120 | |
| MouseEvent("scroll_event", self, | |
| x, y, step=step, modifiers=self._mpl_modifiers(event), | |
| guiEvent=event)._process() | |
| def _mpl_buttons(event): # See _mpl_modifiers. | |
| # NOTE: This fails to report multiclicks on macOS; only one button is | |
| # reported (multiclicks work correctly on Linux & Windows). | |
| modifiers = [ | |
| # macOS appears to swap right and middle (look for "Swap buttons | |
| # 2/3" in tk/macosx/tkMacOSXMouseEvent.c). | |
| (MouseButton.LEFT, 1 << 8), | |
| (MouseButton.RIGHT, 1 << 9), | |
| (MouseButton.MIDDLE, 1 << 10), | |
| (MouseButton.BACK, 1 << 11), | |
| (MouseButton.FORWARD, 1 << 12), | |
| ] if sys.platform == "darwin" else [ | |
| (MouseButton.LEFT, 1 << 8), | |
| (MouseButton.MIDDLE, 1 << 9), | |
| (MouseButton.RIGHT, 1 << 10), | |
| (MouseButton.BACK, 1 << 11), | |
| (MouseButton.FORWARD, 1 << 12), | |
| ] | |
| # State *before* press/release. | |
| return [name for name, mask in modifiers if event.state & mask] | |
| def _mpl_modifiers(event, *, exclude=None): | |
| # Add modifier keys to the key string. Bit values are inferred from | |
| # the implementation of tkinter.Event.__repr__ (1, 2, 4, 8, ... = | |
| # Shift, Lock, Control, Mod1, ..., Mod5, Button1, ..., Button5) | |
| # 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. | |
| modifiers = [ | |
| ("ctrl", 1 << 2, "control"), | |
| ("alt", 1 << 17, "alt"), | |
| ("shift", 1 << 0, "shift"), | |
| ] if sys.platform == "win32" else [ | |
| ("ctrl", 1 << 2, "control"), | |
| ("alt", 1 << 4, "alt"), | |
| ("shift", 1 << 0, "shift"), | |
| ("cmd", 1 << 3, "cmd"), | |
| ] if sys.platform == "darwin" else [ | |
| ("ctrl", 1 << 2, "control"), | |
| ("alt", 1 << 3, "alt"), | |
| ("shift", 1 << 0, "shift"), | |
| ("super", 1 << 6, "super"), | |
| ] | |
| return [name for name, mask, key in modifiers | |
| if event.state & mask and exclude != key] | |
| def _get_key(self, event): | |
| unikey = event.char | |
| key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym) | |
| if key is not None: | |
| mods = self._mpl_modifiers(event, exclude=key) | |
| # shift is not added to the keys as this is already accounted for. | |
| if "shift" in mods and unikey: | |
| mods.remove("shift") | |
| return "+".join([*mods, key]) | |
| def key_press(self, event): | |
| KeyEvent("key_press_event", self, | |
| self._get_key(event), *self._event_mpl_coords(event), | |
| guiEvent=event)._process() | |
| def key_release(self, event): | |
| KeyEvent("key_release_event", self, | |
| self._get_key(event), *self._event_mpl_coords(event), | |
| guiEvent=event)._process() | |
| def new_timer(self, *args, **kwargs): | |
| # docstring inherited | |
| return TimerTk(self._tkcanvas, *args, **kwargs) | |
| def flush_events(self): | |
| # docstring inherited | |
| self._tkcanvas.update() | |
| def start_event_loop(self, timeout=0): | |
| # docstring inherited | |
| if timeout > 0: | |
| milliseconds = int(1000 * timeout) | |
| if milliseconds > 0: | |
| self._event_loop_id = self._tkcanvas.after( | |
| milliseconds, self.stop_event_loop) | |
| else: | |
| self._event_loop_id = self._tkcanvas.after_idle( | |
| self.stop_event_loop) | |
| self._tkcanvas.mainloop() | |
| def stop_event_loop(self): | |
| # docstring inherited | |
| if self._event_loop_id: | |
| self._tkcanvas.after_cancel(self._event_loop_id) | |
| self._event_loop_id = None | |
| self._tkcanvas.quit() | |
| def set_cursor(self, cursor): | |
| try: | |
| self._tkcanvas.configure(cursor=cursord[cursor]) | |
| except tkinter.TclError: | |
| pass | |
| 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 | |
| """ | |
| _owns_mainloop = False | |
| def __init__(self, canvas, num, window): | |
| self.window = window | |
| super().__init__(canvas, num) | |
| self.window.withdraw() | |
| # packing toolbar first, because if space is getting low, last packed | |
| # widget is getting shrunk first (-> the canvas) | |
| self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) | |
| # If the window has per-monitor DPI awareness, then setup a Tk variable | |
| # to store the DPI, which will be updated by the C code, and the trace | |
| # will handle it on the Python side. | |
| window_frame = int(window.wm_frame(), 16) | |
| self._window_dpi = tk.IntVar(master=window, value=96, | |
| name=f'window_dpi{window_frame}') | |
| self._window_dpi_cbname = '' | |
| if _tkagg.enable_dpi_awareness(window_frame, window.tk.interpaddr()): | |
| self._window_dpi_cbname = self._window_dpi.trace_add( | |
| 'write', self._update_window_dpi) | |
| self._shown = False | |
| def create_with_canvas(cls, canvas_class, figure, num): | |
| # docstring inherited | |
| with _restore_foreground_window_at_end(): | |
| if cbook._get_running_interactive_framework() is None: | |
| cbook._setup_new_guiapp() | |
| _c_internal_utils.Win32_SetProcessDpiAwareness_max() | |
| window = tk.Tk(className="matplotlib") | |
| window.withdraw() | |
| # Put a Matplotlib icon on the window rather than the default tk | |
| # icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50 | |
| # | |
| # `ImageTk` can be replaced with `tk` whenever the minimum | |
| # supported Tk version is increased to 8.6, as Tk 8.6+ natively | |
| # supports PNG images. | |
| icon_fname = str(cbook._get_data_path( | |
| 'images/matplotlib.png')) | |
| icon_img = ImageTk.PhotoImage(file=icon_fname, master=window) | |
| icon_fname_large = str(cbook._get_data_path( | |
| 'images/matplotlib_large.png')) | |
| icon_img_large = ImageTk.PhotoImage( | |
| file=icon_fname_large, master=window) | |
| window.iconphoto(False, icon_img_large, icon_img) | |
| canvas = canvas_class(figure, master=window) | |
| manager = cls(canvas, num, window) | |
| if mpl.is_interactive(): | |
| manager.show() | |
| canvas.draw_idle() | |
| return manager | |
| def start_main_loop(cls): | |
| managers = Gcf.get_all_fig_managers() | |
| if managers: | |
| first_manager = managers[0] | |
| manager_class = type(first_manager) | |
| if manager_class._owns_mainloop: | |
| return | |
| manager_class._owns_mainloop = True | |
| try: | |
| first_manager.window.mainloop() | |
| finally: | |
| manager_class._owns_mainloop = False | |
| def _update_window_dpi(self, *args): | |
| newdpi = self._window_dpi.get() | |
| self.window.call('tk', 'scaling', newdpi / 72) | |
| if self.toolbar and hasattr(self.toolbar, '_rescale'): | |
| self.toolbar._rescale() | |
| self.canvas._update_device_pixel_ratio() | |
| def resize(self, width, height): | |
| max_size = 1_400_000 # the measured max on xorg 1.20.8 was 1_409_023 | |
| if (width > max_size or height > max_size) and sys.platform == 'linux': | |
| raise ValueError( | |
| 'You have requested to resize the ' | |
| f'Tk window to ({width}, {height}), one of which ' | |
| f'is bigger than {max_size}. At larger sizes xorg will ' | |
| 'either exit with an error on newer versions (~1.20) or ' | |
| 'cause corruption on older version (~1.19). We ' | |
| 'do not expect a window over a million pixel wide or tall ' | |
| 'to be intended behavior.') | |
| self.canvas._tkcanvas.configure(width=width, height=height) | |
| def show(self): | |
| with _restore_foreground_window_at_end(): | |
| if not self._shown: | |
| def destroy(*args): | |
| Gcf.destroy(self) | |
| self.window.protocol("WM_DELETE_WINDOW", destroy) | |
| self.window.deiconify() | |
| self.canvas._tkcanvas.focus_set() | |
| else: | |
| self.canvas.draw_idle() | |
| if mpl.rcParams['figure.raise_window']: | |
| self.canvas.manager.window.attributes('-topmost', 1) | |
| self.canvas.manager.window.attributes('-topmost', 0) | |
| self._shown = True | |
| def destroy(self, *args): | |
| if self.canvas._idle_draw_id: | |
| self.canvas._tkcanvas.after_cancel(self.canvas._idle_draw_id) | |
| if self.canvas._event_loop_id: | |
| self.canvas._tkcanvas.after_cancel(self.canvas._event_loop_id) | |
| if self._window_dpi_cbname: | |
| self._window_dpi.trace_remove('write', self._window_dpi_cbname) | |
| # NOTE: events need to be flushed before issuing destroy (GH #9956), | |
| # however, self.window.update() can break user code. An async callback | |
| # is the safest way to achieve a complete draining of the event queue, | |
| # but it leaks if no tk event loop is running. Therefore we explicitly | |
| # check for an event loop and choose our best guess. | |
| def delayed_destroy(): | |
| self.window.destroy() | |
| if self._owns_mainloop and not Gcf.get_num_fig_managers(): | |
| self.window.quit() | |
| if cbook._get_running_interactive_framework() == "tk": | |
| # "after idle after 0" avoids Tcl error/race (GH #19940) | |
| self.window.after_idle(self.window.after, 0, delayed_destroy) | |
| else: | |
| self.window.update() | |
| delayed_destroy() | |
| 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) | |
| class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame): | |
| def __init__(self, canvas, window=None, *, pack_toolbar=True): | |
| """ | |
| Parameters | |
| ---------- | |
| canvas : `FigureCanvas` | |
| The figure canvas on which to operate. | |
| window : tk.Window | |
| The tk.Window which owns this toolbar. | |
| pack_toolbar : bool, default: True | |
| If True, add the toolbar to the parent's pack manager's packing | |
| list during initialization with ``side="bottom"`` and ``fill="x"``. | |
| If you want to use the toolbar with a different layout manager, use | |
| ``pack_toolbar=False``. | |
| """ | |
| if window is None: | |
| window = canvas.get_tk_widget().master | |
| tk.Frame.__init__(self, master=window, borderwidth=2, | |
| width=int(canvas.figure.bbox.width), height=50) | |
| self._buttons = {} | |
| for text, tooltip_text, image_file, callback in self.toolitems: | |
| if text is None: | |
| # Add a spacer; return value is unused. | |
| self._Spacer() | |
| else: | |
| self._buttons[text] = button = self._Button( | |
| text, | |
| str(cbook._get_data_path(f"images/{image_file}.png")), | |
| toggle=callback in ["zoom", "pan"], | |
| command=getattr(self, callback), | |
| ) | |
| if tooltip_text is not None: | |
| add_tooltip(button, tooltip_text) | |
| self._label_font = tkinter.font.Font(root=window, size=10) | |
| # This filler item ensures the toolbar is always at least two text | |
| # lines high. Otherwise the canvas gets redrawn as the mouse hovers | |
| # over images because those use two-line messages which resize the | |
| # toolbar. | |
| label = tk.Label(master=self, font=self._label_font, | |
| text='\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') | |
| label.pack(side=tk.RIGHT) | |
| self.message = tk.StringVar(master=self) | |
| self._message_label = tk.Label(master=self, font=self._label_font, | |
| textvariable=self.message, | |
| justify=tk.RIGHT) | |
| self._message_label.pack(side=tk.RIGHT) | |
| NavigationToolbar2.__init__(self, canvas) | |
| if pack_toolbar: | |
| self.pack(side=tk.BOTTOM, fill=tk.X) | |
| def _rescale(self): | |
| """ | |
| Scale all children of the toolbar to current DPI setting. | |
| Before this is called, the Tk scaling setting will have been updated to | |
| match the new DPI. Tk widgets do not update for changes to scaling, but | |
| all measurements made after the change will match the new scaling. Thus | |
| this function re-applies all the same sizes in points, which Tk will | |
| scale correctly to pixels. | |
| """ | |
| for widget in self.winfo_children(): | |
| if isinstance(widget, (tk.Button, tk.Checkbutton)): | |
| if hasattr(widget, '_image_file'): | |
| # Explicit class because ToolbarTk calls _rescale. | |
| NavigationToolbar2Tk._set_image_for_button(self, widget) | |
| else: | |
| # Text-only button is handled by the font setting instead. | |
| pass | |
| elif isinstance(widget, tk.Frame): | |
| widget.configure(height='18p') | |
| widget.pack_configure(padx='3p') | |
| elif isinstance(widget, tk.Label): | |
| pass # Text is handled by the font setting instead. | |
| else: | |
| _log.warning('Unknown child class %s', widget.winfo_class) | |
| self._label_font.configure(size=10) | |
| def _update_buttons_checked(self): | |
| # sync button checkstates to match active mode | |
| for text, mode in [('Zoom', _Mode.ZOOM), ('Pan', _Mode.PAN)]: | |
| if text in self._buttons: | |
| if self.mode == mode: | |
| self._buttons[text].select() # NOT .invoke() | |
| else: | |
| self._buttons[text].deselect() | |
| def pan(self, *args): | |
| super().pan(*args) | |
| self._update_buttons_checked() | |
| def zoom(self, *args): | |
| super().zoom(*args) | |
| self._update_buttons_checked() | |
| def set_message(self, s): | |
| self.message.set(s) | |
| def draw_rubberband(self, event, x0, y0, x1, y1): | |
| # Block copied from remove_rubberband for backend_tools convenience. | |
| if self.canvas._rubberband_rect_white: | |
| self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white) | |
| if self.canvas._rubberband_rect_black: | |
| self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black) | |
| height = self.canvas.figure.bbox.height | |
| y0 = height - y0 | |
| y1 = height - y1 | |
| self.canvas._rubberband_rect_black = ( | |
| self.canvas._tkcanvas.create_rectangle( | |
| x0, y0, x1, y1)) | |
| self.canvas._rubberband_rect_white = ( | |
| self.canvas._tkcanvas.create_rectangle( | |
| x0, y0, x1, y1, outline='white', dash=(3, 3))) | |
| def remove_rubberband(self): | |
| if self.canvas._rubberband_rect_white: | |
| self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white) | |
| self.canvas._rubberband_rect_white = None | |
| if self.canvas._rubberband_rect_black: | |
| self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black) | |
| self.canvas._rubberband_rect_black = None | |
| def _set_image_for_button(self, button): | |
| """ | |
| Set the image for a button based on its pixel size. | |
| The pixel size is determined by the DPI scaling of the window. | |
| """ | |
| if button._image_file is None: | |
| return | |
| # Allow _image_file to be relative to Matplotlib's "images" data | |
| # directory. | |
| path_regular = cbook._get_data_path('images', button._image_file) | |
| path_large = path_regular.with_name( | |
| path_regular.name.replace('.png', '_large.png')) | |
| size = button.winfo_pixels('18p') | |
| # Nested functions because ToolbarTk calls _Button. | |
| def _get_color(color_name): | |
| # `winfo_rgb` returns an (r, g, b) tuple in the range 0-65535 | |
| return button.winfo_rgb(button.cget(color_name)) | |
| def _is_dark(color): | |
| if isinstance(color, str): | |
| color = _get_color(color) | |
| return max(color) < 65535 / 2 | |
| def _recolor_icon(image, color): | |
| image_data = np.asarray(image).copy() | |
| black_mask = (image_data[..., :3] == 0).all(axis=-1) | |
| image_data[black_mask, :3] = color | |
| return Image.fromarray(image_data, mode="RGBA") | |
| # Use the high-resolution (48x48 px) icon if it exists and is needed | |
| with Image.open(path_large if (size > 24 and path_large.exists()) | |
| else path_regular) as im: | |
| # assure a RGBA image as foreground color is RGB | |
| im = im.convert("RGBA") | |
| image = ImageTk.PhotoImage(im.resize((size, size)), master=self) | |
| button._ntimage = image | |
| # create a version of the icon with the button's text color | |
| foreground = (255 / 65535) * np.array( | |
| button.winfo_rgb(button.cget("foreground"))) | |
| im_alt = _recolor_icon(im, foreground) | |
| image_alt = ImageTk.PhotoImage( | |
| im_alt.resize((size, size)), master=self) | |
| button._ntimage_alt = image_alt | |
| if _is_dark("background"): | |
| # For Checkbuttons, we need to set `image` and `selectimage` at | |
| # the same time. Otherwise, when updating the `image` option | |
| # (such as when changing DPI), if the old `selectimage` has | |
| # just been overwritten, Tk will throw an error. | |
| image_kwargs = {"image": image_alt} | |
| else: | |
| image_kwargs = {"image": image} | |
| # Checkbuttons may switch the background to `selectcolor` in the | |
| # checked state, so check separately which image it needs to use in | |
| # that state to still ensure enough contrast with the background. | |
| if ( | |
| isinstance(button, tk.Checkbutton) | |
| and button.cget("selectcolor") != "" | |
| ): | |
| if self._windowingsystem != "x11": | |
| selectcolor = "selectcolor" | |
| else: | |
| # On X11, selectcolor isn't used directly for indicator-less | |
| # buttons. See `::tk::CheckEnter` in the Tk button.tcl source | |
| # code for details. | |
| r1, g1, b1 = _get_color("selectcolor") | |
| r2, g2, b2 = _get_color("activebackground") | |
| selectcolor = ((r1+r2)/2, (g1+g2)/2, (b1+b2)/2) | |
| if _is_dark(selectcolor): | |
| image_kwargs["selectimage"] = image_alt | |
| else: | |
| image_kwargs["selectimage"] = image | |
| button.configure(**image_kwargs, height='18p', width='18p') | |
| def _Button(self, text, image_file, toggle, command): | |
| if not toggle: | |
| b = tk.Button( | |
| master=self, text=text, command=command, | |
| relief="flat", overrelief="groove", borderwidth=1, | |
| ) | |
| 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(master=self) | |
| b = tk.Checkbutton( | |
| master=self, text=text, command=command, indicatoron=False, | |
| variable=var, offrelief="flat", overrelief="groove", | |
| borderwidth=1 | |
| ) | |
| b.var = var | |
| b._image_file = image_file | |
| if image_file is not None: | |
| # Explicit class because ToolbarTk calls _Button. | |
| NavigationToolbar2Tk._set_image_for_button(self, b) | |
| else: | |
| b.configure(font=self._label_font) | |
| b.pack(side=tk.LEFT) | |
| return b | |
| def _Spacer(self): | |
| # Buttons are also 18pt high. | |
| s = tk.Frame(master=self, height='18p', relief=tk.RIDGE, bg='DarkGray') | |
| s.pack(side=tk.LEFT, padx='3p') | |
| return s | |
| def save_figure(self, *args): | |
| filetypes = self.canvas.get_supported_filetypes_grouped() | |
| tk_filetypes = [ | |
| (name, " ".join(f"*.{ext}" for ext in exts)) | |
| for name, exts in sorted(filetypes.items()) | |
| ] | |
| default_extension = self.canvas.get_default_filetype() | |
| default_filetype = self.canvas.get_supported_filetypes()[default_extension] | |
| filetype_variable = tk.StringVar(self.canvas.get_tk_widget(), default_filetype) | |
| # 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(mpl.rcParams['savefig.directory']) | |
| # get_default_filename() contains the default extension. On some platforms, | |
| # choosing a different extension from the dropdown does not overwrite it, | |
| # so we need to remove it to make the dropdown functional. | |
| initialfile = pathlib.Path(self.canvas.get_default_filename()).stem | |
| fname = tkinter.filedialog.asksaveasfilename( | |
| master=self.canvas.get_tk_widget().master, | |
| title='Save the figure', | |
| filetypes=tk_filetypes, | |
| defaultextension=defaultextension, | |
| initialdir=initialdir, | |
| initialfile=initialfile, | |
| typevariable=filetype_variable | |
| ) | |
| if fname in ["", ()]: | |
| return None | |
| # Save dir for next time, unless empty str (i.e., use cwd). | |
| if initialdir != "": | |
| mpl.rcParams['savefig.directory'] = ( | |
| os.path.dirname(str(fname))) | |
| # If the filename contains an extension, let savefig() infer the file | |
| # format from that. If it does not, use the selected dropdown option. | |
| if pathlib.Path(fname).suffix[1:] != "": | |
| extension = None | |
| else: | |
| extension = filetypes[filetype_variable.get()][0] | |
| try: | |
| self.canvas.figure.savefig(fname, format=extension) | |
| return fname | |
| except Exception as e: | |
| tkinter.messagebox.showerror("Error saving file", str(e)) | |
| def set_history_buttons(self): | |
| state_map = {True: tk.NORMAL, False: tk.DISABLED} | |
| can_back = self._nav_stack._pos > 0 | |
| can_forward = self._nav_stack._pos < len(self._nav_stack) - 1 | |
| if "Back" in self._buttons: | |
| self._buttons['Back']['state'] = state_map[can_back] | |
| if "Forward" in self._buttons: | |
| self._buttons['Forward']['state'] = state_map[can_forward] | |
| def add_tooltip(widget, text): | |
| tipwindow = None | |
| def showtip(event): | |
| """Display text in tooltip window.""" | |
| nonlocal tipwindow | |
| if tipwindow or not text: | |
| return | |
| x, y, _, _ = widget.bbox("insert") | |
| x = x + widget.winfo_rootx() + widget.winfo_width() | |
| y = y + widget.winfo_rooty() | |
| tipwindow = tk.Toplevel(widget) | |
| tipwindow.overrideredirect(1) | |
| tipwindow.geometry(f"+{x}+{y}") | |
| try: # For Mac OS | |
| tipwindow.tk.call("::tk::unsupported::MacWindowStyle", | |
| "style", tipwindow._w, | |
| "help", "noActivates") | |
| except tk.TclError: | |
| pass | |
| label = tk.Label(tipwindow, text=text, justify=tk.LEFT, | |
| relief=tk.SOLID, borderwidth=1) | |
| label.pack(ipadx=1) | |
| def hidetip(event): | |
| nonlocal tipwindow | |
| if tipwindow: | |
| tipwindow.destroy() | |
| tipwindow = None | |
| widget.bind("<Enter>", showtip) | |
| widget.bind("<Leave>", hidetip) | |
| class RubberbandTk(backend_tools.RubberbandBase): | |
| def draw_rubberband(self, x0, y0, x1, y1): | |
| NavigationToolbar2Tk.draw_rubberband( | |
| self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) | |
| def remove_rubberband(self): | |
| NavigationToolbar2Tk.remove_rubberband( | |
| self._make_classic_style_pseudo_toolbar()) | |
| class ToolbarTk(ToolContainerBase, tk.Frame): | |
| def __init__(self, toolmanager, window=None): | |
| ToolContainerBase.__init__(self, toolmanager) | |
| if window is None: | |
| window = self.toolmanager.canvas.get_tk_widget().master | |
| 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._label_font = tkinter.font.Font(size=10) | |
| # This filler item ensures the toolbar is always at least two text | |
| # lines high. Otherwise the canvas gets redrawn as the mouse hovers | |
| # over images because those use two-line messages which resize the | |
| # toolbar. | |
| label = tk.Label(master=self, font=self._label_font, | |
| text='\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') | |
| label.pack(side=tk.RIGHT) | |
| self._message = tk.StringVar(master=self) | |
| self._message_label = tk.Label(master=self, font=self._label_font, | |
| textvariable=self._message) | |
| self._message_label.pack(side=tk.RIGHT) | |
| self._toolitems = {} | |
| self.pack(side=tk.TOP, fill=tk.X) | |
| self._groups = {} | |
| def _rescale(self): | |
| return NavigationToolbar2Tk._rescale(self) | |
| def add_toolitem( | |
| self, name, group, position, image_file, description, toggle): | |
| frame = self._get_groupframe(group) | |
| buttons = frame.pack_slaves() | |
| if position >= len(buttons) or position < 0: | |
| before = None | |
| else: | |
| before = buttons[position] | |
| button = NavigationToolbar2Tk._Button(frame, name, image_file, toggle, | |
| lambda: self._button_click(name)) | |
| button.pack_configure(before=before) | |
| if description is not None: | |
| add_tooltip(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) | |
| frame._label_font = self._label_font | |
| self._groups[group] = frame | |
| return self._groups[group] | |
| def _add_separator(self): | |
| return NavigationToolbar2Tk._Spacer(self) | |
| 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.pop(name, []): | |
| toolitem.pack_forget() | |
| def set_message(self, s): | |
| self._message.set(s) | |
| class SaveFigureTk(backend_tools.SaveFigureBase): | |
| def trigger(self, *args): | |
| NavigationToolbar2Tk.save_figure( | |
| self._make_classic_style_pseudo_toolbar()) | |
| class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): | |
| def trigger(self, *args): | |
| NavigationToolbar2Tk.configure_subplots(self) | |
| class HelpTk(backend_tools.ToolHelpBase): | |
| def trigger(self, *args): | |
| dialog = SimpleDialog( | |
| self.figure.canvas._tkcanvas, self._get_help_text(), ["OK"]) | |
| dialog.done = lambda num: dialog.frame.master.withdraw() | |
| Toolbar = ToolbarTk | |
| FigureManagerTk._toolbar2_class = NavigationToolbar2Tk | |
| FigureManagerTk._toolmanager_toolbar_class = ToolbarTk | |
| class _BackendTk(_Backend): | |
| backend_version = tk.TkVersion | |
| FigureCanvas = FigureCanvasTk | |
| FigureManager = FigureManagerTk | |
| mainloop = FigureManagerTk.start_main_loop | |