|
""" |
|
A wxPython backend for matplotlib. |
|
|
|
Originally contributed by Jeremy O'Donoghue (jeremy@o-donoghue.com) and John |
|
Hunter (jdhunter@ace.bsd.uchicago.edu). |
|
|
|
Copyright (C) Jeremy O'Donoghue & John Hunter, 2003-4. |
|
""" |
|
|
|
import functools |
|
import logging |
|
import math |
|
import pathlib |
|
import sys |
|
import weakref |
|
|
|
import numpy as np |
|
import PIL.Image |
|
|
|
import matplotlib as mpl |
|
from matplotlib.backend_bases import ( |
|
_Backend, FigureCanvasBase, FigureManagerBase, |
|
GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase, |
|
TimerBase, ToolContainerBase, cursors, |
|
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) |
|
|
|
from matplotlib import _api, cbook, backend_tools |
|
from matplotlib._pylab_helpers import Gcf |
|
from matplotlib.path import Path |
|
from matplotlib.transforms import Affine2D |
|
|
|
import wx |
|
|
|
_log = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
PIXELS_PER_INCH = 75 |
|
|
|
|
|
|
|
@functools.lru_cache(1) |
|
def _create_wxapp(): |
|
wxapp = wx.App(False) |
|
wxapp.SetExitOnFrameDelete(True) |
|
cbook._setup_new_guiapp() |
|
return wxapp |
|
|
|
|
|
class TimerWx(TimerBase): |
|
"""Subclass of `.TimerBase` using wx.Timer events.""" |
|
|
|
def __init__(self, *args, **kwargs): |
|
self._timer = wx.Timer() |
|
self._timer.Notify = self._on_timer |
|
super().__init__(*args, **kwargs) |
|
|
|
def _timer_start(self): |
|
self._timer.Start(self._interval, self._single) |
|
|
|
def _timer_stop(self): |
|
self._timer.Stop() |
|
|
|
def _timer_set_interval(self): |
|
if self._timer.IsRunning(): |
|
self._timer_start() |
|
|
|
|
|
@_api.deprecated( |
|
"2.0", name="wx", obj_type="backend", removal="the future", |
|
alternative="wxagg", |
|
addendum="See the Matplotlib usage FAQ for more info on backends.") |
|
class RendererWx(RendererBase): |
|
""" |
|
The renderer handles all the drawing primitives using a graphics |
|
context instance that controls the colors/styles. It acts as the |
|
'renderer' instance used by many classes in the hierarchy. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fontweights = { |
|
100: wx.FONTWEIGHT_LIGHT, |
|
200: wx.FONTWEIGHT_LIGHT, |
|
300: wx.FONTWEIGHT_LIGHT, |
|
400: wx.FONTWEIGHT_NORMAL, |
|
500: wx.FONTWEIGHT_NORMAL, |
|
600: wx.FONTWEIGHT_NORMAL, |
|
700: wx.FONTWEIGHT_BOLD, |
|
800: wx.FONTWEIGHT_BOLD, |
|
900: wx.FONTWEIGHT_BOLD, |
|
'ultralight': wx.FONTWEIGHT_LIGHT, |
|
'light': wx.FONTWEIGHT_LIGHT, |
|
'normal': wx.FONTWEIGHT_NORMAL, |
|
'medium': wx.FONTWEIGHT_NORMAL, |
|
'semibold': wx.FONTWEIGHT_NORMAL, |
|
'bold': wx.FONTWEIGHT_BOLD, |
|
'heavy': wx.FONTWEIGHT_BOLD, |
|
'ultrabold': wx.FONTWEIGHT_BOLD, |
|
'black': wx.FONTWEIGHT_BOLD, |
|
} |
|
fontangles = { |
|
'italic': wx.FONTSTYLE_ITALIC, |
|
'normal': wx.FONTSTYLE_NORMAL, |
|
'oblique': wx.FONTSTYLE_SLANT, |
|
} |
|
|
|
|
|
|
|
|
|
fontnames = { |
|
'Sans': wx.FONTFAMILY_SWISS, |
|
'Roman': wx.FONTFAMILY_ROMAN, |
|
'Script': wx.FONTFAMILY_SCRIPT, |
|
'Decorative': wx.FONTFAMILY_DECORATIVE, |
|
'Modern': wx.FONTFAMILY_MODERN, |
|
'Courier': wx.FONTFAMILY_MODERN, |
|
'courier': wx.FONTFAMILY_MODERN, |
|
} |
|
|
|
def __init__(self, bitmap, dpi): |
|
"""Initialise a wxWindows renderer instance.""" |
|
super().__init__() |
|
_log.debug("%s - __init__()", type(self)) |
|
self.width = bitmap.GetWidth() |
|
self.height = bitmap.GetHeight() |
|
self.bitmap = bitmap |
|
self.fontd = {} |
|
self.dpi = dpi |
|
self.gc = None |
|
|
|
def flipy(self): |
|
|
|
return True |
|
|
|
def get_text_width_height_descent(self, s, prop, ismath): |
|
|
|
|
|
if ismath: |
|
s = cbook.strip_math(s) |
|
|
|
if self.gc is None: |
|
gc = self.new_gc() |
|
else: |
|
gc = self.gc |
|
gfx_ctx = gc.gfx_ctx |
|
font = self.get_wx_font(s, prop) |
|
gfx_ctx.SetFont(font, wx.BLACK) |
|
w, h, descent, leading = gfx_ctx.GetFullTextExtent(s) |
|
|
|
return w, h, descent |
|
|
|
def get_canvas_width_height(self): |
|
|
|
return self.width, self.height |
|
|
|
def handle_clip_rectangle(self, gc): |
|
new_bounds = gc.get_clip_rectangle() |
|
if new_bounds is not None: |
|
new_bounds = new_bounds.bounds |
|
gfx_ctx = gc.gfx_ctx |
|
if gfx_ctx._lastcliprect != new_bounds: |
|
gfx_ctx._lastcliprect = new_bounds |
|
if new_bounds is None: |
|
gfx_ctx.ResetClip() |
|
else: |
|
gfx_ctx.Clip(new_bounds[0], |
|
self.height - new_bounds[1] - new_bounds[3], |
|
new_bounds[2], new_bounds[3]) |
|
|
|
@staticmethod |
|
def convert_path(gfx_ctx, path, transform): |
|
wxpath = gfx_ctx.CreatePath() |
|
for points, code in path.iter_segments(transform): |
|
if code == Path.MOVETO: |
|
wxpath.MoveToPoint(*points) |
|
elif code == Path.LINETO: |
|
wxpath.AddLineToPoint(*points) |
|
elif code == Path.CURVE3: |
|
wxpath.AddQuadCurveToPoint(*points) |
|
elif code == Path.CURVE4: |
|
wxpath.AddCurveToPoint(*points) |
|
elif code == Path.CLOSEPOLY: |
|
wxpath.CloseSubpath() |
|
return wxpath |
|
|
|
def draw_path(self, gc, path, transform, rgbFace=None): |
|
|
|
gc.select() |
|
self.handle_clip_rectangle(gc) |
|
gfx_ctx = gc.gfx_ctx |
|
transform = transform + \ |
|
Affine2D().scale(1.0, -1.0).translate(0.0, self.height) |
|
wxpath = self.convert_path(gfx_ctx, path, transform) |
|
if rgbFace is not None: |
|
gfx_ctx.SetBrush(wx.Brush(gc.get_wxcolour(rgbFace))) |
|
gfx_ctx.DrawPath(wxpath) |
|
else: |
|
gfx_ctx.StrokePath(wxpath) |
|
gc.unselect() |
|
|
|
def draw_image(self, gc, x, y, im): |
|
bbox = gc.get_clip_rectangle() |
|
if bbox is not None: |
|
l, b, w, h = bbox.bounds |
|
else: |
|
l = 0 |
|
b = 0 |
|
w = self.width |
|
h = self.height |
|
rows, cols = im.shape[:2] |
|
bitmap = wx.Bitmap.FromBufferRGBA(cols, rows, im.tobytes()) |
|
gc.select() |
|
gc.gfx_ctx.DrawBitmap(bitmap, int(l), int(self.height - b), |
|
int(w), int(-h)) |
|
gc.unselect() |
|
|
|
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): |
|
|
|
|
|
if ismath: |
|
s = cbook.strip_math(s) |
|
_log.debug("%s - draw_text()", type(self)) |
|
gc.select() |
|
self.handle_clip_rectangle(gc) |
|
gfx_ctx = gc.gfx_ctx |
|
|
|
font = self.get_wx_font(s, prop) |
|
color = gc.get_wxcolour(gc.get_rgb()) |
|
gfx_ctx.SetFont(font, color) |
|
|
|
w, h, d = self.get_text_width_height_descent(s, prop, ismath) |
|
x = int(x) |
|
y = int(y - h) |
|
|
|
if angle == 0.0: |
|
gfx_ctx.DrawText(s, x, y) |
|
else: |
|
rads = math.radians(angle) |
|
xo = h * math.sin(rads) |
|
yo = h * math.cos(rads) |
|
gfx_ctx.DrawRotatedText(s, x - xo, y - yo, rads) |
|
|
|
gc.unselect() |
|
|
|
def new_gc(self): |
|
|
|
_log.debug("%s - new_gc()", type(self)) |
|
self.gc = GraphicsContextWx(self.bitmap, self) |
|
self.gc.select() |
|
self.gc.unselect() |
|
return self.gc |
|
|
|
def get_wx_font(self, s, prop): |
|
"""Return a wx font. Cache font instances for efficiency.""" |
|
_log.debug("%s - get_wx_font()", type(self)) |
|
key = hash(prop) |
|
font = self.fontd.get(key) |
|
if font is not None: |
|
return font |
|
size = self.points_to_pixels(prop.get_size_in_points()) |
|
|
|
|
|
self.fontd[key] = font = wx.Font( |
|
pointSize=round(size), |
|
family=self.fontnames.get(prop.get_name(), wx.ROMAN), |
|
style=self.fontangles[prop.get_style()], |
|
weight=self.fontweights[prop.get_weight()]) |
|
return font |
|
|
|
def points_to_pixels(self, points): |
|
|
|
return points * (PIXELS_PER_INCH / 72.0 * self.dpi / 72.0) |
|
|
|
|
|
class GraphicsContextWx(GraphicsContextBase): |
|
""" |
|
The graphics context provides the color, line styles, etc. |
|
|
|
This class stores a reference to a wxMemoryDC, and a |
|
wxGraphicsContext that draws to it. Creating a wxGraphicsContext |
|
seems to be fairly heavy, so these objects are cached based on the |
|
bitmap object that is passed in. |
|
|
|
The base GraphicsContext stores colors as an RGB tuple on the unit |
|
interval, e.g., (0.5, 0.0, 1.0). wxPython uses an int interval, but |
|
since wxPython colour management is rather simple, I have not chosen |
|
to implement a separate colour manager class. |
|
""" |
|
_capd = {'butt': wx.CAP_BUTT, |
|
'projecting': wx.CAP_PROJECTING, |
|
'round': wx.CAP_ROUND} |
|
|
|
_joind = {'bevel': wx.JOIN_BEVEL, |
|
'miter': wx.JOIN_MITER, |
|
'round': wx.JOIN_ROUND} |
|
|
|
_cache = weakref.WeakKeyDictionary() |
|
|
|
def __init__(self, bitmap, renderer): |
|
super().__init__() |
|
|
|
_log.debug("%s - __init__(): %s", type(self), bitmap) |
|
|
|
dc, gfx_ctx = self._cache.get(bitmap, (None, None)) |
|
if dc is None: |
|
dc = wx.MemoryDC(bitmap) |
|
gfx_ctx = wx.GraphicsContext.Create(dc) |
|
gfx_ctx._lastcliprect = None |
|
self._cache[bitmap] = dc, gfx_ctx |
|
|
|
self.bitmap = bitmap |
|
self.dc = dc |
|
self.gfx_ctx = gfx_ctx |
|
self._pen = wx.Pen('BLACK', 1, wx.SOLID) |
|
gfx_ctx.SetPen(self._pen) |
|
self.renderer = renderer |
|
|
|
def select(self): |
|
"""Select the current bitmap into this wxDC instance.""" |
|
if sys.platform == 'win32': |
|
self.dc.SelectObject(self.bitmap) |
|
self.IsSelected = True |
|
|
|
def unselect(self): |
|
"""Select a Null bitmap into this wxDC instance.""" |
|
if sys.platform == 'win32': |
|
self.dc.SelectObject(wx.NullBitmap) |
|
self.IsSelected = False |
|
|
|
def set_foreground(self, fg, isRGBA=None): |
|
|
|
|
|
|
|
|
|
|
|
|
|
_log.debug("%s - set_foreground()", type(self)) |
|
self.select() |
|
super().set_foreground(fg, isRGBA) |
|
|
|
self._pen.SetColour(self.get_wxcolour(self.get_rgb())) |
|
self.gfx_ctx.SetPen(self._pen) |
|
self.unselect() |
|
|
|
def set_linewidth(self, w): |
|
|
|
w = float(w) |
|
_log.debug("%s - set_linewidth()", type(self)) |
|
self.select() |
|
if 0 < w < 1: |
|
w = 1 |
|
super().set_linewidth(w) |
|
lw = int(self.renderer.points_to_pixels(self._linewidth)) |
|
if lw == 0: |
|
lw = 1 |
|
self._pen.SetWidth(lw) |
|
self.gfx_ctx.SetPen(self._pen) |
|
self.unselect() |
|
|
|
def set_capstyle(self, cs): |
|
|
|
_log.debug("%s - set_capstyle()", type(self)) |
|
self.select() |
|
super().set_capstyle(cs) |
|
self._pen.SetCap(GraphicsContextWx._capd[self._capstyle]) |
|
self.gfx_ctx.SetPen(self._pen) |
|
self.unselect() |
|
|
|
def set_joinstyle(self, js): |
|
|
|
_log.debug("%s - set_joinstyle()", type(self)) |
|
self.select() |
|
super().set_joinstyle(js) |
|
self._pen.SetJoin(GraphicsContextWx._joind[self._joinstyle]) |
|
self.gfx_ctx.SetPen(self._pen) |
|
self.unselect() |
|
|
|
def get_wxcolour(self, color): |
|
"""Convert an RGB(A) color to a wx.Colour.""" |
|
_log.debug("%s - get_wx_color()", type(self)) |
|
return wx.Colour(*[int(255 * x) for x in color]) |
|
|
|
|
|
class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel): |
|
""" |
|
The FigureCanvas contains the figure and does event handling. |
|
|
|
In the wxPython backend, it is derived from wxPanel, and (usually) lives |
|
inside a frame instantiated by a FigureManagerWx. The parent window |
|
probably implements a wx.Sizer to control the displayed control size - but |
|
we give a hint as to our preferred minimum size. |
|
""" |
|
|
|
required_interactive_framework = "wx" |
|
_timer_cls = TimerWx |
|
manager_class = _api.classproperty(lambda cls: FigureManagerWx) |
|
|
|
keyvald = { |
|
wx.WXK_CONTROL: 'control', |
|
wx.WXK_SHIFT: 'shift', |
|
wx.WXK_ALT: 'alt', |
|
wx.WXK_CAPITAL: 'caps_lock', |
|
wx.WXK_LEFT: 'left', |
|
wx.WXK_UP: 'up', |
|
wx.WXK_RIGHT: 'right', |
|
wx.WXK_DOWN: 'down', |
|
wx.WXK_ESCAPE: 'escape', |
|
wx.WXK_F1: 'f1', |
|
wx.WXK_F2: 'f2', |
|
wx.WXK_F3: 'f3', |
|
wx.WXK_F4: 'f4', |
|
wx.WXK_F5: 'f5', |
|
wx.WXK_F6: 'f6', |
|
wx.WXK_F7: 'f7', |
|
wx.WXK_F8: 'f8', |
|
wx.WXK_F9: 'f9', |
|
wx.WXK_F10: 'f10', |
|
wx.WXK_F11: 'f11', |
|
wx.WXK_F12: 'f12', |
|
wx.WXK_SCROLL: 'scroll_lock', |
|
wx.WXK_PAUSE: 'break', |
|
wx.WXK_BACK: 'backspace', |
|
wx.WXK_RETURN: 'enter', |
|
wx.WXK_INSERT: 'insert', |
|
wx.WXK_DELETE: 'delete', |
|
wx.WXK_HOME: 'home', |
|
wx.WXK_END: 'end', |
|
wx.WXK_PAGEUP: 'pageup', |
|
wx.WXK_PAGEDOWN: 'pagedown', |
|
wx.WXK_NUMPAD0: '0', |
|
wx.WXK_NUMPAD1: '1', |
|
wx.WXK_NUMPAD2: '2', |
|
wx.WXK_NUMPAD3: '3', |
|
wx.WXK_NUMPAD4: '4', |
|
wx.WXK_NUMPAD5: '5', |
|
wx.WXK_NUMPAD6: '6', |
|
wx.WXK_NUMPAD7: '7', |
|
wx.WXK_NUMPAD8: '8', |
|
wx.WXK_NUMPAD9: '9', |
|
wx.WXK_NUMPAD_ADD: '+', |
|
wx.WXK_NUMPAD_SUBTRACT: '-', |
|
wx.WXK_NUMPAD_MULTIPLY: '*', |
|
wx.WXK_NUMPAD_DIVIDE: '/', |
|
wx.WXK_NUMPAD_DECIMAL: 'dec', |
|
wx.WXK_NUMPAD_ENTER: 'enter', |
|
wx.WXK_NUMPAD_UP: 'up', |
|
wx.WXK_NUMPAD_RIGHT: 'right', |
|
wx.WXK_NUMPAD_DOWN: 'down', |
|
wx.WXK_NUMPAD_LEFT: 'left', |
|
wx.WXK_NUMPAD_PAGEUP: 'pageup', |
|
wx.WXK_NUMPAD_PAGEDOWN: 'pagedown', |
|
wx.WXK_NUMPAD_HOME: 'home', |
|
wx.WXK_NUMPAD_END: 'end', |
|
wx.WXK_NUMPAD_INSERT: 'insert', |
|
wx.WXK_NUMPAD_DELETE: 'delete', |
|
} |
|
|
|
def __init__(self, parent, id, figure=None): |
|
""" |
|
Initialize a FigureWx instance. |
|
|
|
- Initialize the FigureCanvasBase and wxPanel parents. |
|
- Set event handlers for resize, paint, and keyboard and mouse |
|
interaction. |
|
""" |
|
|
|
FigureCanvasBase.__init__(self, figure) |
|
w, h = map(math.ceil, self.figure.bbox.size) |
|
|
|
wx.Panel.__init__(self, parent, id, size=wx.Size(w, h)) |
|
|
|
self.bitmap = wx.Bitmap(w, h) |
|
_log.debug("%s - __init__() - bitmap w:%d h:%d", type(self), w, h) |
|
self._isDrawn = False |
|
self._rubberband_rect = None |
|
self._rubberband_pen_black = wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH) |
|
self._rubberband_pen_white = wx.Pen('WHITE', 1, wx.PENSTYLE_SOLID) |
|
|
|
self.Bind(wx.EVT_SIZE, self._on_size) |
|
self.Bind(wx.EVT_PAINT, self._on_paint) |
|
self.Bind(wx.EVT_CHAR_HOOK, self._on_key_down) |
|
self.Bind(wx.EVT_KEY_UP, self._on_key_up) |
|
self.Bind(wx.EVT_LEFT_DOWN, self._on_mouse_button) |
|
self.Bind(wx.EVT_LEFT_DCLICK, self._on_mouse_button) |
|
self.Bind(wx.EVT_LEFT_UP, self._on_mouse_button) |
|
self.Bind(wx.EVT_MIDDLE_DOWN, self._on_mouse_button) |
|
self.Bind(wx.EVT_MIDDLE_DCLICK, self._on_mouse_button) |
|
self.Bind(wx.EVT_MIDDLE_UP, self._on_mouse_button) |
|
self.Bind(wx.EVT_RIGHT_DOWN, self._on_mouse_button) |
|
self.Bind(wx.EVT_RIGHT_DCLICK, self._on_mouse_button) |
|
self.Bind(wx.EVT_RIGHT_UP, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX1_DOWN, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX1_UP, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX2_DOWN, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX2_UP, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX1_DCLICK, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSE_AUX2_DCLICK, self._on_mouse_button) |
|
self.Bind(wx.EVT_MOUSEWHEEL, self._on_mouse_wheel) |
|
self.Bind(wx.EVT_MOTION, self._on_motion) |
|
self.Bind(wx.EVT_ENTER_WINDOW, self._on_enter) |
|
self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave) |
|
|
|
self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._on_capture_lost) |
|
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._on_capture_lost) |
|
|
|
self.SetBackgroundStyle(wx.BG_STYLE_PAINT) |
|
self.SetBackgroundColour(wx.WHITE) |
|
|
|
def Copy_to_Clipboard(self, event=None): |
|
"""Copy bitmap of canvas to system clipboard.""" |
|
bmp_obj = wx.BitmapDataObject() |
|
bmp_obj.SetBitmap(self.bitmap) |
|
|
|
if not wx.TheClipboard.IsOpened(): |
|
open_success = wx.TheClipboard.Open() |
|
if open_success: |
|
wx.TheClipboard.SetData(bmp_obj) |
|
wx.TheClipboard.Flush() |
|
wx.TheClipboard.Close() |
|
|
|
def draw_idle(self): |
|
|
|
_log.debug("%s - draw_idle()", type(self)) |
|
self._isDrawn = False |
|
|
|
|
|
|
|
self.Refresh(eraseBackground=False) |
|
|
|
def flush_events(self): |
|
|
|
wx.Yield() |
|
|
|
def start_event_loop(self, timeout=0): |
|
|
|
if hasattr(self, '_event_loop'): |
|
raise RuntimeError("Event loop already running") |
|
timer = wx.Timer(self, id=wx.ID_ANY) |
|
if timeout > 0: |
|
timer.Start(int(timeout * 1000), oneShot=True) |
|
self.Bind(wx.EVT_TIMER, self.stop_event_loop, id=timer.GetId()) |
|
|
|
self._event_loop = wx.GUIEventLoop() |
|
self._event_loop.Run() |
|
timer.Stop() |
|
|
|
def stop_event_loop(self, event=None): |
|
|
|
if hasattr(self, '_event_loop'): |
|
if self._event_loop.IsRunning(): |
|
self._event_loop.Exit() |
|
del self._event_loop |
|
|
|
def _get_imagesave_wildcards(self): |
|
"""Return the wildcard string for the filesave dialog.""" |
|
default_filetype = self.get_default_filetype() |
|
filetypes = self.get_supported_filetypes_grouped() |
|
sorted_filetypes = sorted(filetypes.items()) |
|
wildcards = [] |
|
extensions = [] |
|
filter_index = 0 |
|
for i, (name, exts) in enumerate(sorted_filetypes): |
|
ext_list = ';'.join(['*.%s' % ext for ext in exts]) |
|
extensions.append(exts[0]) |
|
wildcard = f'{name} ({ext_list})|{ext_list}' |
|
if default_filetype in exts: |
|
filter_index = i |
|
wildcards.append(wildcard) |
|
wildcards = '|'.join(wildcards) |
|
return wildcards, extensions, filter_index |
|
|
|
def gui_repaint(self, drawDC=None): |
|
""" |
|
Update the displayed image on the GUI canvas, using the supplied |
|
wx.PaintDC device context. |
|
""" |
|
_log.debug("%s - gui_repaint()", type(self)) |
|
|
|
|
|
if not (self and self.IsShownOnScreen()): |
|
return |
|
if not drawDC: |
|
drawDC = wx.ClientDC(self) |
|
|
|
|
|
bmp = (self.bitmap.ConvertToImage().ConvertToBitmap() |
|
if wx.Platform == '__WXMSW__' |
|
and isinstance(self.figure.canvas.get_renderer(), RendererWx) |
|
else self.bitmap) |
|
drawDC.DrawBitmap(bmp, 0, 0) |
|
if self._rubberband_rect is not None: |
|
|
|
x0, y0, x1, y1 = map(round, self._rubberband_rect) |
|
rect = [(x0, y0, x1, y0), (x1, y0, x1, y1), |
|
(x0, y0, x0, y1), (x0, y1, x1, y1)] |
|
drawDC.DrawLineList(rect, self._rubberband_pen_white) |
|
drawDC.DrawLineList(rect, self._rubberband_pen_black) |
|
|
|
filetypes = { |
|
**FigureCanvasBase.filetypes, |
|
'bmp': 'Windows bitmap', |
|
'jpeg': 'JPEG', |
|
'jpg': 'JPEG', |
|
'pcx': 'PCX', |
|
'png': 'Portable Network Graphics', |
|
'tif': 'Tagged Image Format File', |
|
'tiff': 'Tagged Image Format File', |
|
'xpm': 'X pixmap', |
|
} |
|
|
|
def _on_paint(self, event): |
|
"""Called when wxPaintEvt is generated.""" |
|
_log.debug("%s - _on_paint()", type(self)) |
|
drawDC = wx.PaintDC(self) |
|
if not self._isDrawn: |
|
self.draw(drawDC=drawDC) |
|
else: |
|
self.gui_repaint(drawDC=drawDC) |
|
drawDC.Destroy() |
|
|
|
def _on_size(self, event): |
|
""" |
|
Called when wxEventSize is generated. |
|
|
|
In this application we attempt to resize to fit the window, so it |
|
is better to take the performance hit and redraw the whole window. |
|
""" |
|
|
|
_log.debug("%s - _on_size()", type(self)) |
|
sz = self.GetParent().GetSizer() |
|
if sz: |
|
si = sz.GetItem(self) |
|
if sz and si and not si.Proportion and not si.Flag & wx.EXPAND: |
|
|
|
size = self.GetMinSize() |
|
else: |
|
|
|
size = self.GetClientSize() |
|
|
|
size.IncTo(self.GetMinSize()) |
|
if getattr(self, "_width", None): |
|
if size == (self._width, self._height): |
|
|
|
return |
|
self._width, self._height = size |
|
self._isDrawn = False |
|
|
|
if self._width <= 1 or self._height <= 1: |
|
return |
|
|
|
|
|
self.bitmap = wx.Bitmap(self._width, self._height) |
|
|
|
dpival = self.figure.dpi |
|
winch = self._width / dpival |
|
hinch = self._height / dpival |
|
self.figure.set_size_inches(winch, hinch, forward=False) |
|
|
|
|
|
|
|
|
|
self.Refresh(eraseBackground=False) |
|
ResizeEvent("resize_event", self)._process() |
|
self.draw_idle() |
|
|
|
@staticmethod |
|
def _mpl_modifiers(event=None, *, exclude=None): |
|
mod_table = [ |
|
("ctrl", wx.MOD_CONTROL, wx.WXK_CONTROL), |
|
("alt", wx.MOD_ALT, wx.WXK_ALT), |
|
("shift", wx.MOD_SHIFT, wx.WXK_SHIFT), |
|
] |
|
if event is not None: |
|
modifiers = event.GetModifiers() |
|
return [name for name, mod, key in mod_table |
|
if modifiers & mod and exclude != key] |
|
else: |
|
return [name for name, mod, key in mod_table |
|
if wx.GetKeyState(key)] |
|
|
|
def _get_key(self, event): |
|
keyval = event.KeyCode |
|
if keyval in self.keyvald: |
|
key = self.keyvald[keyval] |
|
elif keyval < 256: |
|
key = chr(keyval) |
|
|
|
|
|
if not event.ShiftDown(): |
|
key = key.lower() |
|
else: |
|
return None |
|
mods = self._mpl_modifiers(event, exclude=keyval) |
|
if "shift" in mods and key.isupper(): |
|
mods.remove("shift") |
|
return "+".join([*mods, key]) |
|
|
|
def _mpl_coords(self, pos=None): |
|
""" |
|
Convert a wx position, defaulting to the current cursor position, to |
|
Matplotlib coordinates. |
|
""" |
|
if pos is None: |
|
pos = wx.GetMouseState() |
|
x, y = self.ScreenToClient(pos.X, pos.Y) |
|
else: |
|
x, y = pos.X, pos.Y |
|
|
|
return x, self.figure.bbox.height - y |
|
|
|
def _on_key_down(self, event): |
|
"""Capture key press.""" |
|
KeyEvent("key_press_event", self, |
|
self._get_key(event), *self._mpl_coords(), |
|
guiEvent=event)._process() |
|
if self: |
|
event.Skip() |
|
|
|
def _on_key_up(self, event): |
|
"""Release key.""" |
|
KeyEvent("key_release_event", self, |
|
self._get_key(event), *self._mpl_coords(), |
|
guiEvent=event)._process() |
|
if self: |
|
event.Skip() |
|
|
|
def set_cursor(self, cursor): |
|
|
|
cursor = wx.Cursor(_api.check_getitem({ |
|
cursors.MOVE: wx.CURSOR_HAND, |
|
cursors.HAND: wx.CURSOR_HAND, |
|
cursors.POINTER: wx.CURSOR_ARROW, |
|
cursors.SELECT_REGION: wx.CURSOR_CROSS, |
|
cursors.WAIT: wx.CURSOR_WAIT, |
|
cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE, |
|
cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS, |
|
}, cursor=cursor)) |
|
self.SetCursor(cursor) |
|
self.Refresh() |
|
|
|
def _set_capture(self, capture=True): |
|
"""Control wx mouse capture.""" |
|
if self.HasCapture(): |
|
self.ReleaseMouse() |
|
if capture: |
|
self.CaptureMouse() |
|
|
|
def _on_capture_lost(self, event): |
|
"""Capture changed or lost""" |
|
self._set_capture(False) |
|
|
|
def _on_mouse_button(self, event): |
|
"""Start measuring on an axis.""" |
|
event.Skip() |
|
self._set_capture(event.ButtonDown() or event.ButtonDClick()) |
|
x, y = self._mpl_coords(event) |
|
button_map = { |
|
wx.MOUSE_BTN_LEFT: MouseButton.LEFT, |
|
wx.MOUSE_BTN_MIDDLE: MouseButton.MIDDLE, |
|
wx.MOUSE_BTN_RIGHT: MouseButton.RIGHT, |
|
wx.MOUSE_BTN_AUX1: MouseButton.BACK, |
|
wx.MOUSE_BTN_AUX2: MouseButton.FORWARD, |
|
} |
|
button = event.GetButton() |
|
button = button_map.get(button, button) |
|
modifiers = self._mpl_modifiers(event) |
|
if event.ButtonDown(): |
|
MouseEvent("button_press_event", self, x, y, button, |
|
modifiers=modifiers, guiEvent=event)._process() |
|
elif event.ButtonDClick(): |
|
MouseEvent("button_press_event", self, x, y, button, |
|
dblclick=True, modifiers=modifiers, |
|
guiEvent=event)._process() |
|
elif event.ButtonUp(): |
|
MouseEvent("button_release_event", self, x, y, button, |
|
modifiers=modifiers, guiEvent=event)._process() |
|
|
|
def _on_mouse_wheel(self, event): |
|
"""Translate mouse wheel events into matplotlib events""" |
|
x, y = self._mpl_coords(event) |
|
|
|
step = event.LinesPerAction * event.WheelRotation / event.WheelDelta |
|
|
|
event.Skip() |
|
|
|
if wx.Platform == '__WXMAC__': |
|
if not hasattr(self, '_skipwheelevent'): |
|
self._skipwheelevent = True |
|
elif self._skipwheelevent: |
|
self._skipwheelevent = False |
|
return |
|
else: |
|
self._skipwheelevent = True |
|
MouseEvent("scroll_event", self, x, y, step=step, |
|
modifiers=self._mpl_modifiers(event), |
|
guiEvent=event)._process() |
|
|
|
def _on_motion(self, event): |
|
"""Start measuring on an axis.""" |
|
event.Skip() |
|
MouseEvent("motion_notify_event", self, |
|
*self._mpl_coords(event), |
|
modifiers=self._mpl_modifiers(event), |
|
guiEvent=event)._process() |
|
|
|
def _on_enter(self, event): |
|
"""Mouse has entered the window.""" |
|
event.Skip() |
|
LocationEvent("figure_enter_event", self, |
|
*self._mpl_coords(event), |
|
modifiers=self._mpl_modifiers(), |
|
guiEvent=event)._process() |
|
|
|
def _on_leave(self, event): |
|
"""Mouse has left the window.""" |
|
event.Skip() |
|
LocationEvent("figure_leave_event", self, |
|
*self._mpl_coords(event), |
|
modifiers=self._mpl_modifiers(), |
|
guiEvent=event)._process() |
|
|
|
|
|
class FigureCanvasWx(_FigureCanvasWxBase): |
|
|
|
|
|
def draw(self, drawDC=None): |
|
""" |
|
Render the figure using RendererWx instance renderer, or using a |
|
previously defined renderer if none is specified. |
|
""" |
|
_log.debug("%s - draw()", type(self)) |
|
self.renderer = RendererWx(self.bitmap, self.figure.dpi) |
|
self.figure.draw(self.renderer) |
|
self._isDrawn = True |
|
self.gui_repaint(drawDC=drawDC) |
|
|
|
def _print_image(self, filetype, filename): |
|
bitmap = wx.Bitmap(math.ceil(self.figure.bbox.width), |
|
math.ceil(self.figure.bbox.height)) |
|
self.figure.draw(RendererWx(bitmap, self.figure.dpi)) |
|
saved_obj = (bitmap.ConvertToImage() |
|
if cbook.is_writable_file_like(filename) |
|
else bitmap) |
|
if not saved_obj.SaveFile(filename, filetype): |
|
raise RuntimeError(f'Could not save figure to {filename}') |
|
|
|
|
|
|
|
|
|
if self._isDrawn: |
|
self.draw() |
|
|
|
|
|
if self: |
|
self.Refresh() |
|
|
|
print_bmp = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_BMP) |
|
print_jpeg = print_jpg = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_JPEG) |
|
print_pcx = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_PCX) |
|
print_png = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_PNG) |
|
print_tiff = print_tif = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_TIF) |
|
print_xpm = functools.partialmethod( |
|
_print_image, wx.BITMAP_TYPE_XPM) |
|
|
|
|
|
class FigureFrameWx(wx.Frame): |
|
def __init__(self, num, fig, *, canvas_class): |
|
|
|
|
|
if wx.Platform == '__WXMSW__': |
|
pos = wx.DefaultPosition |
|
else: |
|
pos = wx.Point(20, 20) |
|
super().__init__(parent=None, id=-1, pos=pos) |
|
|
|
_log.debug("%s - __init__()", type(self)) |
|
_set_frame_icon(self) |
|
|
|
self.canvas = canvas_class(self, -1, fig) |
|
|
|
manager = FigureManagerWx(self.canvas, num, self) |
|
|
|
toolbar = self.canvas.manager.toolbar |
|
if toolbar is not None: |
|
self.SetToolBar(toolbar) |
|
|
|
|
|
|
|
w, h = map(math.ceil, fig.bbox.size) |
|
self.canvas.SetInitialSize(wx.Size(w, h)) |
|
self.canvas.SetMinSize((2, 2)) |
|
self.canvas.SetFocus() |
|
|
|
self.Fit() |
|
|
|
self.Bind(wx.EVT_CLOSE, self._on_close) |
|
|
|
def _on_close(self, event): |
|
_log.debug("%s - on_close()", type(self)) |
|
CloseEvent("close_event", self.canvas)._process() |
|
self.canvas.stop_event_loop() |
|
|
|
|
|
self.canvas.manager.frame = None |
|
|
|
Gcf.destroy(self.canvas.manager) |
|
try: |
|
self.canvas.mpl_disconnect(self.canvas.toolbar._id_drag) |
|
except AttributeError: |
|
pass |
|
|
|
event.Skip() |
|
|
|
|
|
class FigureManagerWx(FigureManagerBase): |
|
""" |
|
Container/controller for the FigureCanvas and GUI frame. |
|
|
|
It is instantiated by Gcf whenever a new figure is created. Gcf is |
|
responsible for managing multiple instances of FigureManagerWx. |
|
|
|
Attributes |
|
---------- |
|
canvas : `FigureCanvas` |
|
a FigureCanvasWx(wx.Panel) instance |
|
window : wxFrame |
|
a wxFrame instance - wxpython.org/Phoenix/docs/html/Frame.html |
|
""" |
|
|
|
def __init__(self, canvas, num, frame): |
|
_log.debug("%s - __init__()", type(self)) |
|
self.frame = self.window = frame |
|
super().__init__(canvas, num) |
|
|
|
@classmethod |
|
def create_with_canvas(cls, canvas_class, figure, num): |
|
|
|
wxapp = wx.GetApp() or _create_wxapp() |
|
frame = FigureFrameWx(num, figure, canvas_class=canvas_class) |
|
manager = figure.canvas.manager |
|
if mpl.is_interactive(): |
|
manager.frame.Show() |
|
figure.canvas.draw_idle() |
|
return manager |
|
|
|
@classmethod |
|
def start_main_loop(cls): |
|
if not wx.App.IsMainLoopRunning(): |
|
wxapp = wx.GetApp() |
|
if wxapp is not None: |
|
wxapp.MainLoop() |
|
|
|
def show(self): |
|
|
|
self.frame.Show() |
|
self.canvas.draw() |
|
if mpl.rcParams['figure.raise_window']: |
|
self.frame.Raise() |
|
|
|
def destroy(self, *args): |
|
|
|
_log.debug("%s - destroy()", type(self)) |
|
frame = self.frame |
|
if frame: |
|
|
|
|
|
wx.CallAfter(frame.Close) |
|
|
|
def full_screen_toggle(self): |
|
|
|
self.frame.ShowFullScreen(not self.frame.IsFullScreen()) |
|
|
|
def get_window_title(self): |
|
|
|
return self.window.GetTitle() |
|
|
|
def set_window_title(self, title): |
|
|
|
self.window.SetTitle(title) |
|
|
|
def resize(self, width, height): |
|
|
|
|
|
self.window.SetSize(self.window.ClientToWindowSize(wx.Size( |
|
math.ceil(width), math.ceil(height)))) |
|
|
|
|
|
def _load_bitmap(filename): |
|
""" |
|
Load a wx.Bitmap from a file in the "images" directory of the Matplotlib |
|
data. |
|
""" |
|
return wx.Bitmap(str(cbook._get_data_path('images', filename))) |
|
|
|
|
|
def _set_frame_icon(frame): |
|
bundle = wx.IconBundle() |
|
for image in ('matplotlib.png', 'matplotlib_large.png'): |
|
icon = wx.Icon(_load_bitmap(image)) |
|
if not icon.IsOk(): |
|
return |
|
bundle.AddIcon(icon) |
|
frame.SetIcons(bundle) |
|
|
|
|
|
class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar): |
|
def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM): |
|
wx.ToolBar.__init__(self, canvas.GetParent(), -1, style=style) |
|
|
|
if 'wxMac' in wx.PlatformInfo: |
|
self.SetToolBitmapSize((24, 24)) |
|
self.wx_ids = {} |
|
for text, tooltip_text, image_file, callback in self.toolitems: |
|
if text is None: |
|
self.AddSeparator() |
|
continue |
|
self.wx_ids[text] = ( |
|
self.AddTool( |
|
-1, |
|
bitmap=self._icon(f"{image_file}.png"), |
|
bmpDisabled=wx.NullBitmap, |
|
label=text, shortHelp=tooltip_text, |
|
kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"] |
|
else wx.ITEM_NORMAL)) |
|
.Id) |
|
self.Bind(wx.EVT_TOOL, getattr(self, callback), |
|
id=self.wx_ids[text]) |
|
|
|
self._coordinates = coordinates |
|
if self._coordinates: |
|
self.AddStretchableSpace() |
|
self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) |
|
self.AddControl(self._label_text) |
|
|
|
self.Realize() |
|
|
|
NavigationToolbar2.__init__(self, canvas) |
|
|
|
@staticmethod |
|
def _icon(name): |
|
""" |
|
Construct a `wx.Bitmap` suitable for use as icon from an image file |
|
*name*, including the extension and relative to Matplotlib's "images" |
|
data directory. |
|
""" |
|
pilimg = PIL.Image.open(cbook._get_data_path("images", name)) |
|
|
|
image = np.array(pilimg.convert("RGBA")) |
|
try: |
|
dark = wx.SystemSettings.GetAppearance().IsDark() |
|
except AttributeError: |
|
|
|
bg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) |
|
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) |
|
|
|
bg_lum = (.299 * bg.red + .587 * bg.green + .114 * bg.blue) / 255 |
|
fg_lum = (.299 * fg.red + .587 * fg.green + .114 * fg.blue) / 255 |
|
dark = fg_lum - bg_lum > .2 |
|
if dark: |
|
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) |
|
black_mask = (image[..., :3] == 0).all(axis=-1) |
|
image[black_mask, :3] = (fg.Red(), fg.Green(), fg.Blue()) |
|
return wx.Bitmap.FromBufferRGBA( |
|
image.shape[1], image.shape[0], image.tobytes()) |
|
|
|
def _update_buttons_checked(self): |
|
if "Pan" in self.wx_ids: |
|
self.ToggleTool(self.wx_ids["Pan"], self.mode.name == "PAN") |
|
if "Zoom" in self.wx_ids: |
|
self.ToggleTool(self.wx_ids["Zoom"], self.mode.name == "ZOOM") |
|
|
|
def zoom(self, *args): |
|
super().zoom(*args) |
|
self._update_buttons_checked() |
|
|
|
def pan(self, *args): |
|
super().pan(*args) |
|
self._update_buttons_checked() |
|
|
|
def save_figure(self, *args): |
|
|
|
filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards() |
|
default_file = self.canvas.get_default_filename() |
|
dialog = wx.FileDialog( |
|
self.canvas.GetParent(), "Save to file", |
|
mpl.rcParams["savefig.directory"], default_file, filetypes, |
|
wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) |
|
dialog.SetFilterIndex(filter_index) |
|
if dialog.ShowModal() == wx.ID_OK: |
|
path = pathlib.Path(dialog.GetPath()) |
|
_log.debug('%s - Save file path: %s', type(self), path) |
|
fmt = exts[dialog.GetFilterIndex()] |
|
ext = path.suffix[1:] |
|
if ext in self.canvas.get_supported_filetypes() and fmt != ext: |
|
|
|
|
|
_log.warning('extension %s did not match the selected ' |
|
'image type %s; going with %s', |
|
ext, fmt, ext) |
|
fmt = ext |
|
|
|
if mpl.rcParams["savefig.directory"]: |
|
mpl.rcParams["savefig.directory"] = str(path.parent) |
|
try: |
|
self.canvas.figure.savefig(path, format=fmt) |
|
except Exception as e: |
|
dialog = wx.MessageDialog( |
|
parent=self.canvas.GetParent(), message=str(e), |
|
caption='Matplotlib error') |
|
dialog.ShowModal() |
|
dialog.Destroy() |
|
|
|
def draw_rubberband(self, event, x0, y0, x1, y1): |
|
height = self.canvas.figure.bbox.height |
|
self.canvas._rubberband_rect = (x0, height - y0, x1, height - y1) |
|
self.canvas.Refresh() |
|
|
|
def remove_rubberband(self): |
|
self.canvas._rubberband_rect = None |
|
self.canvas.Refresh() |
|
|
|
def set_message(self, s): |
|
if self._coordinates: |
|
self._label_text.SetLabel(s) |
|
|
|
def set_history_buttons(self): |
|
can_backward = self._nav_stack._pos > 0 |
|
can_forward = self._nav_stack._pos < len(self._nav_stack) - 1 |
|
if 'Back' in self.wx_ids: |
|
self.EnableTool(self.wx_ids['Back'], can_backward) |
|
if 'Forward' in self.wx_ids: |
|
self.EnableTool(self.wx_ids['Forward'], can_forward) |
|
|
|
|
|
|
|
|
|
class ToolbarWx(ToolContainerBase, wx.ToolBar): |
|
def __init__(self, toolmanager, parent=None, style=wx.TB_BOTTOM): |
|
if parent is None: |
|
parent = toolmanager.canvas.GetParent() |
|
ToolContainerBase.__init__(self, toolmanager) |
|
wx.ToolBar.__init__(self, parent, -1, style=style) |
|
self._space = self.AddStretchableSpace() |
|
self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) |
|
self.AddControl(self._label_text) |
|
self._toolitems = {} |
|
self._groups = {} |
|
|
|
def _get_tool_pos(self, tool): |
|
""" |
|
Find the position (index) of a wx.ToolBarToolBase in a ToolBar. |
|
|
|
``ToolBar.GetToolPos`` is not useful because wx assigns the same Id to |
|
all Separators and StretchableSpaces. |
|
""" |
|
pos, = [pos for pos in range(self.ToolsCount) |
|
if self.GetToolByPos(pos) == tool] |
|
return pos |
|
|
|
def add_toolitem(self, name, group, position, image_file, description, |
|
toggle): |
|
|
|
if group not in self._groups: |
|
self._groups[group] = self.InsertSeparator( |
|
self._get_tool_pos(self._space)) |
|
sep = self._groups[group] |
|
|
|
seps = [t for t in map(self.GetToolByPos, range(self.ToolsCount)) |
|
if t.IsSeparator() and not t.IsStretchableSpace()] |
|
|
|
if position >= 0: |
|
|
|
|
|
start = (0 if sep == seps[0] |
|
else self._get_tool_pos(seps[seps.index(sep) - 1]) + 1) |
|
else: |
|
|
|
start = self._get_tool_pos(sep) + 1 |
|
idx = start + position |
|
if image_file: |
|
bmp = NavigationToolbar2Wx._icon(image_file) |
|
kind = wx.ITEM_NORMAL if not toggle else wx.ITEM_CHECK |
|
tool = self.InsertTool(idx, -1, name, bmp, wx.NullBitmap, kind, |
|
description or "") |
|
else: |
|
size = (self.GetTextExtent(name)[0] + 10, -1) |
|
if toggle: |
|
control = wx.ToggleButton(self, -1, name, size=size) |
|
else: |
|
control = wx.Button(self, -1, name, size=size) |
|
tool = self.InsertControl(idx, control, label=name) |
|
self.Realize() |
|
|
|
def handler(event): |
|
self.trigger_tool(name) |
|
|
|
if image_file: |
|
self.Bind(wx.EVT_TOOL, handler, tool) |
|
else: |
|
control.Bind(wx.EVT_LEFT_DOWN, handler) |
|
|
|
self._toolitems.setdefault(name, []) |
|
self._toolitems[name].append((tool, handler)) |
|
|
|
def toggle_toolitem(self, name, toggled): |
|
if name not in self._toolitems: |
|
return |
|
for tool, handler in self._toolitems[name]: |
|
if not tool.IsControl(): |
|
self.ToggleTool(tool.Id, toggled) |
|
else: |
|
tool.GetControl().SetValue(toggled) |
|
self.Refresh() |
|
|
|
def remove_toolitem(self, name): |
|
for tool, handler in self._toolitems[name]: |
|
self.DeleteTool(tool.Id) |
|
del self._toolitems[name] |
|
|
|
def set_message(self, s): |
|
self._label_text.SetLabel(s) |
|
|
|
|
|
@backend_tools._register_tool_class(_FigureCanvasWxBase) |
|
class ConfigureSubplotsWx(backend_tools.ConfigureSubplotsBase): |
|
def trigger(self, *args): |
|
NavigationToolbar2Wx.configure_subplots(self) |
|
|
|
|
|
@backend_tools._register_tool_class(_FigureCanvasWxBase) |
|
class SaveFigureWx(backend_tools.SaveFigureBase): |
|
def trigger(self, *args): |
|
NavigationToolbar2Wx.save_figure( |
|
self._make_classic_style_pseudo_toolbar()) |
|
|
|
|
|
@backend_tools._register_tool_class(_FigureCanvasWxBase) |
|
class RubberbandWx(backend_tools.RubberbandBase): |
|
def draw_rubberband(self, x0, y0, x1, y1): |
|
NavigationToolbar2Wx.draw_rubberband( |
|
self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) |
|
|
|
def remove_rubberband(self): |
|
NavigationToolbar2Wx.remove_rubberband( |
|
self._make_classic_style_pseudo_toolbar()) |
|
|
|
|
|
class _HelpDialog(wx.Dialog): |
|
_instance = None |
|
headers = [("Action", "Shortcuts", "Description")] |
|
widths = [100, 140, 300] |
|
|
|
def __init__(self, parent, help_entries): |
|
super().__init__(parent, title="Help", |
|
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) |
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL) |
|
grid_sizer = wx.FlexGridSizer(0, 3, 8, 6) |
|
|
|
bold = self.GetFont().MakeBold() |
|
for r, row in enumerate(self.headers + help_entries): |
|
for (col, width) in zip(row, self.widths): |
|
label = wx.StaticText(self, label=col) |
|
if r == 0: |
|
label.SetFont(bold) |
|
label.Wrap(width) |
|
grid_sizer.Add(label, 0, 0, 0) |
|
|
|
sizer.Add(grid_sizer, 0, wx.ALL, 6) |
|
ok = wx.Button(self, wx.ID_OK) |
|
sizer.Add(ok, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 8) |
|
self.SetSizer(sizer) |
|
sizer.Fit(self) |
|
self.Layout() |
|
self.Bind(wx.EVT_CLOSE, self._on_close) |
|
ok.Bind(wx.EVT_BUTTON, self._on_close) |
|
|
|
def _on_close(self, event): |
|
_HelpDialog._instance = None |
|
self.DestroyLater() |
|
event.Skip() |
|
|
|
@classmethod |
|
def show(cls, parent, help_entries): |
|
|
|
if cls._instance: |
|
cls._instance.Raise() |
|
return |
|
cls._instance = cls(parent, help_entries) |
|
cls._instance.Show() |
|
|
|
|
|
@backend_tools._register_tool_class(_FigureCanvasWxBase) |
|
class HelpWx(backend_tools.ToolHelpBase): |
|
def trigger(self, *args): |
|
_HelpDialog.show(self.figure.canvas.GetTopLevelParent(), |
|
self._get_help_entries()) |
|
|
|
|
|
@backend_tools._register_tool_class(_FigureCanvasWxBase) |
|
class ToolCopyToClipboardWx(backend_tools.ToolCopyToClipboardBase): |
|
def trigger(self, *args, **kwargs): |
|
if not self.canvas._isDrawn: |
|
self.canvas.draw() |
|
if not self.canvas.bitmap.IsOk() or not wx.TheClipboard.Open(): |
|
return |
|
try: |
|
wx.TheClipboard.SetData(wx.BitmapDataObject(self.canvas.bitmap)) |
|
finally: |
|
wx.TheClipboard.Close() |
|
|
|
|
|
FigureManagerWx._toolbar2_class = NavigationToolbar2Wx |
|
FigureManagerWx._toolmanager_toolbar_class = ToolbarWx |
|
|
|
|
|
@_Backend.export |
|
class _BackendWx(_Backend): |
|
FigureCanvas = FigureCanvasWx |
|
FigureManager = FigureManagerWx |
|
mainloop = FigureManagerWx.start_main_loop |
|
|