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,
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
from matplotlib._pylab_helpers import Gcf
from . import _tkagg
_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",
}
@contextmanager
def _restore_foreground_window_at_end():
foreground = _c_internal_utils.Win32_GetForegroundWindow()
try:
yield
finally:
if 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
TK_PHOTO_COMPOSITE_OVERLAY = 0 # apply transparency rules pixel-wise
TK_PHOTO_COMPOSITE_SET = 1 # set image buffer directly
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, dataptr, offsets, bboxptr, comp_rule = _blit_args.pop(argsid)
if not photoimage.tk.call("info", "commands", photoimage):
return
_tkagg.blit(photoimage.tk.interpaddr(), str(photoimage), dataptr,
comp_rule, offsets, bboxptr)
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]
dataptr = (height, width, data.ctypes.data)
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, dataptr, 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("", self.resize)
if sys.platform == 'win32':
self._tkcanvas.bind("