backend_wx.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927
  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 logging
  8. import math
  9. import os.path
  10. import sys
  11. import weakref
  12. import matplotlib
  13. from matplotlib.backend_bases import (
  14. _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
  15. MouseButton, NavigationToolbar2, RendererBase, StatusbarBase, TimerBase,
  16. ToolContainerBase, _has_pil, cursors)
  17. from matplotlib import cbook, rcParams, backend_tools
  18. from matplotlib._pylab_helpers import Gcf
  19. from matplotlib.backend_managers import ToolManager
  20. from matplotlib.figure import Figure
  21. from matplotlib.path import Path
  22. from matplotlib.transforms import Affine2D
  23. from matplotlib.widgets import SubplotTool
  24. import wx
  25. _log = logging.getLogger(__name__)
  26. # Debugging settings here...
  27. # Debug level set here. If the debug level is less than 5, information
  28. # messages (progressively more info for lower value) are printed. In addition,
  29. # traceback is performed, and pdb activated, for all uncaught exceptions in
  30. # this case
  31. _DEBUG = 5
  32. _DEBUG_lvls = {1: 'Low ', 2: 'Med ', 3: 'High', 4: 'Error'}
  33. def DEBUG_MSG(string, lvl=3, o=None):
  34. if lvl >= _DEBUG:
  35. print(f"{_DEBUG_lvls[lvl]}- {string} in {type(o)}")
  36. @cbook.deprecated("3.1")
  37. def debug_on_error(type, value, tb):
  38. """Code due to Thomas Heller - published in Python Cookbook (O'Reilly)"""
  39. import pdb
  40. import traceback
  41. traceback.print_exception(type, value, tb)
  42. print()
  43. pdb.pm()
  44. @cbook.deprecated("3.1")
  45. class fake_stderr:
  46. """
  47. Wx does strange things with stderr, as it makes the assumption that
  48. there is probably no console. This redirects stderr to the console, since
  49. we know that there is one!
  50. """
  51. def write(self, msg):
  52. print("Stderr: %s\n\r" % msg)
  53. # the True dots per inch on the screen; should be display dependent
  54. # see
  55. # http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5
  56. # for some info about screen dpi
  57. PIXELS_PER_INCH = 75
  58. # Delay time for idle checks
  59. IDLE_DELAY = 5 # Documented as deprecated as of Matplotlib 3.1.
  60. def error_msg_wx(msg, parent=None):
  61. """
  62. Signal an error condition -- in a GUI, popup a error dialog
  63. """
  64. dialog = wx.MessageDialog(parent=parent,
  65. message=msg,
  66. caption='Matplotlib backend_wx error',
  67. style=wx.OK | wx.CENTRE)
  68. dialog.ShowModal()
  69. dialog.Destroy()
  70. return None
  71. @cbook.deprecated("3.1")
  72. def raise_msg_to_str(msg):
  73. """msg is a return arg from a raise. Join with new lines."""
  74. if not isinstance(msg, str):
  75. msg = '\n'.join(map(str, msg))
  76. return msg
  77. class TimerWx(TimerBase):
  78. """
  79. Subclass of `.TimerBase` that uses WxTimer events.
  80. Attributes
  81. ----------
  82. interval : int
  83. The time between timer events in milliseconds. Default is 1000 ms.
  84. single_shot : bool
  85. Boolean flag indicating whether this timer should operate as single
  86. shot (run once and then stop). Defaults to False.
  87. callbacks : list
  88. Stores list of (func, args) tuples that will be called upon timer
  89. events. This list can be manipulated directly, or the functions
  90. `add_callback` and `remove_callback` can be used.
  91. """
  92. def __init__(self, *args, **kwargs):
  93. if args and isinstance(args[0], wx.EvtHandler):
  94. cbook.warn_deprecated(
  95. "3.0", message="Passing a wx.EvtHandler as first argument to "
  96. "the TimerWx constructor is deprecated since %(since)s.")
  97. args = args[1:]
  98. TimerBase.__init__(self, *args, **kwargs)
  99. self._timer = wx.Timer()
  100. self._timer.Notify = self._on_timer
  101. def _timer_start(self):
  102. self._timer.Start(self._interval, self._single)
  103. def _timer_stop(self):
  104. self._timer.Stop()
  105. def _timer_set_interval(self):
  106. self._timer_start()
  107. def _timer_set_single_shot(self):
  108. self._timer.Start()
  109. class RendererWx(RendererBase):
  110. """
  111. The renderer handles all the drawing primitives using a graphics
  112. context instance that controls the colors/styles. It acts as the
  113. 'renderer' instance used by many classes in the hierarchy.
  114. """
  115. # In wxPython, drawing is performed on a wxDC instance, which will
  116. # generally be mapped to the client area of the window displaying
  117. # the plot. Under wxPython, the wxDC instance has a wx.Pen which
  118. # describes the colour and weight of any lines drawn, and a wxBrush
  119. # which describes the fill colour of any closed polygon.
  120. # Font styles, families and weight.
  121. fontweights = {
  122. 100: wx.FONTWEIGHT_LIGHT,
  123. 200: wx.FONTWEIGHT_LIGHT,
  124. 300: wx.FONTWEIGHT_LIGHT,
  125. 400: wx.FONTWEIGHT_NORMAL,
  126. 500: wx.FONTWEIGHT_NORMAL,
  127. 600: wx.FONTWEIGHT_NORMAL,
  128. 700: wx.FONTWEIGHT_BOLD,
  129. 800: wx.FONTWEIGHT_BOLD,
  130. 900: wx.FONTWEIGHT_BOLD,
  131. 'ultralight': wx.FONTWEIGHT_LIGHT,
  132. 'light': wx.FONTWEIGHT_LIGHT,
  133. 'normal': wx.FONTWEIGHT_NORMAL,
  134. 'medium': wx.FONTWEIGHT_NORMAL,
  135. 'semibold': wx.FONTWEIGHT_NORMAL,
  136. 'bold': wx.FONTWEIGHT_BOLD,
  137. 'heavy': wx.FONTWEIGHT_BOLD,
  138. 'ultrabold': wx.FONTWEIGHT_BOLD,
  139. 'black': wx.FONTWEIGHT_BOLD,
  140. }
  141. fontangles = {
  142. 'italic': wx.FONTSTYLE_ITALIC,
  143. 'normal': wx.FONTSTYLE_NORMAL,
  144. 'oblique': wx.FONTSTYLE_SLANT,
  145. }
  146. # wxPython allows for portable font styles, choosing them appropriately for
  147. # the target platform. Map some standard font names to the portable styles.
  148. # QUESTION: Is it be wise to agree standard fontnames across all backends?
  149. fontnames = {
  150. 'Sans': wx.FONTFAMILY_SWISS,
  151. 'Roman': wx.FONTFAMILY_ROMAN,
  152. 'Script': wx.FONTFAMILY_SCRIPT,
  153. 'Decorative': wx.FONTFAMILY_DECORATIVE,
  154. 'Modern': wx.FONTFAMILY_MODERN,
  155. 'Courier': wx.FONTFAMILY_MODERN,
  156. 'courier': wx.FONTFAMILY_MODERN,
  157. }
  158. def __init__(self, bitmap, dpi):
  159. """Initialise a wxWindows renderer instance."""
  160. cbook.warn_deprecated(
  161. "2.0", name="wx", obj_type="backend", removal="the future",
  162. alternative="wxagg", addendum="See the Matplotlib usage FAQ for "
  163. "more info on backends.")
  164. RendererBase.__init__(self)
  165. DEBUG_MSG("__init__()", 1, self)
  166. self.width = bitmap.GetWidth()
  167. self.height = bitmap.GetHeight()
  168. self.bitmap = bitmap
  169. self.fontd = {}
  170. self.dpi = dpi
  171. self.gc = None
  172. def flipy(self):
  173. # docstring inherited
  174. return True
  175. def offset_text_height(self):
  176. return True
  177. def get_text_width_height_descent(self, s, prop, ismath):
  178. # docstring inherited
  179. if ismath:
  180. s = cbook.strip_math(s)
  181. if self.gc is None:
  182. gc = self.new_gc()
  183. else:
  184. gc = self.gc
  185. gfx_ctx = gc.gfx_ctx
  186. font = self.get_wx_font(s, prop)
  187. gfx_ctx.SetFont(font, wx.BLACK)
  188. w, h, descent, leading = gfx_ctx.GetFullTextExtent(s)
  189. return w, h, descent
  190. def get_canvas_width_height(self):
  191. # docstring inherited
  192. return self.width, self.height
  193. def handle_clip_rectangle(self, gc):
  194. new_bounds = gc.get_clip_rectangle()
  195. if new_bounds is not None:
  196. new_bounds = new_bounds.bounds
  197. gfx_ctx = gc.gfx_ctx
  198. if gfx_ctx._lastcliprect != new_bounds:
  199. gfx_ctx._lastcliprect = new_bounds
  200. if new_bounds is None:
  201. gfx_ctx.ResetClip()
  202. else:
  203. gfx_ctx.Clip(new_bounds[0],
  204. self.height - new_bounds[1] - new_bounds[3],
  205. new_bounds[2], new_bounds[3])
  206. @staticmethod
  207. def convert_path(gfx_ctx, path, transform):
  208. wxpath = gfx_ctx.CreatePath()
  209. for points, code in path.iter_segments(transform):
  210. if code == Path.MOVETO:
  211. wxpath.MoveToPoint(*points)
  212. elif code == Path.LINETO:
  213. wxpath.AddLineToPoint(*points)
  214. elif code == Path.CURVE3:
  215. wxpath.AddQuadCurveToPoint(*points)
  216. elif code == Path.CURVE4:
  217. wxpath.AddCurveToPoint(*points)
  218. elif code == Path.CLOSEPOLY:
  219. wxpath.CloseSubpath()
  220. return wxpath
  221. def draw_path(self, gc, path, transform, rgbFace=None):
  222. # docstring inherited
  223. gc.select()
  224. self.handle_clip_rectangle(gc)
  225. gfx_ctx = gc.gfx_ctx
  226. transform = transform + \
  227. Affine2D().scale(1.0, -1.0).translate(0.0, self.height)
  228. wxpath = self.convert_path(gfx_ctx, path, transform)
  229. if rgbFace is not None:
  230. gfx_ctx.SetBrush(wx.Brush(gc.get_wxcolour(rgbFace)))
  231. gfx_ctx.DrawPath(wxpath)
  232. else:
  233. gfx_ctx.StrokePath(wxpath)
  234. gc.unselect()
  235. def draw_image(self, gc, x, y, im):
  236. bbox = gc.get_clip_rectangle()
  237. if bbox is not None:
  238. l, b, w, h = bbox.bounds
  239. else:
  240. l = 0
  241. b = 0
  242. w = self.width
  243. h = self.height
  244. rows, cols = im.shape[:2]
  245. bitmap = wx.Bitmap.FromBufferRGBA(cols, rows, im.tostring())
  246. gc = self.get_gc()
  247. gc.select()
  248. gc.gfx_ctx.DrawBitmap(bitmap, int(l), int(self.height - b),
  249. int(w), int(-h))
  250. gc.unselect()
  251. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  252. # docstring inherited
  253. if ismath:
  254. s = cbook.strip_math(s)
  255. DEBUG_MSG("draw_text()", 1, self)
  256. gc.select()
  257. self.handle_clip_rectangle(gc)
  258. gfx_ctx = gc.gfx_ctx
  259. font = self.get_wx_font(s, prop)
  260. color = gc.get_wxcolour(gc.get_rgb())
  261. gfx_ctx.SetFont(font, color)
  262. w, h, d = self.get_text_width_height_descent(s, prop, ismath)
  263. x = int(x)
  264. y = int(y - h)
  265. if angle == 0.0:
  266. gfx_ctx.DrawText(s, x, y)
  267. else:
  268. rads = math.radians(angle)
  269. xo = h * math.sin(rads)
  270. yo = h * math.cos(rads)
  271. gfx_ctx.DrawRotatedText(s, x - xo, y - yo, rads)
  272. gc.unselect()
  273. def new_gc(self):
  274. # docstring inherited
  275. DEBUG_MSG('new_gc()', 2, self)
  276. self.gc = GraphicsContextWx(self.bitmap, self)
  277. self.gc.select()
  278. self.gc.unselect()
  279. return self.gc
  280. def get_gc(self):
  281. """
  282. Fetch the locally cached gc.
  283. """
  284. # This is a dirty hack to allow anything with access to a renderer to
  285. # access the current graphics context
  286. assert self.gc is not None, "gc must be defined"
  287. return self.gc
  288. def get_wx_font(self, s, prop):
  289. """
  290. Return a wx font. Cache instances in a font dictionary for
  291. efficiency
  292. """
  293. DEBUG_MSG("get_wx_font()", 1, self)
  294. key = hash(prop)
  295. fontprop = prop
  296. fontname = fontprop.get_name()
  297. font = self.fontd.get(key)
  298. if font is not None:
  299. return font
  300. # Allow use of platform independent and dependent font names
  301. wxFontname = self.fontnames.get(fontname, wx.ROMAN)
  302. wxFacename = '' # Empty => wxPython chooses based on wx_fontname
  303. # Font colour is determined by the active wx.Pen
  304. # TODO: It may be wise to cache font information
  305. size = self.points_to_pixels(fontprop.get_size_in_points())
  306. font = wx.Font(int(size + 0.5), # Size
  307. wxFontname, # 'Generic' name
  308. self.fontangles[fontprop.get_style()], # Angle
  309. self.fontweights[fontprop.get_weight()], # Weight
  310. False, # Underline
  311. wxFacename) # Platform font name
  312. # cache the font and gc and return it
  313. self.fontd[key] = font
  314. return font
  315. def points_to_pixels(self, points):
  316. # docstring inherited
  317. return points * (PIXELS_PER_INCH / 72.0 * self.dpi / 72.0)
  318. class GraphicsContextWx(GraphicsContextBase):
  319. """
  320. The graphics context provides the color, line styles, etc...
  321. This class stores a reference to a wxMemoryDC, and a
  322. wxGraphicsContext that draws to it. Creating a wxGraphicsContext
  323. seems to be fairly heavy, so these objects are cached based on the
  324. bitmap object that is passed in.
  325. The base GraphicsContext stores colors as a RGB tuple on the unit
  326. interval, e.g., (0.5, 0.0, 1.0). wxPython uses an int interval, but
  327. since wxPython colour management is rather simple, I have not chosen
  328. to implement a separate colour manager class.
  329. """
  330. _capd = {'butt': wx.CAP_BUTT,
  331. 'projecting': wx.CAP_PROJECTING,
  332. 'round': wx.CAP_ROUND}
  333. _joind = {'bevel': wx.JOIN_BEVEL,
  334. 'miter': wx.JOIN_MITER,
  335. 'round': wx.JOIN_ROUND}
  336. _cache = weakref.WeakKeyDictionary()
  337. def __init__(self, bitmap, renderer):
  338. GraphicsContextBase.__init__(self)
  339. # assert self.Ok(), "wxMemoryDC not OK to use"
  340. DEBUG_MSG("__init__()", 1, self)
  341. DEBUG_MSG("__init__() 2: %s" % bitmap, 1, self)
  342. dc, gfx_ctx = self._cache.get(bitmap, (None, None))
  343. if dc is None:
  344. dc = wx.MemoryDC()
  345. dc.SelectObject(bitmap)
  346. gfx_ctx = wx.GraphicsContext.Create(dc)
  347. gfx_ctx._lastcliprect = None
  348. self._cache[bitmap] = dc, gfx_ctx
  349. self.bitmap = bitmap
  350. self.dc = dc
  351. self.gfx_ctx = gfx_ctx
  352. self._pen = wx.Pen('BLACK', 1, wx.SOLID)
  353. gfx_ctx.SetPen(self._pen)
  354. self.renderer = renderer
  355. def select(self):
  356. """Select the current bitmap into this wxDC instance."""
  357. if sys.platform == 'win32':
  358. self.dc.SelectObject(self.bitmap)
  359. self.IsSelected = True
  360. def unselect(self):
  361. """Select a Null bitmap into this wxDC instance."""
  362. if sys.platform == 'win32':
  363. self.dc.SelectObject(wx.NullBitmap)
  364. self.IsSelected = False
  365. def set_foreground(self, fg, isRGBA=None):
  366. # docstring inherited
  367. # Implementation note: wxPython has a separate concept of pen and
  368. # brush - the brush fills any outline trace left by the pen.
  369. # Here we set both to the same colour - if a figure is not to be
  370. # filled, the renderer will set the brush to be transparent
  371. # Same goes for text foreground...
  372. DEBUG_MSG("set_foreground()", 1, self)
  373. self.select()
  374. GraphicsContextBase.set_foreground(self, fg, isRGBA)
  375. self._pen.SetColour(self.get_wxcolour(self.get_rgb()))
  376. self.gfx_ctx.SetPen(self._pen)
  377. self.unselect()
  378. def set_linewidth(self, w):
  379. # docstring inherited
  380. w = float(w)
  381. DEBUG_MSG("set_linewidth()", 1, self)
  382. self.select()
  383. if 0 < w < 1:
  384. w = 1
  385. GraphicsContextBase.set_linewidth(self, w)
  386. lw = int(self.renderer.points_to_pixels(self._linewidth))
  387. if lw == 0:
  388. lw = 1
  389. self._pen.SetWidth(lw)
  390. self.gfx_ctx.SetPen(self._pen)
  391. self.unselect()
  392. def set_capstyle(self, cs):
  393. # docstring inherited
  394. DEBUG_MSG("set_capstyle()", 1, self)
  395. self.select()
  396. GraphicsContextBase.set_capstyle(self, cs)
  397. self._pen.SetCap(GraphicsContextWx._capd[self._capstyle])
  398. self.gfx_ctx.SetPen(self._pen)
  399. self.unselect()
  400. def set_joinstyle(self, js):
  401. # docstring inherited
  402. DEBUG_MSG("set_joinstyle()", 1, self)
  403. self.select()
  404. GraphicsContextBase.set_joinstyle(self, js)
  405. self._pen.SetJoin(GraphicsContextWx._joind[self._joinstyle])
  406. self.gfx_ctx.SetPen(self._pen)
  407. self.unselect()
  408. def get_wxcolour(self, color):
  409. """return a wx.Colour from RGB format"""
  410. DEBUG_MSG("get_wx_color()", 1, self)
  411. if len(color) == 3:
  412. r, g, b = color
  413. r *= 255
  414. g *= 255
  415. b *= 255
  416. return wx.Colour(red=int(r), green=int(g), blue=int(b))
  417. else:
  418. r, g, b, a = color
  419. r *= 255
  420. g *= 255
  421. b *= 255
  422. a *= 255
  423. return wx.Colour(
  424. red=int(r),
  425. green=int(g),
  426. blue=int(b),
  427. alpha=int(a))
  428. class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel):
  429. """
  430. The FigureCanvas contains the figure and does event handling.
  431. In the wxPython backend, it is derived from wxPanel, and (usually) lives
  432. inside a frame instantiated by a FigureManagerWx. The parent window
  433. probably implements a wx.Sizer to control the displayed control size - but
  434. we give a hint as to our preferred minimum size.
  435. """
  436. required_interactive_framework = "wx"
  437. keyvald = {
  438. wx.WXK_CONTROL: 'control',
  439. wx.WXK_SHIFT: 'shift',
  440. wx.WXK_ALT: 'alt',
  441. wx.WXK_LEFT: 'left',
  442. wx.WXK_UP: 'up',
  443. wx.WXK_RIGHT: 'right',
  444. wx.WXK_DOWN: 'down',
  445. wx.WXK_ESCAPE: 'escape',
  446. wx.WXK_F1: 'f1',
  447. wx.WXK_F2: 'f2',
  448. wx.WXK_F3: 'f3',
  449. wx.WXK_F4: 'f4',
  450. wx.WXK_F5: 'f5',
  451. wx.WXK_F6: 'f6',
  452. wx.WXK_F7: 'f7',
  453. wx.WXK_F8: 'f8',
  454. wx.WXK_F9: 'f9',
  455. wx.WXK_F10: 'f10',
  456. wx.WXK_F11: 'f11',
  457. wx.WXK_F12: 'f12',
  458. wx.WXK_SCROLL: 'scroll_lock',
  459. wx.WXK_PAUSE: 'break',
  460. wx.WXK_BACK: 'backspace',
  461. wx.WXK_RETURN: 'enter',
  462. wx.WXK_INSERT: 'insert',
  463. wx.WXK_DELETE: 'delete',
  464. wx.WXK_HOME: 'home',
  465. wx.WXK_END: 'end',
  466. wx.WXK_PAGEUP: 'pageup',
  467. wx.WXK_PAGEDOWN: 'pagedown',
  468. wx.WXK_NUMPAD0: '0',
  469. wx.WXK_NUMPAD1: '1',
  470. wx.WXK_NUMPAD2: '2',
  471. wx.WXK_NUMPAD3: '3',
  472. wx.WXK_NUMPAD4: '4',
  473. wx.WXK_NUMPAD5: '5',
  474. wx.WXK_NUMPAD6: '6',
  475. wx.WXK_NUMPAD7: '7',
  476. wx.WXK_NUMPAD8: '8',
  477. wx.WXK_NUMPAD9: '9',
  478. wx.WXK_NUMPAD_ADD: '+',
  479. wx.WXK_NUMPAD_SUBTRACT: '-',
  480. wx.WXK_NUMPAD_MULTIPLY: '*',
  481. wx.WXK_NUMPAD_DIVIDE: '/',
  482. wx.WXK_NUMPAD_DECIMAL: 'dec',
  483. wx.WXK_NUMPAD_ENTER: 'enter',
  484. wx.WXK_NUMPAD_UP: 'up',
  485. wx.WXK_NUMPAD_RIGHT: 'right',
  486. wx.WXK_NUMPAD_DOWN: 'down',
  487. wx.WXK_NUMPAD_LEFT: 'left',
  488. wx.WXK_NUMPAD_PAGEUP: 'pageup',
  489. wx.WXK_NUMPAD_PAGEDOWN: 'pagedown',
  490. wx.WXK_NUMPAD_HOME: 'home',
  491. wx.WXK_NUMPAD_END: 'end',
  492. wx.WXK_NUMPAD_INSERT: 'insert',
  493. wx.WXK_NUMPAD_DELETE: 'delete',
  494. }
  495. def __init__(self, parent, id, figure):
  496. """
  497. Initialise a FigureWx instance.
  498. - Initialise the FigureCanvasBase and wxPanel parents.
  499. - Set event handlers for:
  500. EVT_SIZE (Resize event)
  501. EVT_PAINT (Paint event)
  502. """
  503. FigureCanvasBase.__init__(self, figure)
  504. # Set preferred window size hint - helps the sizer (if one is
  505. # connected)
  506. l, b, w, h = figure.bbox.bounds
  507. w = math.ceil(w)
  508. h = math.ceil(h)
  509. wx.Panel.__init__(self, parent, id, size=wx.Size(w, h))
  510. # Create the drawing bitmap
  511. self.bitmap = wx.Bitmap(w, h)
  512. DEBUG_MSG("__init__() - bitmap w:%d h:%d" % (w, h), 2, self)
  513. # TODO: Add support for 'point' inspection and plot navigation.
  514. self._isDrawn = False
  515. self.Bind(wx.EVT_SIZE, self._onSize)
  516. self.Bind(wx.EVT_PAINT, self._onPaint)
  517. self.Bind(wx.EVT_KEY_DOWN, self._onKeyDown)
  518. self.Bind(wx.EVT_KEY_UP, self._onKeyUp)
  519. self.Bind(wx.EVT_LEFT_DOWN, self._onMouseButton)
  520. self.Bind(wx.EVT_LEFT_DCLICK, self._onMouseButton)
  521. self.Bind(wx.EVT_LEFT_UP, self._onMouseButton)
  522. self.Bind(wx.EVT_MIDDLE_DOWN, self._onMouseButton)
  523. self.Bind(wx.EVT_MIDDLE_DCLICK, self._onMouseButton)
  524. self.Bind(wx.EVT_MIDDLE_UP, self._onMouseButton)
  525. self.Bind(wx.EVT_RIGHT_DOWN, self._onMouseButton)
  526. self.Bind(wx.EVT_RIGHT_DCLICK, self._onMouseButton)
  527. self.Bind(wx.EVT_RIGHT_UP, self._onMouseButton)
  528. self.Bind(wx.EVT_MOUSEWHEEL, self._onMouseWheel)
  529. self.Bind(wx.EVT_MOTION, self._onMotion)
  530. self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeave)
  531. self.Bind(wx.EVT_ENTER_WINDOW, self._onEnter)
  532. self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._onCaptureLost)
  533. self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._onCaptureLost)
  534. self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker.
  535. self.SetBackgroundColour(wx.WHITE)
  536. def Copy_to_Clipboard(self, event=None):
  537. "copy bitmap of canvas to system clipboard"
  538. bmp_obj = wx.BitmapDataObject()
  539. bmp_obj.SetBitmap(self.bitmap)
  540. if not wx.TheClipboard.IsOpened():
  541. open_success = wx.TheClipboard.Open()
  542. if open_success:
  543. wx.TheClipboard.SetData(bmp_obj)
  544. wx.TheClipboard.Close()
  545. wx.TheClipboard.Flush()
  546. def draw_idle(self):
  547. # docstring inherited
  548. DEBUG_MSG("draw_idle()", 1, self)
  549. self._isDrawn = False # Force redraw
  550. # Triggering a paint event is all that is needed to defer drawing
  551. # until later. The platform will send the event when it thinks it is
  552. # a good time (usually as soon as there are no other events pending).
  553. self.Refresh(eraseBackground=False)
  554. def new_timer(self, *args, **kwargs):
  555. # docstring inherited
  556. return TimerWx(*args, **kwargs)
  557. def flush_events(self):
  558. # docstring inherited
  559. wx.Yield()
  560. def start_event_loop(self, timeout=0):
  561. # docstring inherited
  562. if hasattr(self, '_event_loop'):
  563. raise RuntimeError("Event loop already running")
  564. timer = wx.Timer(self, id=wx.ID_ANY)
  565. if timeout > 0:
  566. timer.Start(timeout * 1000, oneShot=True)
  567. self.Bind(wx.EVT_TIMER, self.stop_event_loop, id=timer.GetId())
  568. # Event loop handler for start/stop event loop
  569. self._event_loop = wx.GUIEventLoop()
  570. self._event_loop.Run()
  571. timer.Stop()
  572. def stop_event_loop(self, event=None):
  573. # docstring inherited
  574. if hasattr(self, '_event_loop'):
  575. if self._event_loop.IsRunning():
  576. self._event_loop.Exit()
  577. del self._event_loop
  578. def _get_imagesave_wildcards(self):
  579. 'return the wildcard string for the filesave dialog'
  580. default_filetype = self.get_default_filetype()
  581. filetypes = self.get_supported_filetypes_grouped()
  582. sorted_filetypes = sorted(filetypes.items())
  583. wildcards = []
  584. extensions = []
  585. filter_index = 0
  586. for i, (name, exts) in enumerate(sorted_filetypes):
  587. ext_list = ';'.join(['*.%s' % ext for ext in exts])
  588. extensions.append(exts[0])
  589. wildcard = '%s (%s)|%s' % (name, ext_list, ext_list)
  590. if default_filetype in exts:
  591. filter_index = i
  592. wildcards.append(wildcard)
  593. wildcards = '|'.join(wildcards)
  594. return wildcards, extensions, filter_index
  595. def gui_repaint(self, drawDC=None, origin='WX'):
  596. """
  597. Performs update of the displayed image on the GUI canvas, using the
  598. supplied wx.PaintDC device context.
  599. The 'WXAgg' backend sets origin accordingly.
  600. """
  601. DEBUG_MSG("gui_repaint()", 1, self)
  602. if self.IsShownOnScreen():
  603. if not drawDC:
  604. # not called from OnPaint use a ClientDC
  605. drawDC = wx.ClientDC(self)
  606. # following is for 'WX' backend on Windows
  607. # the bitmap can not be in use by another DC,
  608. # see GraphicsContextWx._cache
  609. if wx.Platform == '__WXMSW__' and origin == 'WX':
  610. img = self.bitmap.ConvertToImage()
  611. bmp = img.ConvertToBitmap()
  612. drawDC.DrawBitmap(bmp, 0, 0)
  613. else:
  614. drawDC.DrawBitmap(self.bitmap, 0, 0)
  615. filetypes = {
  616. **FigureCanvasBase.filetypes,
  617. 'bmp': 'Windows bitmap',
  618. 'jpeg': 'JPEG',
  619. 'jpg': 'JPEG',
  620. 'pcx': 'PCX',
  621. 'png': 'Portable Network Graphics',
  622. 'tif': 'Tagged Image Format File',
  623. 'tiff': 'Tagged Image Format File',
  624. 'xpm': 'X pixmap',
  625. }
  626. def print_figure(self, filename, *args, **kwargs):
  627. # docstring inherited
  628. super().print_figure(filename, *args, **kwargs)
  629. # Restore the current view; this is needed because the artist contains
  630. # methods rely on particular attributes of the rendered figure for
  631. # determining things like bounding boxes.
  632. if self._isDrawn:
  633. self.draw()
  634. def _onPaint(self, evt):
  635. """
  636. Called when wxPaintEvt is generated
  637. """
  638. DEBUG_MSG("_onPaint()", 1, self)
  639. drawDC = wx.PaintDC(self)
  640. if not self._isDrawn:
  641. self.draw(drawDC=drawDC)
  642. else:
  643. self.gui_repaint(drawDC=drawDC)
  644. drawDC.Destroy()
  645. def _onSize(self, evt):
  646. """
  647. Called when wxEventSize is generated.
  648. In this application we attempt to resize to fit the window, so it
  649. is better to take the performance hit and redraw the whole window.
  650. """
  651. DEBUG_MSG("_onSize()", 2, self)
  652. sz = self.GetParent().GetSizer()
  653. if sz:
  654. si = sz.GetItem(self)
  655. if sz and si and not si.Proportion and not si.Flag & wx.EXPAND:
  656. # managed by a sizer, but with a fixed size
  657. size = self.GetMinSize()
  658. else:
  659. # variable size
  660. size = self.GetClientSize()
  661. # Do not allow size to become smaller than MinSize
  662. size.IncTo(self.GetMinSize())
  663. if getattr(self, "_width", None):
  664. if size == (self._width, self._height):
  665. # no change in size
  666. return
  667. self._width, self._height = size
  668. # Create a new, correctly sized bitmap
  669. self.bitmap = wx.Bitmap(self._width, self._height)
  670. self._isDrawn = False
  671. if self._width <= 1 or self._height <= 1:
  672. return # Empty figure
  673. dpival = self.figure.dpi
  674. winch = self._width / dpival
  675. hinch = self._height / dpival
  676. self.figure.set_size_inches(winch, hinch, forward=False)
  677. # Rendering will happen on the associated paint event
  678. # so no need to do anything here except to make sure
  679. # the whole background is repainted.
  680. self.Refresh(eraseBackground=False)
  681. FigureCanvasBase.resize_event(self)
  682. def _get_key(self, evt):
  683. keyval = evt.KeyCode
  684. if keyval in self.keyvald:
  685. key = self.keyvald[keyval]
  686. elif keyval < 256:
  687. key = chr(keyval)
  688. # wx always returns an uppercase, so make it lowercase if the shift
  689. # key is not depressed (NOTE: this will not handle Caps Lock)
  690. if not evt.ShiftDown():
  691. key = key.lower()
  692. else:
  693. key = None
  694. for meth, prefix in (
  695. [evt.AltDown, 'alt'],
  696. [evt.ControlDown, 'ctrl'], ):
  697. if meth():
  698. key = '{0}+{1}'.format(prefix, key)
  699. return key
  700. def _onKeyDown(self, evt):
  701. """Capture key press."""
  702. key = self._get_key(evt)
  703. FigureCanvasBase.key_press_event(self, key, guiEvent=evt)
  704. if self:
  705. evt.Skip()
  706. def _onKeyUp(self, evt):
  707. """Release key."""
  708. key = self._get_key(evt)
  709. FigureCanvasBase.key_release_event(self, key, guiEvent=evt)
  710. if self:
  711. evt.Skip()
  712. def _set_capture(self, capture=True):
  713. """Control wx mouse capture."""
  714. if self.HasCapture():
  715. self.ReleaseMouse()
  716. if capture:
  717. self.CaptureMouse()
  718. def _onCaptureLost(self, evt):
  719. """Capture changed or lost"""
  720. self._set_capture(False)
  721. def _onMouseButton(self, evt):
  722. """Start measuring on an axis."""
  723. evt.Skip()
  724. self._set_capture(evt.ButtonDown() or evt.ButtonDClick())
  725. x = evt.X
  726. y = self.figure.bbox.height - evt.Y
  727. button_map = {
  728. wx.MOUSE_BTN_LEFT: MouseButton.LEFT,
  729. wx.MOUSE_BTN_MIDDLE: MouseButton.MIDDLE,
  730. wx.MOUSE_BTN_RIGHT: MouseButton.RIGHT,
  731. }
  732. button = evt.GetButton()
  733. button = button_map.get(button, button)
  734. if evt.ButtonDown():
  735. self.button_press_event(x, y, button, guiEvent=evt)
  736. elif evt.ButtonDClick():
  737. self.button_press_event(x, y, button, dblclick=True, guiEvent=evt)
  738. elif evt.ButtonUp():
  739. self.button_release_event(x, y, button, guiEvent=evt)
  740. def _onMouseWheel(self, evt):
  741. """Translate mouse wheel events into matplotlib events"""
  742. # Determine mouse location
  743. x = evt.GetX()
  744. y = self.figure.bbox.height - evt.GetY()
  745. # Convert delta/rotation/rate into a floating point step size
  746. delta = evt.GetWheelDelta()
  747. rotation = evt.GetWheelRotation()
  748. rate = evt.GetLinesPerAction()
  749. step = rate * rotation / delta
  750. # Done handling event
  751. evt.Skip()
  752. # Mac is giving two events for every wheel event
  753. # Need to skip every second one
  754. if wx.Platform == '__WXMAC__':
  755. if not hasattr(self, '_skipwheelevent'):
  756. self._skipwheelevent = True
  757. elif self._skipwheelevent:
  758. self._skipwheelevent = False
  759. return # Return without processing event
  760. else:
  761. self._skipwheelevent = True
  762. FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=evt)
  763. def _onMotion(self, evt):
  764. """Start measuring on an axis."""
  765. x = evt.GetX()
  766. y = self.figure.bbox.height - evt.GetY()
  767. evt.Skip()
  768. FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=evt)
  769. def _onLeave(self, evt):
  770. """Mouse has left the window."""
  771. evt.Skip()
  772. FigureCanvasBase.leave_notify_event(self, guiEvent=evt)
  773. def _onEnter(self, evt):
  774. """Mouse has entered the window."""
  775. x = evt.GetX()
  776. y = self.figure.bbox.height - evt.GetY()
  777. evt.Skip()
  778. FigureCanvasBase.enter_notify_event(self, guiEvent=evt, xy=(x, y))
  779. class FigureCanvasWx(_FigureCanvasWxBase):
  780. # Rendering to a Wx canvas using the deprecated Wx renderer.
  781. def draw(self, drawDC=None):
  782. """
  783. Render the figure using RendererWx instance renderer, or using a
  784. previously defined renderer if none is specified.
  785. """
  786. DEBUG_MSG("draw()", 1, self)
  787. self.renderer = RendererWx(self.bitmap, self.figure.dpi)
  788. self.figure.draw(self.renderer)
  789. self._isDrawn = True
  790. self.gui_repaint(drawDC=drawDC)
  791. def print_bmp(self, filename, *args, **kwargs):
  792. return self._print_image(filename, wx.BITMAP_TYPE_BMP, *args, **kwargs)
  793. if not _has_pil:
  794. def print_jpeg(self, filename, *args, **kwargs):
  795. return self._print_image(filename, wx.BITMAP_TYPE_JPEG,
  796. *args, **kwargs)
  797. print_jpg = print_jpeg
  798. def print_pcx(self, filename, *args, **kwargs):
  799. return self._print_image(filename, wx.BITMAP_TYPE_PCX, *args, **kwargs)
  800. def print_png(self, filename, *args, **kwargs):
  801. return self._print_image(filename, wx.BITMAP_TYPE_PNG, *args, **kwargs)
  802. if not _has_pil:
  803. def print_tiff(self, filename, *args, **kwargs):
  804. return self._print_image(filename, wx.BITMAP_TYPE_TIF,
  805. *args, **kwargs)
  806. print_tif = print_tiff
  807. def print_xpm(self, filename, *args, **kwargs):
  808. return self._print_image(filename, wx.BITMAP_TYPE_XPM, *args, **kwargs)
  809. def _print_image(self, filename, filetype, *args, **kwargs):
  810. origBitmap = self.bitmap
  811. l, b, width, height = self.figure.bbox.bounds
  812. width = math.ceil(width)
  813. height = math.ceil(height)
  814. self.bitmap = wx.Bitmap(width, height)
  815. renderer = RendererWx(self.bitmap, self.figure.dpi)
  816. gc = renderer.new_gc()
  817. self.figure.draw(renderer)
  818. # image is the object that we call SaveFile on.
  819. image = self.bitmap
  820. # set the JPEG quality appropriately. Unfortunately, it is only
  821. # possible to set the quality on a wx.Image object. So if we
  822. # are saving a JPEG, convert the wx.Bitmap to a wx.Image,
  823. # and set the quality.
  824. if filetype == wx.BITMAP_TYPE_JPEG:
  825. jpeg_quality = kwargs.get('quality',
  826. rcParams['savefig.jpeg_quality'])
  827. image = self.bitmap.ConvertToImage()
  828. image.SetOption(wx.IMAGE_OPTION_QUALITY, str(jpeg_quality))
  829. # Now that we have rendered into the bitmap, save it to the appropriate
  830. # file type and clean up.
  831. if isinstance(filename, str):
  832. if not image.SaveFile(filename, filetype):
  833. raise RuntimeError(f'Could not save figure to {filename}')
  834. elif cbook.is_writable_file_like(filename):
  835. if not isinstance(image, wx.Image):
  836. image = image.ConvertToImage()
  837. if not image.SaveStream(filename, filetype):
  838. raise RuntimeError(f'Could not save figure to {filename}')
  839. # Restore everything to normal
  840. self.bitmap = origBitmap
  841. # Note: draw is required here since bits of state about the
  842. # last renderer are strewn about the artist draw methods. Do
  843. # not remove the draw without first verifying that these have
  844. # been cleaned up. The artist contains() methods will fail
  845. # otherwise.
  846. if self._isDrawn:
  847. self.draw()
  848. self.Refresh()
  849. ########################################################################
  850. #
  851. # The following functions and classes are for pylab compatibility
  852. # mode (matplotlib.pylab) and implement figure managers, etc...
  853. #
  854. ########################################################################
  855. class FigureFrameWx(wx.Frame):
  856. def __init__(self, num, fig):
  857. # On non-Windows platform, explicitly set the position - fix
  858. # positioning bug on some Linux platforms
  859. if wx.Platform == '__WXMSW__':
  860. pos = wx.DefaultPosition
  861. else:
  862. pos = wx.Point(20, 20)
  863. wx.Frame.__init__(self, parent=None, id=-1, pos=pos,
  864. title="Figure %d" % num)
  865. # Frame will be sized later by the Fit method
  866. DEBUG_MSG("__init__()", 1, self)
  867. self.num = num
  868. _set_frame_icon(self)
  869. self.canvas = self.get_canvas(fig)
  870. self.canvas.SetInitialSize(wx.Size(fig.bbox.width, fig.bbox.height))
  871. self.canvas.SetFocus()
  872. self.sizer = wx.BoxSizer(wx.VERTICAL)
  873. self.sizer.Add(self.canvas, 1, wx.TOP | wx.LEFT | wx.EXPAND)
  874. # By adding toolbar in sizer, we are able to put it at the bottom
  875. # of the frame - so appearance is closer to GTK version
  876. self.toolmanager = self._get_toolmanager()
  877. statusbar = (StatusbarWx(self, self.toolmanager)
  878. if self.toolmanager else StatusBarWx(self))
  879. self.SetStatusBar(statusbar)
  880. self.toolbar = self._get_toolbar()
  881. if self.toolmanager:
  882. backend_tools.add_tools_to_manager(self.toolmanager)
  883. if self.toolbar:
  884. backend_tools.add_tools_to_container(self.toolbar)
  885. if self.toolbar is not None:
  886. self.toolbar.Realize()
  887. # On Windows platform, default window size is incorrect, so set
  888. # toolbar width to figure width.
  889. tw, th = self.toolbar.GetSize()
  890. fw, fh = self.canvas.GetSize()
  891. # By adding toolbar in sizer, we are able to put it at the bottom
  892. # of the frame - so appearance is closer to GTK version.
  893. self.toolbar.SetSize(wx.Size(fw, th))
  894. self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
  895. self.SetSizer(self.sizer)
  896. self.Fit()
  897. self.canvas.SetMinSize((2, 2))
  898. self.figmgr = FigureManagerWx(self.canvas, num, self)
  899. self.Bind(wx.EVT_CLOSE, self._onClose)
  900. @cbook.deprecated("3.2", alternative="self.GetStatusBar()")
  901. @property
  902. def statusbar(self):
  903. return self.GetStatusBar()
  904. def _get_toolbar(self):
  905. if rcParams['toolbar'] == 'toolbar2':
  906. toolbar = NavigationToolbar2Wx(self.canvas)
  907. elif matplotlib.rcParams['toolbar'] == 'toolmanager':
  908. toolbar = ToolbarWx(self.toolmanager, self)
  909. else:
  910. toolbar = None
  911. return toolbar
  912. def _get_toolmanager(self):
  913. if matplotlib.rcParams['toolbar'] == 'toolmanager':
  914. toolmanager = ToolManager(self.canvas.figure)
  915. else:
  916. toolmanager = None
  917. return toolmanager
  918. def get_canvas(self, fig):
  919. return FigureCanvasWx(self, -1, fig)
  920. def get_figure_manager(self):
  921. DEBUG_MSG("get_figure_manager()", 1, self)
  922. return self.figmgr
  923. def _onClose(self, evt):
  924. DEBUG_MSG("onClose()", 1, self)
  925. self.canvas.close_event()
  926. self.canvas.stop_event_loop()
  927. Gcf.destroy(self.num)
  928. # self.Destroy()
  929. def GetToolBar(self):
  930. """Override wxFrame::GetToolBar as we don't have managed toolbar"""
  931. return self.toolbar
  932. def Destroy(self, *args, **kwargs):
  933. try:
  934. self.canvas.mpl_disconnect(self.toolbar._idDrag)
  935. # Rationale for line above: see issue 2941338.
  936. except AttributeError:
  937. pass # classic toolbar lacks the attribute
  938. if not self.IsBeingDeleted():
  939. wx.Frame.Destroy(self, *args, **kwargs)
  940. if self.toolbar is not None:
  941. self.toolbar.Destroy()
  942. wxapp = wx.GetApp()
  943. if wxapp:
  944. wxapp.Yield()
  945. return True
  946. class FigureManagerWx(FigureManagerBase):
  947. """
  948. This class contains the FigureCanvas and GUI frame
  949. It is instantiated by GcfWx whenever a new figure is created. GcfWx is
  950. responsible for managing multiple instances of FigureManagerWx.
  951. Attributes
  952. ----------
  953. canvas : `FigureCanvas`
  954. a FigureCanvasWx(wx.Panel) instance
  955. window : wxFrame
  956. a wxFrame instance - wxpython.org/Phoenix/docs/html/Frame.html
  957. """
  958. def __init__(self, canvas, num, frame):
  959. DEBUG_MSG("__init__()", 1, self)
  960. FigureManagerBase.__init__(self, canvas, num)
  961. self.frame = frame
  962. self.window = frame
  963. self.toolmanager = getattr(frame, "toolmanager", None)
  964. self.toolbar = frame.GetToolBar()
  965. def show(self):
  966. self.frame.Show()
  967. self.canvas.draw()
  968. def destroy(self, *args):
  969. DEBUG_MSG("destroy()", 1, self)
  970. self.frame.Destroy()
  971. wxapp = wx.GetApp()
  972. if wxapp:
  973. wxapp.Yield()
  974. def get_window_title(self):
  975. return self.window.GetTitle()
  976. def set_window_title(self, title):
  977. self.window.SetTitle(title)
  978. def resize(self, width, height):
  979. 'Set the canvas size in pixels'
  980. self.canvas.SetInitialSize(wx.Size(width, height))
  981. self.window.GetSizer().Fit(self.window)
  982. def _load_bitmap(filename):
  983. """
  984. Load a bitmap file from the backends/images subdirectory in which the
  985. matplotlib library is installed. The filename parameter should not
  986. contain any path information as this is determined automatically.
  987. Returns a wx.Bitmap object.
  988. """
  989. path = cbook._get_data_path('images', filename)
  990. if not path.exists():
  991. raise IOError(f"Could not find bitmap file '{path}'; dying")
  992. return wx.Bitmap(str(path))
  993. def _set_frame_icon(frame):
  994. bundle = wx.IconBundle()
  995. for image in ('matplotlib.png', 'matplotlib_large.png'):
  996. try:
  997. icon = wx.Icon(_load_bitmap(image))
  998. except IOError:
  999. continue
  1000. if not icon.IsOk():
  1001. return
  1002. bundle.AddIcon(icon)
  1003. frame.SetIcons(bundle)
  1004. @cbook.deprecated("3.1")
  1005. class MenuButtonWx(wx.Button):
  1006. """
  1007. wxPython does not permit a menu to be incorporated directly into a toolbar.
  1008. This class simulates the effect by associating a pop-up menu with a button
  1009. in the toolbar, and managing this as though it were a menu.
  1010. """
  1011. def __init__(self, parent):
  1012. wx.Button.__init__(self, parent, wx.ID_ANY, "Axes: ",
  1013. style=wx.BU_EXACTFIT)
  1014. self._toolbar = parent
  1015. self._menu = wx.Menu()
  1016. self._axisId = []
  1017. # First two menu items never change...
  1018. self._allId = wx.NewId()
  1019. self._invertId = wx.NewId()
  1020. self._menu.Append(self._allId, "All", "Select all axes", False)
  1021. self._menu.Append(self._invertId, "Invert", "Invert axes selected",
  1022. False)
  1023. self._menu.AppendSeparator()
  1024. self.Bind(wx.EVT_BUTTON, self._onMenuButton, id=self.GetId())
  1025. self.Bind(wx.EVT_MENU, self._handleSelectAllAxes, id=self._allId)
  1026. self.Bind(wx.EVT_MENU, self._handleInvertAxesSelected,
  1027. id=self._invertId)
  1028. def Destroy(self):
  1029. self._menu.Destroy()
  1030. self.Destroy()
  1031. def _onMenuButton(self, evt):
  1032. """Handle menu button pressed."""
  1033. x, y = self.GetPosition()
  1034. w, h = self.GetSize()
  1035. self.PopupMenuXY(self._menu, x, y + h - 4)
  1036. # When menu returned, indicate selection in button
  1037. evt.Skip()
  1038. def _handleSelectAllAxes(self, evt):
  1039. """Called when the 'select all axes' menu item is selected."""
  1040. if len(self._axisId) == 0:
  1041. return
  1042. for i in range(len(self._axisId)):
  1043. self._menu.Check(self._axisId[i], True)
  1044. self._toolbar.set_active(self.getActiveAxes())
  1045. evt.Skip()
  1046. def _handleInvertAxesSelected(self, evt):
  1047. """Called when the invert all menu item is selected"""
  1048. if len(self._axisId) == 0:
  1049. return
  1050. for i in range(len(self._axisId)):
  1051. if self._menu.IsChecked(self._axisId[i]):
  1052. self._menu.Check(self._axisId[i], False)
  1053. else:
  1054. self._menu.Check(self._axisId[i], True)
  1055. self._toolbar.set_active(self.getActiveAxes())
  1056. evt.Skip()
  1057. def _onMenuItemSelected(self, evt):
  1058. """Called whenever one of the specific axis menu items is selected"""
  1059. current = self._menu.IsChecked(evt.GetId())
  1060. if current:
  1061. new = False
  1062. else:
  1063. new = True
  1064. self._menu.Check(evt.GetId(), new)
  1065. # Lines above would be deleted based on svn tracker ID 2841525;
  1066. # not clear whether this matters or not.
  1067. self._toolbar.set_active(self.getActiveAxes())
  1068. evt.Skip()
  1069. def updateAxes(self, maxAxis):
  1070. """Ensures that there are entries for max_axis axes in the menu
  1071. (selected by default)."""
  1072. if maxAxis > len(self._axisId):
  1073. for i in range(len(self._axisId) + 1, maxAxis + 1):
  1074. menuId = wx.NewId()
  1075. self._axisId.append(menuId)
  1076. self._menu.Append(menuId, "Axis %d" % i,
  1077. "Select axis %d" % i,
  1078. True)
  1079. self._menu.Check(menuId, True)
  1080. self.Bind(wx.EVT_MENU, self._onMenuItemSelected, id=menuId)
  1081. elif maxAxis < len(self._axisId):
  1082. for menuId in self._axisId[maxAxis:]:
  1083. self._menu.Delete(menuId)
  1084. self._axisId = self._axisId[:maxAxis]
  1085. self._toolbar.set_active(list(range(maxAxis)))
  1086. def getActiveAxes(self):
  1087. """Return a list of the selected axes."""
  1088. active = [idx for idx, ax_id in enumerate(self._axisId)
  1089. if self._menu.IsChecked(ax_id)]
  1090. return active
  1091. def updateButtonText(self, lst):
  1092. """Update the list of selected axes in the menu button."""
  1093. self.SetLabel(
  1094. 'Axes: ' + ','.join('%d' % (e + 1) for e in lst))
  1095. cursord = {
  1096. cursors.MOVE: wx.CURSOR_HAND,
  1097. cursors.HAND: wx.CURSOR_HAND,
  1098. cursors.POINTER: wx.CURSOR_ARROW,
  1099. cursors.SELECT_REGION: wx.CURSOR_CROSS,
  1100. cursors.WAIT: wx.CURSOR_WAIT,
  1101. }
  1102. class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar):
  1103. def __init__(self, canvas):
  1104. wx.ToolBar.__init__(self, canvas.GetParent(), -1)
  1105. NavigationToolbar2.__init__(self, canvas)
  1106. self.canvas = canvas
  1107. self._idle = True
  1108. self.prevZoomRect = None
  1109. # for now, use alternate zoom-rectangle drawing on all
  1110. # Macs. N.B. In future versions of wx it may be possible to
  1111. # detect Retina displays with window.GetContentScaleFactor()
  1112. # and/or dc.GetContentScaleFactor()
  1113. self.retinaFix = 'wxMac' in wx.PlatformInfo
  1114. def get_canvas(self, frame, fig):
  1115. return type(self.canvas)(frame, -1, fig)
  1116. def _init_toolbar(self):
  1117. DEBUG_MSG("_init_toolbar", 1, self)
  1118. self._parent = self.canvas.GetParent()
  1119. self.wx_ids = {}
  1120. for text, tooltip_text, image_file, callback in self.toolitems:
  1121. if text is None:
  1122. self.AddSeparator()
  1123. continue
  1124. self.wx_ids[text] = (
  1125. self.AddTool(
  1126. -1,
  1127. bitmap=_load_bitmap(image_file + ".png"),
  1128. bmpDisabled=wx.NullBitmap,
  1129. label=text, shortHelp=text, longHelp=tooltip_text,
  1130. kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"]
  1131. else wx.ITEM_NORMAL))
  1132. .Id)
  1133. self.Bind(wx.EVT_TOOL, getattr(self, callback),
  1134. id=self.wx_ids[text])
  1135. self.Realize()
  1136. def zoom(self, *args):
  1137. self.ToggleTool(self.wx_ids['Pan'], False)
  1138. NavigationToolbar2.zoom(self, *args)
  1139. def pan(self, *args):
  1140. self.ToggleTool(self.wx_ids['Zoom'], False)
  1141. NavigationToolbar2.pan(self, *args)
  1142. def configure_subplots(self, *args):
  1143. global FigureManager # placates pyflakes: created by @_Backend.export
  1144. frame = wx.Frame(None, -1, "Configure subplots")
  1145. _set_frame_icon(frame)
  1146. toolfig = Figure((6, 3))
  1147. canvas = type(self.canvas)(frame, -1, toolfig)
  1148. # Create a figure manager to manage things
  1149. FigureManager(canvas, 1, frame)
  1150. # Now put all into a sizer
  1151. sizer = wx.BoxSizer(wx.VERTICAL)
  1152. # This way of adding to sizer allows resizing
  1153. sizer.Add(canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
  1154. frame.SetSizer(sizer)
  1155. frame.Fit()
  1156. SubplotTool(self.canvas.figure, toolfig)
  1157. frame.Show()
  1158. def save_figure(self, *args):
  1159. # Fetch the required filename and file type.
  1160. filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards()
  1161. default_file = self.canvas.get_default_filename()
  1162. dlg = wx.FileDialog(self.canvas.GetParent(),
  1163. "Save to file", "", default_file, filetypes,
  1164. wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
  1165. dlg.SetFilterIndex(filter_index)
  1166. if dlg.ShowModal() == wx.ID_OK:
  1167. dirname = dlg.GetDirectory()
  1168. filename = dlg.GetFilename()
  1169. DEBUG_MSG(
  1170. 'Save file dir:%s name:%s' %
  1171. (dirname, filename), 3, self)
  1172. format = exts[dlg.GetFilterIndex()]
  1173. basename, ext = os.path.splitext(filename)
  1174. if ext.startswith('.'):
  1175. ext = ext[1:]
  1176. if ext in ('svg', 'pdf', 'ps', 'eps', 'png') and format != ext:
  1177. # looks like they forgot to set the image type drop
  1178. # down, going with the extension.
  1179. _log.warning('extension %s did not match the selected '
  1180. 'image type %s; going with %s',
  1181. ext, format, ext)
  1182. format = ext
  1183. try:
  1184. self.canvas.figure.savefig(
  1185. os.path.join(dirname, filename), format=format)
  1186. except Exception as e:
  1187. error_msg_wx(str(e))
  1188. def set_cursor(self, cursor):
  1189. cursor = wx.Cursor(cursord[cursor])
  1190. self.canvas.SetCursor(cursor)
  1191. self.canvas.Update()
  1192. def press(self, event):
  1193. if self._active == 'ZOOM':
  1194. if not self.retinaFix:
  1195. self.wxoverlay = wx.Overlay()
  1196. else:
  1197. if event.inaxes is not None:
  1198. self.savedRetinaImage = self.canvas.copy_from_bbox(
  1199. event.inaxes.bbox)
  1200. self.zoomStartX = event.xdata
  1201. self.zoomStartY = event.ydata
  1202. self.zoomAxes = event.inaxes
  1203. def release(self, event):
  1204. if self._active == 'ZOOM':
  1205. # When the mouse is released we reset the overlay and it
  1206. # restores the former content to the window.
  1207. if not self.retinaFix:
  1208. self.wxoverlay.Reset()
  1209. del self.wxoverlay
  1210. else:
  1211. del self.savedRetinaImage
  1212. if self.prevZoomRect:
  1213. self.prevZoomRect.pop(0).remove()
  1214. self.prevZoomRect = None
  1215. if self.zoomAxes:
  1216. self.zoomAxes = None
  1217. def draw_rubberband(self, event, x0, y0, x1, y1):
  1218. if self.retinaFix: # On Macs, use the following code
  1219. # wx.DCOverlay does not work properly on Retina displays.
  1220. rubberBandColor = '#C0C0FF'
  1221. if self.prevZoomRect:
  1222. self.prevZoomRect.pop(0).remove()
  1223. self.canvas.restore_region(self.savedRetinaImage)
  1224. X0, X1 = self.zoomStartX, event.xdata
  1225. Y0, Y1 = self.zoomStartY, event.ydata
  1226. lineX = (X0, X0, X1, X1, X0)
  1227. lineY = (Y0, Y1, Y1, Y0, Y0)
  1228. self.prevZoomRect = self.zoomAxes.plot(
  1229. lineX, lineY, '-', color=rubberBandColor)
  1230. self.zoomAxes.draw_artist(self.prevZoomRect[0])
  1231. self.canvas.blit(self.zoomAxes.bbox)
  1232. return
  1233. # Use an Overlay to draw a rubberband-like bounding box.
  1234. dc = wx.ClientDC(self.canvas)
  1235. odc = wx.DCOverlay(self.wxoverlay, dc)
  1236. odc.Clear()
  1237. # Mac's DC is already the same as a GCDC, and it causes
  1238. # problems with the overlay if we try to use an actual
  1239. # wx.GCDC so don't try it.
  1240. if 'wxMac' not in wx.PlatformInfo:
  1241. dc = wx.GCDC(dc)
  1242. height = self.canvas.figure.bbox.height
  1243. y1 = height - y1
  1244. y0 = height - y0
  1245. if y1 < y0:
  1246. y0, y1 = y1, y0
  1247. if x1 < x0:
  1248. x0, x1 = x1, x0
  1249. w = x1 - x0
  1250. h = y1 - y0
  1251. rect = wx.Rect(x0, y0, w, h)
  1252. rubberBandColor = '#C0C0FF' # or load from config?
  1253. # Set a pen for the border
  1254. color = wx.Colour(rubberBandColor)
  1255. dc.SetPen(wx.Pen(color, 1))
  1256. # use the same color, plus alpha for the brush
  1257. r, g, b, a = color.Get(True)
  1258. color.Set(r, g, b, 0x60)
  1259. dc.SetBrush(wx.Brush(color))
  1260. dc.DrawRectangle(rect)
  1261. @cbook.deprecated("3.2")
  1262. def set_status_bar(self, statbar):
  1263. self.GetTopLevelParent().SetStatusBar(statbar)
  1264. @cbook.deprecated("3.2",
  1265. alternative="self.GetTopLevelParent().GetStatusBar()")
  1266. @property
  1267. def statbar(self):
  1268. return self.GetTopLevelParent().GetStatusBar()
  1269. def set_message(self, s):
  1270. status_bar = self.GetTopLevelParent().GetStatusBar()
  1271. if status_bar is not None:
  1272. status_bar.set_function(s)
  1273. def set_history_buttons(self):
  1274. can_backward = self._nav_stack._pos > 0
  1275. can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
  1276. if 'Back' in self.wx_ids:
  1277. self.EnableTool(self.wx_ids['Back'], can_backward)
  1278. if 'Forward' in self.wx_ids:
  1279. self.EnableTool(self.wx_ids['Forward'], can_forward)
  1280. class StatusBarWx(wx.StatusBar):
  1281. """
  1282. A status bar is added to _FigureFrame to allow measurements and the
  1283. previously selected scroll function to be displayed as a user
  1284. convenience.
  1285. """
  1286. def __init__(self, parent, *args, **kwargs):
  1287. wx.StatusBar.__init__(self, parent, -1)
  1288. self.SetFieldsCount(2)
  1289. def set_function(self, string):
  1290. self.SetStatusText("%s" % string, 1)
  1291. # tools for matplotlib.backend_managers.ToolManager:
  1292. class ToolbarWx(ToolContainerBase, wx.ToolBar):
  1293. def __init__(self, toolmanager, parent, style=wx.TB_HORIZONTAL):
  1294. ToolContainerBase.__init__(self, toolmanager)
  1295. wx.ToolBar.__init__(self, parent, -1, style=style)
  1296. self._toolitems = {}
  1297. self._groups = {}
  1298. def add_toolitem(
  1299. self, name, group, position, image_file, description, toggle):
  1300. before, group = self._add_to_group(group, name, position)
  1301. idx = self.GetToolPos(before.Id)
  1302. if image_file:
  1303. bmp = _load_bitmap(image_file)
  1304. kind = wx.ITEM_NORMAL if not toggle else wx.ITEM_CHECK
  1305. tool = self.InsertTool(idx, -1, name, bmp, wx.NullBitmap, kind,
  1306. description or "")
  1307. else:
  1308. size = (self.GetTextExtent(name)[0]+10, -1)
  1309. if toggle:
  1310. control = wx.ToggleButton(self, -1, name, size=size)
  1311. else:
  1312. control = wx.Button(self, -1, name, size=size)
  1313. tool = self.InsertControl(idx, control, label=name)
  1314. self.Realize()
  1315. def handler(event):
  1316. self.trigger_tool(name)
  1317. if image_file:
  1318. self.Bind(wx.EVT_TOOL, handler, tool)
  1319. else:
  1320. control.Bind(wx.EVT_LEFT_DOWN, handler)
  1321. self._toolitems.setdefault(name, [])
  1322. group.insert(position, tool)
  1323. self._toolitems[name].append((tool, handler))
  1324. def _add_to_group(self, group, name, position):
  1325. gr = self._groups.get(group, [])
  1326. if not gr:
  1327. sep = self.AddSeparator()
  1328. gr.append(sep)
  1329. before = gr[position]
  1330. self._groups[group] = gr
  1331. return before, gr
  1332. def toggle_toolitem(self, name, toggled):
  1333. if name not in self._toolitems:
  1334. return
  1335. for tool, handler in self._toolitems[name]:
  1336. if not tool.IsControl():
  1337. self.ToggleTool(tool.Id, toggled)
  1338. else:
  1339. tool.GetControl().SetValue(toggled)
  1340. self.Refresh()
  1341. def remove_toolitem(self, name):
  1342. for tool, handler in self._toolitems[name]:
  1343. self.DeleteTool(tool.Id)
  1344. del self._toolitems[name]
  1345. class StatusbarWx(StatusbarBase, wx.StatusBar):
  1346. """for use with ToolManager"""
  1347. def __init__(self, parent, *args, **kwargs):
  1348. StatusbarBase.__init__(self, *args, **kwargs)
  1349. wx.StatusBar.__init__(self, parent, -1)
  1350. self.SetFieldsCount(1)
  1351. self.SetStatusText("")
  1352. def set_message(self, s):
  1353. self.SetStatusText(s)
  1354. class ConfigureSubplotsWx(backend_tools.ConfigureSubplotsBase):
  1355. def trigger(self, *args):
  1356. NavigationToolbar2Wx.configure_subplots(
  1357. self._make_classic_style_pseudo_toolbar())
  1358. @cbook.deprecated("3.2")
  1359. def configure_subplots(self):
  1360. frame = wx.Frame(None, -1, "Configure subplots")
  1361. _set_frame_icon(frame)
  1362. toolfig = Figure((6, 3))
  1363. canvas = self.get_canvas(frame, toolfig)
  1364. # Now put all into a sizer
  1365. sizer = wx.BoxSizer(wx.VERTICAL)
  1366. # This way of adding to sizer allows resizing
  1367. sizer.Add(canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
  1368. frame.SetSizer(sizer)
  1369. frame.Fit()
  1370. SubplotTool(self.canvas.figure, toolfig)
  1371. frame.Show()
  1372. @cbook.deprecated("3.2")
  1373. def get_canvas(self, frame, fig):
  1374. return type(self.canvas)(frame, -1, fig)
  1375. class SaveFigureWx(backend_tools.SaveFigureBase):
  1376. def trigger(self, *args):
  1377. NavigationToolbar2Wx.save_figure(
  1378. self._make_classic_style_pseudo_toolbar())
  1379. class SetCursorWx(backend_tools.SetCursorBase):
  1380. def set_cursor(self, cursor):
  1381. NavigationToolbar2Wx.set_cursor(
  1382. self._make_classic_style_pseudo_toolbar(), cursor)
  1383. if 'wxMac' not in wx.PlatformInfo:
  1384. # on most platforms, use overlay
  1385. class RubberbandWx(backend_tools.RubberbandBase):
  1386. def __init__(self, *args, **kwargs):
  1387. backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
  1388. self.wxoverlay = None
  1389. def draw_rubberband(self, x0, y0, x1, y1):
  1390. # Use an Overlay to draw a rubberband-like bounding box.
  1391. if self.wxoverlay is None:
  1392. self.wxoverlay = wx.Overlay()
  1393. dc = wx.ClientDC(self.canvas)
  1394. odc = wx.DCOverlay(self.wxoverlay, dc)
  1395. odc.Clear()
  1396. dc = wx.GCDC(dc)
  1397. height = self.canvas.figure.bbox.height
  1398. y1 = height - y1
  1399. y0 = height - y0
  1400. if y1 < y0:
  1401. y0, y1 = y1, y0
  1402. if x1 < x0:
  1403. x0, x1 = x1, x0
  1404. w = x1 - x0
  1405. h = y1 - y0
  1406. rect = wx.Rect(x0, y0, w, h)
  1407. rubberBandColor = '#C0C0FF' # or load from config?
  1408. # Set a pen for the border
  1409. color = wx.Colour(rubberBandColor)
  1410. dc.SetPen(wx.Pen(color, 1))
  1411. # use the same color, plus alpha for the brush
  1412. r, g, b, a = color.Get(True)
  1413. color.Set(r, g, b, 0x60)
  1414. dc.SetBrush(wx.Brush(color))
  1415. dc.DrawRectangle(rect)
  1416. def remove_rubberband(self):
  1417. if self.wxoverlay is None:
  1418. return
  1419. self.wxoverlay.Reset()
  1420. self.wxoverlay = None
  1421. else:
  1422. # on Mac OS retina displays DCOverlay does not work
  1423. # and dc.SetLogicalFunction does not have an effect on any display
  1424. # the workaround is to blit the full image for remove_rubberband
  1425. class RubberbandWx(backend_tools.RubberbandBase):
  1426. def __init__(self, *args, **kwargs):
  1427. backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
  1428. self._rect = None
  1429. def draw_rubberband(self, x0, y0, x1, y1):
  1430. dc = wx.ClientDC(self.canvas)
  1431. # this would be required if the Canvas is a ScrolledWindow,
  1432. # which is not the case for now
  1433. # self.PrepareDC(dc)
  1434. # delete old rubberband
  1435. if self._rect:
  1436. self.remove_rubberband(dc)
  1437. # draw new rubberband
  1438. dc.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID))
  1439. dc.SetBrush(wx.TRANSPARENT_BRUSH)
  1440. self._rect = (x0, self.canvas._height-y0, x1-x0, -y1+y0)
  1441. dc.DrawRectangle(self._rect)
  1442. def remove_rubberband(self, dc=None):
  1443. if not self._rect:
  1444. return
  1445. if self.canvas.bitmap:
  1446. if dc is None:
  1447. dc = wx.ClientDC(self.canvas)
  1448. dc.DrawBitmap(self.canvas.bitmap, 0, 0)
  1449. # for testing the method on Windows, use this code instead:
  1450. # img = self.canvas.bitmap.ConvertToImage()
  1451. # bmp = img.ConvertToBitmap()
  1452. # dc.DrawBitmap(bmp, 0, 0)
  1453. self._rect = None
  1454. class _HelpDialog(wx.Dialog):
  1455. _instance = None # a reference to an open dialog singleton
  1456. headers = [("Action", "Shortcuts", "Description")]
  1457. widths = [100, 140, 300]
  1458. def __init__(self, parent, help_entries):
  1459. wx.Dialog.__init__(self, parent, title="Help",
  1460. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
  1461. sizer = wx.BoxSizer(wx.VERTICAL)
  1462. grid_sizer = wx.FlexGridSizer(0, 3, 8, 6)
  1463. # create and add the entries
  1464. bold = self.GetFont().MakeBold()
  1465. for r, row in enumerate(self.headers + help_entries):
  1466. for (col, width) in zip(row, self.widths):
  1467. label = wx.StaticText(self, label=col)
  1468. if r == 0:
  1469. label.SetFont(bold)
  1470. label.Wrap(width)
  1471. grid_sizer.Add(label, 0, 0, 0)
  1472. # finalize layout, create button
  1473. sizer.Add(grid_sizer, 0, wx.ALL, 6)
  1474. OK = wx.Button(self, wx.ID_OK)
  1475. sizer.Add(OK, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 8)
  1476. self.SetSizer(sizer)
  1477. sizer.Fit(self)
  1478. self.Layout()
  1479. self.Bind(wx.EVT_CLOSE, self.OnClose)
  1480. OK.Bind(wx.EVT_BUTTON, self.OnClose)
  1481. def OnClose(self, evt):
  1482. _HelpDialog._instance = None # remove global reference
  1483. self.DestroyLater()
  1484. evt.Skip()
  1485. @classmethod
  1486. def show(cls, parent, help_entries):
  1487. # if no dialog is shown, create one; otherwise just re-raise it
  1488. if cls._instance:
  1489. cls._instance.Raise()
  1490. return
  1491. cls._instance = cls(parent, help_entries)
  1492. cls._instance.Show()
  1493. class HelpWx(backend_tools.ToolHelpBase):
  1494. def trigger(self, *args):
  1495. _HelpDialog.show(self.figure.canvas.GetTopLevelParent(),
  1496. self._get_help_entries())
  1497. class ToolCopyToClipboardWx(backend_tools.ToolCopyToClipboardBase):
  1498. def trigger(self, *args, **kwargs):
  1499. if not self.canvas._isDrawn:
  1500. self.canvas.draw()
  1501. if not self.canvas.bitmap.IsOk() or not wx.TheClipboard.Open():
  1502. return
  1503. try:
  1504. wx.TheClipboard.SetData(wx.BitmapDataObject(self.canvas.bitmap))
  1505. finally:
  1506. wx.TheClipboard.Close()
  1507. backend_tools.ToolSaveFigure = SaveFigureWx
  1508. backend_tools.ToolConfigureSubplots = ConfigureSubplotsWx
  1509. backend_tools.ToolSetCursor = SetCursorWx
  1510. backend_tools.ToolRubberband = RubberbandWx
  1511. backend_tools.ToolHelp = HelpWx
  1512. backend_tools.ToolCopyToClipboard = ToolCopyToClipboardWx
  1513. # < Additions for printing support: Matt Newville
  1514. @cbook.deprecated("3.1")
  1515. class PrintoutWx(wx.Printout):
  1516. """
  1517. Simple wrapper around wx Printout class -- all the real work
  1518. here is scaling the matplotlib canvas bitmap to the current
  1519. printer's definition.
  1520. """
  1521. def __init__(self, canvas, width=5.5, margin=0.5, title='matplotlib'):
  1522. wx.Printout.__init__(self, title=title)
  1523. self.canvas = canvas
  1524. # width, in inches of output figure (approximate)
  1525. self.width = width
  1526. self.margin = margin
  1527. def HasPage(self, page):
  1528. # current only supports 1 page print
  1529. return page == 1
  1530. def GetPageInfo(self):
  1531. return (1, 1, 1, 1)
  1532. def OnPrintPage(self, page):
  1533. self.canvas.draw()
  1534. dc = self.GetDC()
  1535. ppw, pph = self.GetPPIPrinter() # printer's pixels per in
  1536. pgw, pgh = self.GetPageSizePixels() # page size in pixels
  1537. dcw, dch = dc.GetSize()
  1538. grw, grh = self.canvas.GetSize()
  1539. # save current figure dpi resolution and bg color,
  1540. # so that we can temporarily set them to the dpi of
  1541. # the printer, and the bg color to white
  1542. bgcolor = self.canvas.figure.get_facecolor()
  1543. fig_dpi = self.canvas.figure.dpi
  1544. # draw the bitmap, scaled appropriately
  1545. vscale = float(ppw) / fig_dpi
  1546. # set figure resolution,bg color for printer
  1547. self.canvas.figure.dpi = ppw
  1548. self.canvas.figure.set_facecolor('#FFFFFF')
  1549. renderer = RendererWx(self.canvas.bitmap, self.canvas.figure.dpi)
  1550. self.canvas.figure.draw(renderer)
  1551. self.canvas.bitmap.SetWidth(
  1552. int(self.canvas.bitmap.GetWidth() * vscale))
  1553. self.canvas.bitmap.SetHeight(
  1554. int(self.canvas.bitmap.GetHeight() * vscale))
  1555. self.canvas.draw()
  1556. # page may need additional scaling on preview
  1557. page_scale = 1.0
  1558. if self.IsPreview():
  1559. page_scale = float(dcw) / pgw
  1560. # get margin in pixels = (margin in in) * (pixels/in)
  1561. top_margin = int(self.margin * pph * page_scale)
  1562. left_margin = int(self.margin * ppw * page_scale)
  1563. # set scale so that width of output is self.width inches
  1564. # (assuming grw is size of graph in inches....)
  1565. user_scale = (self.width * fig_dpi * page_scale) / float(grw)
  1566. dc.SetDeviceOrigin(left_margin, top_margin)
  1567. dc.SetUserScale(user_scale, user_scale)
  1568. # this cute little number avoid API inconsistencies in wx
  1569. try:
  1570. dc.DrawBitmap(self.canvas.bitmap, 0, 0)
  1571. except Exception:
  1572. try:
  1573. dc.DrawBitmap(self.canvas.bitmap, (0, 0))
  1574. except Exception:
  1575. pass
  1576. # restore original figure resolution
  1577. self.canvas.figure.set_facecolor(bgcolor)
  1578. self.canvas.figure.dpi = fig_dpi
  1579. self.canvas.draw()
  1580. return True
  1581. @_Backend.export
  1582. class _BackendWx(_Backend):
  1583. FigureCanvas = FigureCanvasWx
  1584. FigureManager = FigureManagerWx
  1585. _frame_class = FigureFrameWx
  1586. @staticmethod
  1587. def trigger_manager_draw(manager):
  1588. manager.canvas.draw_idle()
  1589. @classmethod
  1590. def new_figure_manager(cls, num, *args, **kwargs):
  1591. # Create a wx.App instance if it has not been created so far.
  1592. wxapp = wx.GetApp()
  1593. if wxapp is None:
  1594. wxapp = wx.App(False)
  1595. wxapp.SetExitOnFrameDelete(True)
  1596. # Retain a reference to the app object so that it does not get
  1597. # garbage collected.
  1598. _BackendWx._theWxApp = wxapp
  1599. return super().new_figure_manager(num, *args, **kwargs)
  1600. @classmethod
  1601. def new_figure_manager_given_figure(cls, num, figure):
  1602. frame = cls._frame_class(num, figure)
  1603. figmgr = frame.get_figure_manager()
  1604. if matplotlib.is_interactive():
  1605. figmgr.frame.Show()
  1606. figure.canvas.draw_idle()
  1607. return figmgr
  1608. @staticmethod
  1609. def mainloop():
  1610. if not wx.App.IsMainLoopRunning():
  1611. wxapp = wx.GetApp()
  1612. if wxapp is not None:
  1613. wxapp.MainLoop()