backend_wx.py 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  1. """
  2. A wxPython backend for matplotlib.
  3. Originally contributed by Jeremy O'Donoghue (jeremy@o-donoghue.com) and John
  4. Hunter (jdhunter@ace.bsd.uchicago.edu).
  5. Copyright (C) Jeremy O'Donoghue & John Hunter, 2003-4.
  6. """
  7. import functools
  8. import logging
  9. import math
  10. import pathlib
  11. import sys
  12. import weakref
  13. import numpy as np
  14. import PIL.Image
  15. import matplotlib as mpl
  16. from matplotlib.backend_bases import (
  17. _Backend, FigureCanvasBase, FigureManagerBase,
  18. GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase,
  19. TimerBase, ToolContainerBase, cursors,
  20. CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
  21. from matplotlib import _api, cbook, backend_tools, _c_internal_utils
  22. from matplotlib._pylab_helpers import Gcf
  23. from matplotlib.path import Path
  24. from matplotlib.transforms import Affine2D
  25. import wx
  26. import wx.svg # noqa: F401
  27. _log = logging.getLogger(__name__)
  28. # the True dots per inch on the screen; should be display dependent; see
  29. # http://groups.google.com/d/msg/comp.lang.postscript/-/omHAc9FEuAsJ?hl=en
  30. # for some info about screen dpi
  31. PIXELS_PER_INCH = 75
  32. # lru_cache holds a reference to the App and prevents it from being gc'ed.
  33. @functools.lru_cache(1)
  34. def _create_wxapp():
  35. wxapp = wx.App(False)
  36. wxapp.SetExitOnFrameDelete(True)
  37. cbook._setup_new_guiapp()
  38. # Set per-process DPI awareness. This is a NoOp except in MSW
  39. _c_internal_utils.Win32_SetProcessDpiAwareness_max()
  40. return wxapp
  41. class TimerWx(TimerBase):
  42. """Subclass of `.TimerBase` using wx.Timer events."""
  43. def __init__(self, *args, **kwargs):
  44. self._timer = wx.Timer()
  45. self._timer.Notify = self._on_timer
  46. super().__init__(*args, **kwargs)
  47. def _timer_start(self):
  48. self._timer.Start(self._interval, self._single)
  49. def _timer_stop(self):
  50. self._timer.Stop()
  51. def _timer_set_interval(self):
  52. if self._timer.IsRunning():
  53. self._timer_start() # Restart with new interval.
  54. @_api.deprecated(
  55. "2.0", name="wx", obj_type="backend", removal="the future",
  56. alternative="wxagg",
  57. addendum="See the Matplotlib usage FAQ for more info on backends.")
  58. class RendererWx(RendererBase):
  59. """
  60. The renderer handles all the drawing primitives using a graphics
  61. context instance that controls the colors/styles. It acts as the
  62. 'renderer' instance used by many classes in the hierarchy.
  63. """
  64. # In wxPython, drawing is performed on a wxDC instance, which will
  65. # generally be mapped to the client area of the window displaying
  66. # the plot. Under wxPython, the wxDC instance has a wx.Pen which
  67. # describes the colour and weight of any lines drawn, and a wxBrush
  68. # which describes the fill colour of any closed polygon.
  69. # Font styles, families and weight.
  70. fontweights = {
  71. 100: wx.FONTWEIGHT_LIGHT,
  72. 200: wx.FONTWEIGHT_LIGHT,
  73. 300: wx.FONTWEIGHT_LIGHT,
  74. 400: wx.FONTWEIGHT_NORMAL,
  75. 500: wx.FONTWEIGHT_NORMAL,
  76. 600: wx.FONTWEIGHT_NORMAL,
  77. 700: wx.FONTWEIGHT_BOLD,
  78. 800: wx.FONTWEIGHT_BOLD,
  79. 900: wx.FONTWEIGHT_BOLD,
  80. 'ultralight': wx.FONTWEIGHT_LIGHT,
  81. 'light': wx.FONTWEIGHT_LIGHT,
  82. 'normal': wx.FONTWEIGHT_NORMAL,
  83. 'medium': wx.FONTWEIGHT_NORMAL,
  84. 'semibold': wx.FONTWEIGHT_NORMAL,
  85. 'bold': wx.FONTWEIGHT_BOLD,
  86. 'heavy': wx.FONTWEIGHT_BOLD,
  87. 'ultrabold': wx.FONTWEIGHT_BOLD,
  88. 'black': wx.FONTWEIGHT_BOLD,
  89. }
  90. fontangles = {
  91. 'italic': wx.FONTSTYLE_ITALIC,
  92. 'normal': wx.FONTSTYLE_NORMAL,
  93. 'oblique': wx.FONTSTYLE_SLANT,
  94. }
  95. # wxPython allows for portable font styles, choosing them appropriately for
  96. # the target platform. Map some standard font names to the portable styles.
  97. # QUESTION: Is it wise to agree to standard fontnames across all backends?
  98. fontnames = {
  99. 'Sans': wx.FONTFAMILY_SWISS,
  100. 'Roman': wx.FONTFAMILY_ROMAN,
  101. 'Script': wx.FONTFAMILY_SCRIPT,
  102. 'Decorative': wx.FONTFAMILY_DECORATIVE,
  103. 'Modern': wx.FONTFAMILY_MODERN,
  104. 'Courier': wx.FONTFAMILY_MODERN,
  105. 'courier': wx.FONTFAMILY_MODERN,
  106. }
  107. def __init__(self, bitmap, dpi):
  108. """Initialise a wxWindows renderer instance."""
  109. super().__init__()
  110. _log.debug("%s - __init__()", type(self))
  111. self.width = bitmap.GetWidth()
  112. self.height = bitmap.GetHeight()
  113. self.bitmap = bitmap
  114. self.fontd = {}
  115. self.dpi = dpi
  116. self.gc = None
  117. def flipy(self):
  118. # docstring inherited
  119. return True
  120. def get_text_width_height_descent(self, s, prop, ismath):
  121. # docstring inherited
  122. if ismath:
  123. s = cbook.strip_math(s)
  124. if self.gc is None:
  125. gc = self.new_gc()
  126. else:
  127. gc = self.gc
  128. gfx_ctx = gc.gfx_ctx
  129. font = self.get_wx_font(s, prop)
  130. gfx_ctx.SetFont(font, wx.BLACK)
  131. w, h, descent, leading = gfx_ctx.GetFullTextExtent(s)
  132. return w, h, descent
  133. def get_canvas_width_height(self):
  134. # docstring inherited
  135. return self.width, self.height
  136. def handle_clip_rectangle(self, gc):
  137. new_bounds = gc.get_clip_rectangle()
  138. if new_bounds is not None:
  139. new_bounds = new_bounds.bounds
  140. gfx_ctx = gc.gfx_ctx
  141. if gfx_ctx._lastcliprect != new_bounds:
  142. gfx_ctx._lastcliprect = new_bounds
  143. if new_bounds is None:
  144. gfx_ctx.ResetClip()
  145. else:
  146. gfx_ctx.Clip(new_bounds[0],
  147. self.height - new_bounds[1] - new_bounds[3],
  148. new_bounds[2], new_bounds[3])
  149. @staticmethod
  150. def convert_path(gfx_ctx, path, transform):
  151. wxpath = gfx_ctx.CreatePath()
  152. for points, code in path.iter_segments(transform):
  153. if code == Path.MOVETO:
  154. wxpath.MoveToPoint(*points)
  155. elif code == Path.LINETO:
  156. wxpath.AddLineToPoint(*points)
  157. elif code == Path.CURVE3:
  158. wxpath.AddQuadCurveToPoint(*points)
  159. elif code == Path.CURVE4:
  160. wxpath.AddCurveToPoint(*points)
  161. elif code == Path.CLOSEPOLY:
  162. wxpath.CloseSubpath()
  163. return wxpath
  164. def draw_path(self, gc, path, transform, rgbFace=None):
  165. # docstring inherited
  166. gc.select()
  167. self.handle_clip_rectangle(gc)
  168. gfx_ctx = gc.gfx_ctx
  169. transform = transform + \
  170. Affine2D().scale(1.0, -1.0).translate(0.0, self.height)
  171. wxpath = self.convert_path(gfx_ctx, path, transform)
  172. if rgbFace is not None:
  173. gfx_ctx.SetBrush(wx.Brush(gc.get_wxcolour(rgbFace)))
  174. gfx_ctx.DrawPath(wxpath)
  175. else:
  176. gfx_ctx.StrokePath(wxpath)
  177. gc.unselect()
  178. def draw_image(self, gc, x, y, im):
  179. bbox = gc.get_clip_rectangle()
  180. if bbox is not None:
  181. l, b, w, h = bbox.bounds
  182. else:
  183. l = 0
  184. b = 0
  185. w = self.width
  186. h = self.height
  187. rows, cols = im.shape[:2]
  188. bitmap = wx.Bitmap.FromBufferRGBA(cols, rows, im.tobytes())
  189. gc.select()
  190. gc.gfx_ctx.DrawBitmap(bitmap, int(l), int(self.height - b),
  191. int(w), int(-h))
  192. gc.unselect()
  193. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  194. # docstring inherited
  195. if ismath:
  196. s = cbook.strip_math(s)
  197. _log.debug("%s - draw_text()", type(self))
  198. gc.select()
  199. self.handle_clip_rectangle(gc)
  200. gfx_ctx = gc.gfx_ctx
  201. font = self.get_wx_font(s, prop)
  202. color = gc.get_wxcolour(gc.get_rgb())
  203. gfx_ctx.SetFont(font, color)
  204. w, h, d = self.get_text_width_height_descent(s, prop, ismath)
  205. x = int(x)
  206. y = int(y - h)
  207. if angle == 0.0:
  208. gfx_ctx.DrawText(s, x, y)
  209. else:
  210. rads = math.radians(angle)
  211. xo = h * math.sin(rads)
  212. yo = h * math.cos(rads)
  213. gfx_ctx.DrawRotatedText(s, x - xo, y - yo, rads)
  214. gc.unselect()
  215. def new_gc(self):
  216. # docstring inherited
  217. _log.debug("%s - new_gc()", type(self))
  218. self.gc = GraphicsContextWx(self.bitmap, self)
  219. self.gc.select()
  220. self.gc.unselect()
  221. return self.gc
  222. def get_wx_font(self, s, prop):
  223. """Return a wx font. Cache font instances for efficiency."""
  224. _log.debug("%s - get_wx_font()", type(self))
  225. key = hash(prop)
  226. font = self.fontd.get(key)
  227. if font is not None:
  228. return font
  229. size = self.points_to_pixels(prop.get_size_in_points())
  230. # Font colour is determined by the active wx.Pen
  231. # TODO: It may be wise to cache font information
  232. self.fontd[key] = font = wx.Font( # Cache the font and gc.
  233. pointSize=round(size),
  234. family=self.fontnames.get(prop.get_name(), wx.ROMAN),
  235. style=self.fontangles[prop.get_style()],
  236. weight=self.fontweights[prop.get_weight()])
  237. return font
  238. def points_to_pixels(self, points):
  239. # docstring inherited
  240. return points * (PIXELS_PER_INCH / 72.0 * self.dpi / 72.0)
  241. class GraphicsContextWx(GraphicsContextBase):
  242. """
  243. The graphics context provides the color, line styles, etc.
  244. This class stores a reference to a wxMemoryDC, and a
  245. wxGraphicsContext that draws to it. Creating a wxGraphicsContext
  246. seems to be fairly heavy, so these objects are cached based on the
  247. bitmap object that is passed in.
  248. The base GraphicsContext stores colors as an RGB tuple on the unit
  249. interval, e.g., (0.5, 0.0, 1.0). wxPython uses an int interval, but
  250. since wxPython colour management is rather simple, I have not chosen
  251. to implement a separate colour manager class.
  252. """
  253. _capd = {'butt': wx.CAP_BUTT,
  254. 'projecting': wx.CAP_PROJECTING,
  255. 'round': wx.CAP_ROUND}
  256. _joind = {'bevel': wx.JOIN_BEVEL,
  257. 'miter': wx.JOIN_MITER,
  258. 'round': wx.JOIN_ROUND}
  259. _cache = weakref.WeakKeyDictionary()
  260. def __init__(self, bitmap, renderer):
  261. super().__init__()
  262. # assert self.Ok(), "wxMemoryDC not OK to use"
  263. _log.debug("%s - __init__(): %s", type(self), bitmap)
  264. dc, gfx_ctx = self._cache.get(bitmap, (None, None))
  265. if dc is None:
  266. dc = wx.MemoryDC(bitmap)
  267. gfx_ctx = wx.GraphicsContext.Create(dc)
  268. gfx_ctx._lastcliprect = None
  269. self._cache[bitmap] = dc, gfx_ctx
  270. self.bitmap = bitmap
  271. self.dc = dc
  272. self.gfx_ctx = gfx_ctx
  273. self._pen = wx.Pen('BLACK', 1, wx.SOLID)
  274. gfx_ctx.SetPen(self._pen)
  275. self.renderer = renderer
  276. def select(self):
  277. """Select the current bitmap into this wxDC instance."""
  278. if sys.platform == 'win32':
  279. self.dc.SelectObject(self.bitmap)
  280. self.IsSelected = True
  281. def unselect(self):
  282. """Select a Null bitmap into this wxDC instance."""
  283. if sys.platform == 'win32':
  284. self.dc.SelectObject(wx.NullBitmap)
  285. self.IsSelected = False
  286. def set_foreground(self, fg, isRGBA=None):
  287. # docstring inherited
  288. # Implementation note: wxPython has a separate concept of pen and
  289. # brush - the brush fills any outline trace left by the pen.
  290. # Here we set both to the same colour - if a figure is not to be
  291. # filled, the renderer will set the brush to be transparent
  292. # Same goes for text foreground...
  293. _log.debug("%s - set_foreground()", type(self))
  294. self.select()
  295. super().set_foreground(fg, isRGBA)
  296. self._pen.SetColour(self.get_wxcolour(self.get_rgb()))
  297. self.gfx_ctx.SetPen(self._pen)
  298. self.unselect()
  299. def set_linewidth(self, w):
  300. # docstring inherited
  301. w = float(w)
  302. _log.debug("%s - set_linewidth()", type(self))
  303. self.select()
  304. if 0 < w < 1:
  305. w = 1
  306. super().set_linewidth(w)
  307. lw = int(self.renderer.points_to_pixels(self._linewidth))
  308. if lw == 0:
  309. lw = 1
  310. self._pen.SetWidth(lw)
  311. self.gfx_ctx.SetPen(self._pen)
  312. self.unselect()
  313. def set_capstyle(self, cs):
  314. # docstring inherited
  315. _log.debug("%s - set_capstyle()", type(self))
  316. self.select()
  317. super().set_capstyle(cs)
  318. self._pen.SetCap(GraphicsContextWx._capd[self._capstyle])
  319. self.gfx_ctx.SetPen(self._pen)
  320. self.unselect()
  321. def set_joinstyle(self, js):
  322. # docstring inherited
  323. _log.debug("%s - set_joinstyle()", type(self))
  324. self.select()
  325. super().set_joinstyle(js)
  326. self._pen.SetJoin(GraphicsContextWx._joind[self._joinstyle])
  327. self.gfx_ctx.SetPen(self._pen)
  328. self.unselect()
  329. def get_wxcolour(self, color):
  330. """Convert an RGB(A) color to a wx.Colour."""
  331. _log.debug("%s - get_wx_color()", type(self))
  332. return wx.Colour(*[int(255 * x) for x in color])
  333. class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel):
  334. """
  335. The FigureCanvas contains the figure and does event handling.
  336. In the wxPython backend, it is derived from wxPanel, and (usually) lives
  337. inside a frame instantiated by a FigureManagerWx. The parent window
  338. probably implements a wx.Sizer to control the displayed control size - but
  339. we give a hint as to our preferred minimum size.
  340. """
  341. required_interactive_framework = "wx"
  342. _timer_cls = TimerWx
  343. manager_class = _api.classproperty(lambda cls: FigureManagerWx)
  344. keyvald = {
  345. wx.WXK_CONTROL: 'control',
  346. wx.WXK_SHIFT: 'shift',
  347. wx.WXK_ALT: 'alt',
  348. wx.WXK_CAPITAL: 'caps_lock',
  349. wx.WXK_LEFT: 'left',
  350. wx.WXK_UP: 'up',
  351. wx.WXK_RIGHT: 'right',
  352. wx.WXK_DOWN: 'down',
  353. wx.WXK_ESCAPE: 'escape',
  354. wx.WXK_F1: 'f1',
  355. wx.WXK_F2: 'f2',
  356. wx.WXK_F3: 'f3',
  357. wx.WXK_F4: 'f4',
  358. wx.WXK_F5: 'f5',
  359. wx.WXK_F6: 'f6',
  360. wx.WXK_F7: 'f7',
  361. wx.WXK_F8: 'f8',
  362. wx.WXK_F9: 'f9',
  363. wx.WXK_F10: 'f10',
  364. wx.WXK_F11: 'f11',
  365. wx.WXK_F12: 'f12',
  366. wx.WXK_SCROLL: 'scroll_lock',
  367. wx.WXK_PAUSE: 'break',
  368. wx.WXK_BACK: 'backspace',
  369. wx.WXK_RETURN: 'enter',
  370. wx.WXK_INSERT: 'insert',
  371. wx.WXK_DELETE: 'delete',
  372. wx.WXK_HOME: 'home',
  373. wx.WXK_END: 'end',
  374. wx.WXK_PAGEUP: 'pageup',
  375. wx.WXK_PAGEDOWN: 'pagedown',
  376. wx.WXK_NUMPAD0: '0',
  377. wx.WXK_NUMPAD1: '1',
  378. wx.WXK_NUMPAD2: '2',
  379. wx.WXK_NUMPAD3: '3',
  380. wx.WXK_NUMPAD4: '4',
  381. wx.WXK_NUMPAD5: '5',
  382. wx.WXK_NUMPAD6: '6',
  383. wx.WXK_NUMPAD7: '7',
  384. wx.WXK_NUMPAD8: '8',
  385. wx.WXK_NUMPAD9: '9',
  386. wx.WXK_NUMPAD_ADD: '+',
  387. wx.WXK_NUMPAD_SUBTRACT: '-',
  388. wx.WXK_NUMPAD_MULTIPLY: '*',
  389. wx.WXK_NUMPAD_DIVIDE: '/',
  390. wx.WXK_NUMPAD_DECIMAL: 'dec',
  391. wx.WXK_NUMPAD_ENTER: 'enter',
  392. wx.WXK_NUMPAD_UP: 'up',
  393. wx.WXK_NUMPAD_RIGHT: 'right',
  394. wx.WXK_NUMPAD_DOWN: 'down',
  395. wx.WXK_NUMPAD_LEFT: 'left',
  396. wx.WXK_NUMPAD_PAGEUP: 'pageup',
  397. wx.WXK_NUMPAD_PAGEDOWN: 'pagedown',
  398. wx.WXK_NUMPAD_HOME: 'home',
  399. wx.WXK_NUMPAD_END: 'end',
  400. wx.WXK_NUMPAD_INSERT: 'insert',
  401. wx.WXK_NUMPAD_DELETE: 'delete',
  402. }
  403. def __init__(self, parent, id, figure=None):
  404. """
  405. Initialize a FigureWx instance.
  406. - Initialize the FigureCanvasBase and wxPanel parents.
  407. - Set event handlers for resize, paint, and keyboard and mouse
  408. interaction.
  409. """
  410. FigureCanvasBase.__init__(self, figure)
  411. size = wx.Size(*map(math.ceil, self.figure.bbox.size))
  412. if wx.Platform != '__WXMSW__':
  413. size = parent.FromDIP(size)
  414. # Set preferred window size hint - helps the sizer, if one is connected
  415. wx.Panel.__init__(self, parent, id, size=size)
  416. self.bitmap = None
  417. self._isDrawn = False
  418. self._rubberband_rect = None
  419. self._rubberband_pen_black = wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH)
  420. self._rubberband_pen_white = wx.Pen('WHITE', 1, wx.PENSTYLE_SOLID)
  421. self.Bind(wx.EVT_SIZE, self._on_size)
  422. self.Bind(wx.EVT_PAINT, self._on_paint)
  423. self.Bind(wx.EVT_CHAR_HOOK, self._on_key_down)
  424. self.Bind(wx.EVT_KEY_UP, self._on_key_up)
  425. self.Bind(wx.EVT_LEFT_DOWN, self._on_mouse_button)
  426. self.Bind(wx.EVT_LEFT_DCLICK, self._on_mouse_button)
  427. self.Bind(wx.EVT_LEFT_UP, self._on_mouse_button)
  428. self.Bind(wx.EVT_MIDDLE_DOWN, self._on_mouse_button)
  429. self.Bind(wx.EVT_MIDDLE_DCLICK, self._on_mouse_button)
  430. self.Bind(wx.EVT_MIDDLE_UP, self._on_mouse_button)
  431. self.Bind(wx.EVT_RIGHT_DOWN, self._on_mouse_button)
  432. self.Bind(wx.EVT_RIGHT_DCLICK, self._on_mouse_button)
  433. self.Bind(wx.EVT_RIGHT_UP, self._on_mouse_button)
  434. self.Bind(wx.EVT_MOUSE_AUX1_DOWN, self._on_mouse_button)
  435. self.Bind(wx.EVT_MOUSE_AUX1_UP, self._on_mouse_button)
  436. self.Bind(wx.EVT_MOUSE_AUX2_DOWN, self._on_mouse_button)
  437. self.Bind(wx.EVT_MOUSE_AUX2_UP, self._on_mouse_button)
  438. self.Bind(wx.EVT_MOUSE_AUX1_DCLICK, self._on_mouse_button)
  439. self.Bind(wx.EVT_MOUSE_AUX2_DCLICK, self._on_mouse_button)
  440. self.Bind(wx.EVT_MOUSEWHEEL, self._on_mouse_wheel)
  441. self.Bind(wx.EVT_MOTION, self._on_motion)
  442. self.Bind(wx.EVT_ENTER_WINDOW, self._on_enter)
  443. self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave)
  444. self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._on_capture_lost)
  445. self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._on_capture_lost)
  446. self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker.
  447. self.SetBackgroundColour(wx.WHITE)
  448. if wx.Platform == '__WXMAC__':
  449. # Initial scaling. Other platforms handle this automatically
  450. dpiScale = self.GetDPIScaleFactor()
  451. self.SetInitialSize(self.GetSize()*(1/dpiScale))
  452. self._set_device_pixel_ratio(dpiScale)
  453. def Copy_to_Clipboard(self, event=None):
  454. """Copy bitmap of canvas to system clipboard."""
  455. bmp_obj = wx.BitmapDataObject()
  456. bmp_obj.SetBitmap(self.bitmap)
  457. if not wx.TheClipboard.IsOpened():
  458. open_success = wx.TheClipboard.Open()
  459. if open_success:
  460. wx.TheClipboard.SetData(bmp_obj)
  461. wx.TheClipboard.Flush()
  462. wx.TheClipboard.Close()
  463. def _update_device_pixel_ratio(self, *args, **kwargs):
  464. # We need to be careful in cases with mixed resolution displays if
  465. # device_pixel_ratio changes.
  466. if self._set_device_pixel_ratio(self.GetDPIScaleFactor()):
  467. self.draw()
  468. def draw_idle(self):
  469. # docstring inherited
  470. _log.debug("%s - draw_idle()", type(self))
  471. self._isDrawn = False # Force redraw
  472. # Triggering a paint event is all that is needed to defer drawing
  473. # until later. The platform will send the event when it thinks it is
  474. # a good time (usually as soon as there are no other events pending).
  475. self.Refresh(eraseBackground=False)
  476. def flush_events(self):
  477. # docstring inherited
  478. wx.Yield()
  479. def start_event_loop(self, timeout=0):
  480. # docstring inherited
  481. if hasattr(self, '_event_loop'):
  482. raise RuntimeError("Event loop already running")
  483. timer = wx.Timer(self, id=wx.ID_ANY)
  484. if timeout > 0:
  485. timer.Start(int(timeout * 1000), oneShot=True)
  486. self.Bind(wx.EVT_TIMER, self.stop_event_loop, id=timer.GetId())
  487. # Event loop handler for start/stop event loop
  488. self._event_loop = wx.GUIEventLoop()
  489. self._event_loop.Run()
  490. timer.Stop()
  491. def stop_event_loop(self, event=None):
  492. # docstring inherited
  493. if hasattr(self, '_event_loop'):
  494. if self._event_loop.IsRunning():
  495. self._event_loop.Exit()
  496. del self._event_loop
  497. def _get_imagesave_wildcards(self):
  498. """Return the wildcard string for the filesave dialog."""
  499. default_filetype = self.get_default_filetype()
  500. filetypes = self.get_supported_filetypes_grouped()
  501. sorted_filetypes = sorted(filetypes.items())
  502. wildcards = []
  503. extensions = []
  504. filter_index = 0
  505. for i, (name, exts) in enumerate(sorted_filetypes):
  506. ext_list = ';'.join(['*.%s' % ext for ext in exts])
  507. extensions.append(exts[0])
  508. wildcard = f'{name} ({ext_list})|{ext_list}'
  509. if default_filetype in exts:
  510. filter_index = i
  511. wildcards.append(wildcard)
  512. wildcards = '|'.join(wildcards)
  513. return wildcards, extensions, filter_index
  514. def gui_repaint(self, drawDC=None):
  515. """
  516. Update the displayed image on the GUI canvas, using the supplied
  517. wx.PaintDC device context.
  518. """
  519. _log.debug("%s - gui_repaint()", type(self))
  520. # The "if self" check avoids a "wrapped C/C++ object has been deleted"
  521. # RuntimeError if doing things after window is closed.
  522. if not (self and self.IsShownOnScreen()):
  523. return
  524. if not drawDC: # not called from OnPaint use a ClientDC
  525. drawDC = wx.ClientDC(self)
  526. # For 'WX' backend on Windows, the bitmap cannot be in use by another
  527. # DC (see GraphicsContextWx._cache).
  528. bmp = (self.bitmap.ConvertToImage().ConvertToBitmap()
  529. if wx.Platform == '__WXMSW__'
  530. and isinstance(self.figure.canvas.get_renderer(), RendererWx)
  531. else self.bitmap)
  532. drawDC.DrawBitmap(bmp, 0, 0)
  533. if self._rubberband_rect is not None:
  534. # Some versions of wx+python don't support numpy.float64 here.
  535. x0, y0, x1, y1 = map(round, self._rubberband_rect)
  536. rect = [(x0, y0, x1, y0), (x1, y0, x1, y1),
  537. (x0, y0, x0, y1), (x0, y1, x1, y1)]
  538. drawDC.DrawLineList(rect, self._rubberband_pen_white)
  539. drawDC.DrawLineList(rect, self._rubberband_pen_black)
  540. filetypes = {
  541. **FigureCanvasBase.filetypes,
  542. 'bmp': 'Windows bitmap',
  543. 'jpeg': 'JPEG',
  544. 'jpg': 'JPEG',
  545. 'pcx': 'PCX',
  546. 'png': 'Portable Network Graphics',
  547. 'tif': 'Tagged Image Format File',
  548. 'tiff': 'Tagged Image Format File',
  549. 'xpm': 'X pixmap',
  550. }
  551. def _on_paint(self, event):
  552. """Called when wxPaintEvt is generated."""
  553. _log.debug("%s - _on_paint()", type(self))
  554. drawDC = wx.PaintDC(self)
  555. if not self._isDrawn:
  556. self.draw(drawDC=drawDC)
  557. else:
  558. self.gui_repaint(drawDC=drawDC)
  559. drawDC.Destroy()
  560. def _on_size(self, event):
  561. """
  562. Called when wxEventSize is generated.
  563. In this application we attempt to resize to fit the window, so it
  564. is better to take the performance hit and redraw the whole window.
  565. """
  566. self._update_device_pixel_ratio()
  567. _log.debug("%s - _on_size()", type(self))
  568. sz = self.GetParent().GetSizer()
  569. if sz:
  570. si = sz.GetItem(self)
  571. if sz and si and not si.Proportion and not si.Flag & wx.EXPAND:
  572. # managed by a sizer, but with a fixed size
  573. size = self.GetMinSize()
  574. else:
  575. # variable size
  576. size = self.GetClientSize()
  577. # Do not allow size to become smaller than MinSize
  578. size.IncTo(self.GetMinSize())
  579. if getattr(self, "_width", None):
  580. if size == (self._width, self._height):
  581. # no change in size
  582. return
  583. self._width, self._height = size
  584. self._isDrawn = False
  585. if self._width <= 1 or self._height <= 1:
  586. return # Empty figure
  587. # Create a new, correctly sized bitmap
  588. dpival = self.figure.dpi
  589. if not wx.Platform == '__WXMSW__':
  590. scale = self.GetDPIScaleFactor()
  591. dpival /= scale
  592. winch = self._width / dpival
  593. hinch = self._height / dpival
  594. self.figure.set_size_inches(winch, hinch, forward=False)
  595. # Rendering will happen on the associated paint event
  596. # so no need to do anything here except to make sure
  597. # the whole background is repainted.
  598. self.Refresh(eraseBackground=False)
  599. ResizeEvent("resize_event", self)._process()
  600. self.draw_idle()
  601. @staticmethod
  602. def _mpl_buttons():
  603. state = wx.GetMouseState()
  604. # NOTE: Alternatively, we could use event.LeftIsDown() / etc. but this
  605. # fails to report multiclick drags on macOS (other OSes have not been
  606. # verified).
  607. mod_table = [
  608. (MouseButton.LEFT, state.LeftIsDown()),
  609. (MouseButton.RIGHT, state.RightIsDown()),
  610. (MouseButton.MIDDLE, state.MiddleIsDown()),
  611. (MouseButton.BACK, state.Aux1IsDown()),
  612. (MouseButton.FORWARD, state.Aux2IsDown()),
  613. ]
  614. # State *after* press/release.
  615. return {button for button, flag in mod_table if flag}
  616. @staticmethod
  617. def _mpl_modifiers(event=None, *, exclude=None):
  618. mod_table = [
  619. ("ctrl", wx.MOD_CONTROL, wx.WXK_CONTROL),
  620. ("alt", wx.MOD_ALT, wx.WXK_ALT),
  621. ("shift", wx.MOD_SHIFT, wx.WXK_SHIFT),
  622. ]
  623. if event is not None:
  624. modifiers = event.GetModifiers()
  625. return [name for name, mod, key in mod_table
  626. if modifiers & mod and exclude != key]
  627. else:
  628. return [name for name, mod, key in mod_table
  629. if wx.GetKeyState(key)]
  630. def _get_key(self, event):
  631. keyval = event.KeyCode
  632. if keyval in self.keyvald:
  633. key = self.keyvald[keyval]
  634. elif keyval < 256:
  635. key = chr(keyval)
  636. # wx always returns an uppercase, so make it lowercase if the shift
  637. # key is not depressed (NOTE: this will not handle Caps Lock)
  638. if not event.ShiftDown():
  639. key = key.lower()
  640. else:
  641. return None
  642. mods = self._mpl_modifiers(event, exclude=keyval)
  643. if "shift" in mods and key.isupper():
  644. mods.remove("shift")
  645. return "+".join([*mods, key])
  646. def _mpl_coords(self, pos=None):
  647. """
  648. Convert a wx position, defaulting to the current cursor position, to
  649. Matplotlib coordinates.
  650. """
  651. if pos is None:
  652. pos = wx.GetMouseState()
  653. x, y = self.ScreenToClient(pos.X, pos.Y)
  654. else:
  655. x, y = pos.X, pos.Y
  656. # flip y so y=0 is bottom of canvas
  657. if not wx.Platform == '__WXMSW__':
  658. scale = self.GetDPIScaleFactor()
  659. return x*scale, self.figure.bbox.height - y*scale
  660. else:
  661. return x, self.figure.bbox.height - y
  662. def _on_key_down(self, event):
  663. """Capture key press."""
  664. KeyEvent("key_press_event", self,
  665. self._get_key(event), *self._mpl_coords(),
  666. guiEvent=event)._process()
  667. if self:
  668. event.Skip()
  669. def _on_key_up(self, event):
  670. """Release key."""
  671. KeyEvent("key_release_event", self,
  672. self._get_key(event), *self._mpl_coords(),
  673. guiEvent=event)._process()
  674. if self:
  675. event.Skip()
  676. def set_cursor(self, cursor):
  677. # docstring inherited
  678. cursor = wx.Cursor(_api.check_getitem({
  679. cursors.MOVE: wx.CURSOR_HAND,
  680. cursors.HAND: wx.CURSOR_HAND,
  681. cursors.POINTER: wx.CURSOR_ARROW,
  682. cursors.SELECT_REGION: wx.CURSOR_CROSS,
  683. cursors.WAIT: wx.CURSOR_WAIT,
  684. cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE,
  685. cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS,
  686. }, cursor=cursor))
  687. self.SetCursor(cursor)
  688. self.Refresh()
  689. def _set_capture(self, capture=True):
  690. """Control wx mouse capture."""
  691. if self.HasCapture():
  692. self.ReleaseMouse()
  693. if capture:
  694. self.CaptureMouse()
  695. def _on_capture_lost(self, event):
  696. """Capture changed or lost"""
  697. self._set_capture(False)
  698. def _on_mouse_button(self, event):
  699. """Start measuring on an axis."""
  700. event.Skip()
  701. self._set_capture(event.ButtonDown() or event.ButtonDClick())
  702. x, y = self._mpl_coords(event)
  703. button_map = {
  704. wx.MOUSE_BTN_LEFT: MouseButton.LEFT,
  705. wx.MOUSE_BTN_MIDDLE: MouseButton.MIDDLE,
  706. wx.MOUSE_BTN_RIGHT: MouseButton.RIGHT,
  707. wx.MOUSE_BTN_AUX1: MouseButton.BACK,
  708. wx.MOUSE_BTN_AUX2: MouseButton.FORWARD,
  709. }
  710. button = event.GetButton()
  711. button = button_map.get(button, button)
  712. modifiers = self._mpl_modifiers(event)
  713. if event.ButtonDown():
  714. MouseEvent("button_press_event", self, x, y, button,
  715. modifiers=modifiers, guiEvent=event)._process()
  716. elif event.ButtonDClick():
  717. MouseEvent("button_press_event", self, x, y, button, dblclick=True,
  718. modifiers=modifiers, guiEvent=event)._process()
  719. elif event.ButtonUp():
  720. MouseEvent("button_release_event", self, x, y, button,
  721. modifiers=modifiers, guiEvent=event)._process()
  722. def _on_mouse_wheel(self, event):
  723. """Translate mouse wheel events into matplotlib events"""
  724. x, y = self._mpl_coords(event)
  725. # Convert delta/rotation/rate into a floating point step size
  726. step = event.LinesPerAction * event.WheelRotation / event.WheelDelta
  727. # Done handling event
  728. event.Skip()
  729. # Mac gives two events for every wheel event; skip every second one.
  730. if wx.Platform == '__WXMAC__':
  731. if not hasattr(self, '_skipwheelevent'):
  732. self._skipwheelevent = True
  733. elif self._skipwheelevent:
  734. self._skipwheelevent = False
  735. return # Return without processing event
  736. else:
  737. self._skipwheelevent = True
  738. MouseEvent("scroll_event", self, x, y, step=step,
  739. modifiers=self._mpl_modifiers(event),
  740. guiEvent=event)._process()
  741. def _on_motion(self, event):
  742. """Start measuring on an axis."""
  743. event.Skip()
  744. MouseEvent("motion_notify_event", self,
  745. *self._mpl_coords(event),
  746. buttons=self._mpl_buttons(),
  747. modifiers=self._mpl_modifiers(event),
  748. guiEvent=event)._process()
  749. def _on_enter(self, event):
  750. """Mouse has entered the window."""
  751. event.Skip()
  752. LocationEvent("figure_enter_event", self,
  753. *self._mpl_coords(event),
  754. modifiers=self._mpl_modifiers(),
  755. guiEvent=event)._process()
  756. def _on_leave(self, event):
  757. """Mouse has left the window."""
  758. event.Skip()
  759. LocationEvent("figure_leave_event", self,
  760. *self._mpl_coords(event),
  761. modifiers=self._mpl_modifiers(),
  762. guiEvent=event)._process()
  763. class FigureCanvasWx(_FigureCanvasWxBase):
  764. # Rendering to a Wx canvas using the deprecated Wx renderer.
  765. def draw(self, drawDC=None):
  766. """
  767. Render the figure using RendererWx instance renderer, or using a
  768. previously defined renderer if none is specified.
  769. """
  770. _log.debug("%s - draw()", type(self))
  771. self.renderer = RendererWx(self.bitmap, self.figure.dpi)
  772. self.figure.draw(self.renderer)
  773. self._isDrawn = True
  774. self.gui_repaint(drawDC=drawDC)
  775. def _print_image(self, filetype, filename):
  776. bitmap = wx.Bitmap(math.ceil(self.figure.bbox.width),
  777. math.ceil(self.figure.bbox.height))
  778. self.figure.draw(RendererWx(bitmap, self.figure.dpi))
  779. saved_obj = (bitmap.ConvertToImage()
  780. if cbook.is_writable_file_like(filename)
  781. else bitmap)
  782. if not saved_obj.SaveFile(filename, filetype):
  783. raise RuntimeError(f'Could not save figure to {filename}')
  784. # draw() is required here since bits of state about the last renderer
  785. # are strewn about the artist draw methods. Do not remove the draw
  786. # without first verifying that these have been cleaned up. The artist
  787. # contains() methods will fail otherwise.
  788. if self._isDrawn:
  789. self.draw()
  790. # The "if self" check avoids a "wrapped C/C++ object has been deleted"
  791. # RuntimeError if doing things after window is closed.
  792. if self:
  793. self.Refresh()
  794. print_bmp = functools.partialmethod(
  795. _print_image, wx.BITMAP_TYPE_BMP)
  796. print_jpeg = print_jpg = functools.partialmethod(
  797. _print_image, wx.BITMAP_TYPE_JPEG)
  798. print_pcx = functools.partialmethod(
  799. _print_image, wx.BITMAP_TYPE_PCX)
  800. print_png = functools.partialmethod(
  801. _print_image, wx.BITMAP_TYPE_PNG)
  802. print_tiff = print_tif = functools.partialmethod(
  803. _print_image, wx.BITMAP_TYPE_TIF)
  804. print_xpm = functools.partialmethod(
  805. _print_image, wx.BITMAP_TYPE_XPM)
  806. class FigureFrameWx(wx.Frame):
  807. def __init__(self, num, fig, *, canvas_class):
  808. # On non-Windows platform, explicitly set the position - fix
  809. # positioning bug on some Linux platforms
  810. if wx.Platform == '__WXMSW__':
  811. pos = wx.DefaultPosition
  812. else:
  813. pos = wx.Point(20, 20)
  814. super().__init__(parent=None, id=-1, pos=pos)
  815. # Frame will be sized later by the Fit method
  816. _log.debug("%s - __init__()", type(self))
  817. _set_frame_icon(self)
  818. self.canvas = canvas_class(self, -1, fig)
  819. # Auto-attaches itself to self.canvas.manager
  820. manager = FigureManagerWx(self.canvas, num, self)
  821. toolbar = self.canvas.manager.toolbar
  822. if toolbar is not None:
  823. self.SetToolBar(toolbar)
  824. # On Windows, canvas sizing must occur after toolbar addition;
  825. # otherwise the toolbar further resizes the canvas.
  826. w, h = map(math.ceil, fig.bbox.size)
  827. self.canvas.SetInitialSize(self.FromDIP(wx.Size(w, h)))
  828. self.canvas.SetMinSize(self.FromDIP(wx.Size(2, 2)))
  829. self.canvas.SetFocus()
  830. self.Fit()
  831. self.Bind(wx.EVT_CLOSE, self._on_close)
  832. def _on_close(self, event):
  833. _log.debug("%s - on_close()", type(self))
  834. CloseEvent("close_event", self.canvas)._process()
  835. self.canvas.stop_event_loop()
  836. # set FigureManagerWx.frame to None to prevent repeated attempts to
  837. # close this frame from FigureManagerWx.destroy()
  838. self.canvas.manager.frame = None
  839. # remove figure manager from Gcf.figs
  840. Gcf.destroy(self.canvas.manager)
  841. try: # See issue 2941338.
  842. self.canvas.mpl_disconnect(self.canvas.toolbar._id_drag)
  843. except AttributeError: # If there's no toolbar.
  844. pass
  845. # Carry on with close event propagation, frame & children destruction
  846. event.Skip()
  847. class FigureManagerWx(FigureManagerBase):
  848. """
  849. Container/controller for the FigureCanvas and GUI frame.
  850. It is instantiated by Gcf whenever a new figure is created. Gcf is
  851. responsible for managing multiple instances of FigureManagerWx.
  852. Attributes
  853. ----------
  854. canvas : `FigureCanvas`
  855. a FigureCanvasWx(wx.Panel) instance
  856. window : wxFrame
  857. a wxFrame instance - wxpython.org/Phoenix/docs/html/Frame.html
  858. """
  859. def __init__(self, canvas, num, frame):
  860. _log.debug("%s - __init__()", type(self))
  861. self.frame = self.window = frame
  862. super().__init__(canvas, num)
  863. @classmethod
  864. def create_with_canvas(cls, canvas_class, figure, num):
  865. # docstring inherited
  866. wxapp = wx.GetApp() or _create_wxapp()
  867. frame = FigureFrameWx(num, figure, canvas_class=canvas_class)
  868. manager = figure.canvas.manager
  869. if mpl.is_interactive():
  870. manager.frame.Show()
  871. figure.canvas.draw_idle()
  872. return manager
  873. @classmethod
  874. def start_main_loop(cls):
  875. if not wx.App.IsMainLoopRunning():
  876. wxapp = wx.GetApp()
  877. if wxapp is not None:
  878. wxapp.MainLoop()
  879. def show(self):
  880. # docstring inherited
  881. self.frame.Show()
  882. self.canvas.draw()
  883. if mpl.rcParams['figure.raise_window']:
  884. self.frame.Raise()
  885. def destroy(self, *args):
  886. # docstring inherited
  887. _log.debug("%s - destroy()", type(self))
  888. frame = self.frame
  889. if frame: # Else, may have been already deleted, e.g. when closing.
  890. # As this can be called from non-GUI thread from plt.close use
  891. # wx.CallAfter to ensure thread safety.
  892. wx.CallAfter(frame.Close)
  893. def full_screen_toggle(self):
  894. # docstring inherited
  895. self.frame.ShowFullScreen(not self.frame.IsFullScreen())
  896. def get_window_title(self):
  897. # docstring inherited
  898. return self.window.GetTitle()
  899. def set_window_title(self, title):
  900. # docstring inherited
  901. self.window.SetTitle(title)
  902. def resize(self, width, height):
  903. # docstring inherited
  904. # Directly using SetClientSize doesn't handle the toolbar on Windows.
  905. self.window.SetSize(self.window.ClientToWindowSize(wx.Size(
  906. math.ceil(width), math.ceil(height))))
  907. def _load_bitmap(filename):
  908. """
  909. Load a wx.Bitmap from a file in the "images" directory of the Matplotlib
  910. data.
  911. """
  912. return wx.Bitmap(str(cbook._get_data_path('images', filename)))
  913. def _set_frame_icon(frame):
  914. bundle = wx.IconBundle()
  915. for image in ('matplotlib.png', 'matplotlib_large.png'):
  916. icon = wx.Icon(_load_bitmap(image))
  917. if not icon.IsOk():
  918. return
  919. bundle.AddIcon(icon)
  920. frame.SetIcons(bundle)
  921. class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar):
  922. def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM):
  923. wx.ToolBar.__init__(self, canvas.GetParent(), -1, style=style)
  924. if wx.Platform == '__WXMAC__':
  925. self.SetToolBitmapSize(self.GetToolBitmapSize()*self.GetDPIScaleFactor())
  926. self.wx_ids = {}
  927. for text, tooltip_text, image_file, callback in self.toolitems:
  928. if text is None:
  929. self.AddSeparator()
  930. continue
  931. self.wx_ids[text] = (
  932. self.AddTool(
  933. -1,
  934. bitmap=self._icon(f"{image_file}.svg"),
  935. bmpDisabled=wx.NullBitmap,
  936. label=text, shortHelp=tooltip_text,
  937. kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"]
  938. else wx.ITEM_NORMAL))
  939. .Id)
  940. self.Bind(wx.EVT_TOOL, getattr(self, callback),
  941. id=self.wx_ids[text])
  942. self._coordinates = coordinates
  943. if self._coordinates:
  944. self.AddStretchableSpace()
  945. self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT)
  946. self.AddControl(self._label_text)
  947. self.Realize()
  948. NavigationToolbar2.__init__(self, canvas)
  949. @staticmethod
  950. def _icon(name):
  951. """
  952. Construct a `wx.Bitmap` suitable for use as icon from an image file
  953. *name*, including the extension and relative to Matplotlib's "images"
  954. data directory.
  955. """
  956. try:
  957. dark = wx.SystemSettings.GetAppearance().IsDark()
  958. except AttributeError: # wxpython < 4.1
  959. # copied from wx's IsUsingDarkBackground / GetLuminance.
  960. bg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
  961. fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
  962. # See wx.Colour.GetLuminance.
  963. bg_lum = (.299 * bg.red + .587 * bg.green + .114 * bg.blue) / 255
  964. fg_lum = (.299 * fg.red + .587 * fg.green + .114 * fg.blue) / 255
  965. dark = fg_lum - bg_lum > .2
  966. path = cbook._get_data_path('images', name)
  967. if path.suffix == '.svg':
  968. svg = path.read_bytes()
  969. if dark:
  970. svg = svg.replace(b'fill:black;', b'fill:white;')
  971. toolbarIconSize = wx.ArtProvider().GetDIPSizeHint(wx.ART_TOOLBAR)
  972. return wx.BitmapBundle.FromSVG(svg, toolbarIconSize)
  973. else:
  974. pilimg = PIL.Image.open(path)
  975. # ensure RGBA as wx BitMap expects RGBA format
  976. image = np.array(pilimg.convert("RGBA"))
  977. if dark:
  978. fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
  979. black_mask = (image[..., :3] == 0).all(axis=-1)
  980. image[black_mask, :3] = (fg.Red(), fg.Green(), fg.Blue())
  981. return wx.Bitmap.FromBufferRGBA(
  982. image.shape[1], image.shape[0], image.tobytes())
  983. def _update_buttons_checked(self):
  984. if "Pan" in self.wx_ids:
  985. self.ToggleTool(self.wx_ids["Pan"], self.mode.name == "PAN")
  986. if "Zoom" in self.wx_ids:
  987. self.ToggleTool(self.wx_ids["Zoom"], self.mode.name == "ZOOM")
  988. def zoom(self, *args):
  989. super().zoom(*args)
  990. self._update_buttons_checked()
  991. def pan(self, *args):
  992. super().pan(*args)
  993. self._update_buttons_checked()
  994. def save_figure(self, *args):
  995. # Fetch the required filename and file type.
  996. filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards()
  997. default_file = self.canvas.get_default_filename()
  998. dialog = wx.FileDialog(
  999. self.canvas.GetParent(), "Save to file",
  1000. mpl.rcParams["savefig.directory"], default_file, filetypes,
  1001. wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
  1002. dialog.SetFilterIndex(filter_index)
  1003. if dialog.ShowModal() == wx.ID_OK:
  1004. path = pathlib.Path(dialog.GetPath())
  1005. _log.debug('%s - Save file path: %s', type(self), path)
  1006. fmt = exts[dialog.GetFilterIndex()]
  1007. ext = path.suffix[1:]
  1008. if ext in self.canvas.get_supported_filetypes() and fmt != ext:
  1009. # looks like they forgot to set the image type drop
  1010. # down, going with the extension.
  1011. _log.warning('extension %s did not match the selected '
  1012. 'image type %s; going with %s',
  1013. ext, fmt, ext)
  1014. fmt = ext
  1015. # Save dir for next time, unless empty str (which means use cwd).
  1016. if mpl.rcParams["savefig.directory"]:
  1017. mpl.rcParams["savefig.directory"] = str(path.parent)
  1018. try:
  1019. self.canvas.figure.savefig(path, format=fmt)
  1020. return path
  1021. except Exception as e:
  1022. dialog = wx.MessageDialog(
  1023. parent=self.canvas.GetParent(), message=str(e),
  1024. caption='Matplotlib error')
  1025. dialog.ShowModal()
  1026. dialog.Destroy()
  1027. def draw_rubberband(self, event, x0, y0, x1, y1):
  1028. height = self.canvas.figure.bbox.height
  1029. sf = 1 if wx.Platform == '__WXMSW__' else self.canvas.GetDPIScaleFactor()
  1030. self.canvas._rubberband_rect = (x0/sf, (height - y0)/sf,
  1031. x1/sf, (height - y1)/sf)
  1032. self.canvas.Refresh()
  1033. def remove_rubberband(self):
  1034. self.canvas._rubberband_rect = None
  1035. self.canvas.Refresh()
  1036. def set_message(self, s):
  1037. if self._coordinates:
  1038. self._label_text.SetLabel(s)
  1039. def set_history_buttons(self):
  1040. can_backward = self._nav_stack._pos > 0
  1041. can_forward = self._nav_stack._pos < len(self._nav_stack) - 1
  1042. if 'Back' in self.wx_ids:
  1043. self.EnableTool(self.wx_ids['Back'], can_backward)
  1044. if 'Forward' in self.wx_ids:
  1045. self.EnableTool(self.wx_ids['Forward'], can_forward)
  1046. # tools for matplotlib.backend_managers.ToolManager:
  1047. class ToolbarWx(ToolContainerBase, wx.ToolBar):
  1048. _icon_extension = '.svg'
  1049. def __init__(self, toolmanager, parent=None, style=wx.TB_BOTTOM):
  1050. if parent is None:
  1051. parent = toolmanager.canvas.GetParent()
  1052. ToolContainerBase.__init__(self, toolmanager)
  1053. wx.ToolBar.__init__(self, parent, -1, style=style)
  1054. self._space = self.AddStretchableSpace()
  1055. self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT)
  1056. self.AddControl(self._label_text)
  1057. self._toolitems = {}
  1058. self._groups = {} # Mapping of groups to the separator after them.
  1059. def _get_tool_pos(self, tool):
  1060. """
  1061. Find the position (index) of a wx.ToolBarToolBase in a ToolBar.
  1062. ``ToolBar.GetToolPos`` is not useful because wx assigns the same Id to
  1063. all Separators and StretchableSpaces.
  1064. """
  1065. pos, = (pos for pos in range(self.ToolsCount)
  1066. if self.GetToolByPos(pos) == tool)
  1067. return pos
  1068. def add_toolitem(self, name, group, position, image_file, description,
  1069. toggle):
  1070. # Find or create the separator that follows this group.
  1071. if group not in self._groups:
  1072. self._groups[group] = self.InsertSeparator(
  1073. self._get_tool_pos(self._space))
  1074. sep = self._groups[group]
  1075. # List all separators.
  1076. seps = [t for t in map(self.GetToolByPos, range(self.ToolsCount))
  1077. if t.IsSeparator() and not t.IsStretchableSpace()]
  1078. # Find where to insert the tool.
  1079. if position >= 0:
  1080. # Find the start of the group by looking for the separator
  1081. # preceding this one; then move forward from it.
  1082. start = (0 if sep == seps[0]
  1083. else self._get_tool_pos(seps[seps.index(sep) - 1]) + 1)
  1084. else:
  1085. # Move backwards from this separator.
  1086. start = self._get_tool_pos(sep) + 1
  1087. idx = start + position
  1088. if image_file:
  1089. bmp = NavigationToolbar2Wx._icon(image_file)
  1090. kind = wx.ITEM_NORMAL if not toggle else wx.ITEM_CHECK
  1091. tool = self.InsertTool(idx, -1, name, bmp, wx.NullBitmap, kind,
  1092. description or "")
  1093. else:
  1094. size = (self.GetTextExtent(name)[0] + 10, -1)
  1095. if toggle:
  1096. control = wx.ToggleButton(self, -1, name, size=size)
  1097. else:
  1098. control = wx.Button(self, -1, name, size=size)
  1099. tool = self.InsertControl(idx, control, label=name)
  1100. self.Realize()
  1101. def handler(event):
  1102. self.trigger_tool(name)
  1103. if image_file:
  1104. self.Bind(wx.EVT_TOOL, handler, tool)
  1105. else:
  1106. control.Bind(wx.EVT_LEFT_DOWN, handler)
  1107. self._toolitems.setdefault(name, [])
  1108. self._toolitems[name].append((tool, handler))
  1109. def toggle_toolitem(self, name, toggled):
  1110. if name not in self._toolitems:
  1111. return
  1112. for tool, handler in self._toolitems[name]:
  1113. if not tool.IsControl():
  1114. self.ToggleTool(tool.Id, toggled)
  1115. else:
  1116. tool.GetControl().SetValue(toggled)
  1117. self.Refresh()
  1118. def remove_toolitem(self, name):
  1119. for tool, handler in self._toolitems.pop(name, []):
  1120. self.DeleteTool(tool.Id)
  1121. def set_message(self, s):
  1122. self._label_text.SetLabel(s)
  1123. @backend_tools._register_tool_class(_FigureCanvasWxBase)
  1124. class ConfigureSubplotsWx(backend_tools.ConfigureSubplotsBase):
  1125. def trigger(self, *args):
  1126. NavigationToolbar2Wx.configure_subplots(self)
  1127. @backend_tools._register_tool_class(_FigureCanvasWxBase)
  1128. class SaveFigureWx(backend_tools.SaveFigureBase):
  1129. def trigger(self, *args):
  1130. NavigationToolbar2Wx.save_figure(
  1131. self._make_classic_style_pseudo_toolbar())
  1132. @backend_tools._register_tool_class(_FigureCanvasWxBase)
  1133. class RubberbandWx(backend_tools.RubberbandBase):
  1134. def draw_rubberband(self, x0, y0, x1, y1):
  1135. NavigationToolbar2Wx.draw_rubberband(
  1136. self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
  1137. def remove_rubberband(self):
  1138. NavigationToolbar2Wx.remove_rubberband(
  1139. self._make_classic_style_pseudo_toolbar())
  1140. class _HelpDialog(wx.Dialog):
  1141. _instance = None # a reference to an open dialog singleton
  1142. headers = [("Action", "Shortcuts", "Description")]
  1143. widths = [100, 140, 300]
  1144. def __init__(self, parent, help_entries):
  1145. super().__init__(parent, title="Help",
  1146. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
  1147. sizer = wx.BoxSizer(wx.VERTICAL)
  1148. grid_sizer = wx.FlexGridSizer(0, 3, 8, 6)
  1149. # create and add the entries
  1150. bold = self.GetFont().MakeBold()
  1151. for r, row in enumerate(self.headers + help_entries):
  1152. for (col, width) in zip(row, self.widths):
  1153. label = wx.StaticText(self, label=col)
  1154. if r == 0:
  1155. label.SetFont(bold)
  1156. label.Wrap(width)
  1157. grid_sizer.Add(label, 0, 0, 0)
  1158. # finalize layout, create button
  1159. sizer.Add(grid_sizer, 0, wx.ALL, 6)
  1160. ok = wx.Button(self, wx.ID_OK)
  1161. sizer.Add(ok, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 8)
  1162. self.SetSizer(sizer)
  1163. sizer.Fit(self)
  1164. self.Layout()
  1165. self.Bind(wx.EVT_CLOSE, self._on_close)
  1166. ok.Bind(wx.EVT_BUTTON, self._on_close)
  1167. def _on_close(self, event):
  1168. _HelpDialog._instance = None # remove global reference
  1169. self.DestroyLater()
  1170. event.Skip()
  1171. @classmethod
  1172. def show(cls, parent, help_entries):
  1173. # if no dialog is shown, create one; otherwise just re-raise it
  1174. if cls._instance:
  1175. cls._instance.Raise()
  1176. return
  1177. cls._instance = cls(parent, help_entries)
  1178. cls._instance.Show()
  1179. @backend_tools._register_tool_class(_FigureCanvasWxBase)
  1180. class HelpWx(backend_tools.ToolHelpBase):
  1181. def trigger(self, *args):
  1182. _HelpDialog.show(self.figure.canvas.GetTopLevelParent(),
  1183. self._get_help_entries())
  1184. @backend_tools._register_tool_class(_FigureCanvasWxBase)
  1185. class ToolCopyToClipboardWx(backend_tools.ToolCopyToClipboardBase):
  1186. def trigger(self, *args, **kwargs):
  1187. if not self.canvas._isDrawn:
  1188. self.canvas.draw()
  1189. if not self.canvas.bitmap.IsOk() or not wx.TheClipboard.Open():
  1190. return
  1191. try:
  1192. wx.TheClipboard.SetData(wx.BitmapDataObject(self.canvas.bitmap))
  1193. finally:
  1194. wx.TheClipboard.Close()
  1195. FigureManagerWx._toolbar2_class = NavigationToolbar2Wx
  1196. FigureManagerWx._toolmanager_toolbar_class = ToolbarWx
  1197. @_Backend.export
  1198. class _BackendWx(_Backend):
  1199. FigureCanvas = FigureCanvasWx
  1200. FigureManager = FigureManagerWx
  1201. mainloop = FigureManagerWx.start_main_loop