backend_wx.py 48 KB

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