mwxlib 1.4.20__py3-none-any.whl → 1.5.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mwxlib might be problematic. Click here for more details.

mwx/graphman.py CHANGED
@@ -100,8 +100,6 @@ class Thread:
100
100
  None : {
101
101
  'thread_begin' : [ None ], # begin processing
102
102
  'thread_end' : [ None ], # end processing
103
- 'thread_quit' : [ None ], # terminated by user
104
- 'thread_error' : [ None ], # failed in error
105
103
  },
106
104
  })
107
105
 
@@ -115,9 +113,7 @@ class Thread:
115
113
  Allows only this worker (but no other thread) to enter.
116
114
  """
117
115
  frame = inspect.currentframe().f_back.f_back
118
- filename = frame.f_code.co_filename
119
116
  name = frame.f_code.co_name
120
- fname, _ = os.path.splitext(os.path.basename(filename))
121
117
 
122
118
  ## Other threads are not allowed to enter.
123
119
  ct = threading.current_thread()
@@ -125,17 +121,22 @@ class Thread:
125
121
 
126
122
  ## The thread must be activated to enter.
127
123
  ## assert self.active, f"{self!r} must be activated to enter {name!r}."
128
- try:
129
- self.handler(f"{fname}/{name}:enter", self)
130
- yield self
131
- except Exception:
132
- self.handler(f"{fname}/{name}:error", self)
133
- raise
134
- finally:
135
- self.handler(f"{fname}/{name}:exit", self)
124
+ yield self
125
+
126
+ def enters(self, f):
127
+ """Decorator to add a one-time handler for the enter event.
128
+ The specified function will be called from the main thread.
129
+ """
130
+ return self.handler.binds('thread_begin', _F(f))
131
+
132
+ def exits(self, f):
133
+ """Decorator to add a one-time handler for the exit event.
134
+ The specified function will be called from the main thread.
135
+ """
136
+ return self.handler.binds('thread_end', _F(f))
136
137
 
137
138
  def wraps(self, f, *args, **kwargs):
138
- """Decorator of thread starter function."""
139
+ """Decorator for a function that starts a new thread."""
139
140
  @wraps(f)
140
141
  def _f(*v, **kw):
141
142
  return self.Start(f, *v, *args, **kw, **kwargs)
@@ -186,21 +187,23 @@ class Thread:
186
187
  @wraps(f)
187
188
  def _f(*v, **kw):
188
189
  try:
189
- self.handler('thread_begin', self)
190
+ wx.CallAfter(self.handler, 'thread_begin', self)
190
191
  self.result = f(*v, **kw)
191
192
  except BdbQuit:
192
193
  pass
193
194
  except KeyboardInterrupt as e:
194
- print("- Thread execution stopped:", e)
195
- except AssertionError as e:
196
- print("- Thread execution failed:", e)
195
+ print("- Thread terminated by user:", e)
197
196
  except Exception as e:
197
+ tbstr = traceback.format_tb(e.__traceback__)
198
+ wx.CallAfter(wx.MessageBox,
199
+ f"{e}\n\n" + tbstr[-1] + f"{type(e).__name__}: {e}",
200
+ f"Error in the thread running {f.__name__!r}\n\n",
201
+ style=wx.ICON_ERROR)
198
202
  traceback.print_exc()
199
- print("- Thread exception:", e)
200
- self.handler('thread_error', self)
203
+ print("- Thread failed in error:", e)
201
204
  finally:
202
205
  self.active = 0
203
- self.handler('thread_end', self)
206
+ wx.CallAfter(self.handler, 'thread_end', self)
204
207
 
205
208
  if self.running:
206
209
  wx.MessageBox("The thread is running (Press [C-g] to quit).",
@@ -226,11 +229,10 @@ class Thread:
226
229
  def _stop():
227
230
  with wx.BusyInfo("One moment please, "
228
231
  "waiting for threads to die..."):
229
- self.handler('thread_quit', self)
230
232
  self.worker.join(1)
231
233
  if self.running:
232
234
  self.active = 0
233
- wx.CallAfter(_stop) # main-thread で終了させる
235
+ wx.CallAfter(_stop) # main thread で終了させる
234
236
 
235
237
 
236
238
  class LayerInterface(CtrlInterface):
@@ -349,10 +351,6 @@ class LayerInterface(CtrlInterface):
349
351
 
350
352
  self.handler.append({ # DNA<Layer>
351
353
  None : {
352
- 'thread_begin' : [ None ], # begin processing
353
- 'thread_end' : [ None ], # end processing
354
- 'thread_quit' : [ None ], # terminated by user
355
- 'thread_error' : [ None ], # failed in error
356
354
  'page_shown' : [ None, _F(self.Draw, show=True) ],
357
355
  'page_closed' : [ None, _F(self.Draw, show=False) ],
358
356
  'page_hidden' : [ None, _F(self.Draw, show=False) ],
@@ -448,7 +446,7 @@ class LayerInterface(CtrlInterface):
448
446
 
449
447
  Shown = property(
450
448
  lambda self: self.IsShown(),
451
- lambda self,v: self.Show(v))
449
+ lambda self, v: self.Show(v))
452
450
 
453
451
  def IsShown(self):
454
452
  """Return True if the window is physically visible on the screen.
@@ -465,11 +463,11 @@ class LayerInterface(CtrlInterface):
465
463
  (override) Show associated pane window.
466
464
  Note: This might be called from a thread.
467
465
  """
468
- wx.CallAfter(self.parent.show_pane, self, show)
466
+ wx.CallAfter(self.parent.show_pane, self, show) # Show pane windows in the main thread.
469
467
 
470
468
  Drawn = property(
471
469
  lambda self: self.IsDrawn(),
472
- lambda self,v: self.Draw(v))
470
+ lambda self, v: self.Draw(v))
473
471
 
474
472
  def IsDrawn(self):
475
473
  return any(art.get_visible() for art in self.Arts)
@@ -503,6 +501,30 @@ class Layer(LayerInterface, KnobCtrlPanel):
503
501
  LayerInterface.__init__(self, parent, session)
504
502
 
505
503
 
504
+ def register(cls, module=None):
505
+ """Register dummy plug; Add module.Plugin <Layer>.
506
+ """
507
+ if not module:
508
+ module = inspect.getmodule(cls) # rebase module or __main__
509
+
510
+ if issubclass(cls, LayerInterface):
511
+ cls.__module__ = module.__name__ # __main__ to module
512
+ warn(f"Duplicate iniheritance of LayerInterface by {cls}.")
513
+ module.Plugin = cls
514
+ return cls
515
+
516
+ class _Plugin(LayerInterface, cls):
517
+ def __init__(self, parent, session=None, **kwargs):
518
+ cls.__init__(self, parent, **kwargs)
519
+ LayerInterface.__init__(self, parent, session)
520
+
521
+ _Plugin.__module__ = cls.__module__ = module.__name__
522
+ _Plugin.__name__ = cls.__name__ + str("~")
523
+ _Plugin.__doc__ = cls.__doc__
524
+ module.Plugin = _Plugin
525
+ return _Plugin
526
+
527
+
506
528
  class Graph(GraphPlot):
507
529
  """GraphPlot (override) to better make use for graph manager
508
530
 
@@ -583,7 +605,7 @@ class MyFileDropLoader(wx.FileDropTarget):
583
605
  self.loader = loader
584
606
 
585
607
  def OnDropFiles(self, x, y, filenames):
586
- pos = self.view.ScreenPosition + (x,y)
608
+ pos = self.view.ScreenPosition + (x, y)
587
609
  paths = []
588
610
  for fn in filenames:
589
611
  name, ext = os.path.splitext(fn)
@@ -660,7 +682,7 @@ class Frame(mwx.Frame):
660
682
  self.histogram.Name = "histogram"
661
683
 
662
684
  self._mgr.AddPane(self.graph,
663
- aui.AuiPaneInfo().CenterPane().CloseButton(1)
685
+ aui.AuiPaneInfo().CenterPane()
664
686
  .Name("graph").Caption("graph").CaptionVisible(1))
665
687
 
666
688
  size = (200, 200)
@@ -785,7 +807,7 @@ class Frame(mwx.Frame):
785
807
  self.menubar.reset()
786
808
 
787
809
  def show_frameview(frame):
788
- wx.CallAfter(self.show_pane, frame.parent) # Show graph / output
810
+ wx.CallAfter(self.show_pane, frame.parent) # Show graph / output in the main thread.
789
811
 
790
812
  self.graph.handler.append({ # DNA<Graph:Frame>
791
813
  None : {
@@ -824,8 +846,8 @@ class Frame(mwx.Frame):
824
846
  _display(self.graph, show)
825
847
  _display(self.output, show)
826
848
  evt.Skip()
827
- self.Bind(wx.EVT_MOVE_START, lambda v :on_move(v, show=0))
828
- self.Bind(wx.EVT_MOVE_END, lambda v :on_move(v, show=1))
849
+ self.Bind(wx.EVT_MOVE_START, lambda v: on_move(v, show=0))
850
+ self.Bind(wx.EVT_MOVE_END, lambda v: on_move(v, show=1))
829
851
 
830
852
  ## Custom Key Bindings
831
853
  self.define_key('* C-g', self.Quit)
@@ -843,13 +865,13 @@ class Frame(mwx.Frame):
843
865
  def sync(self, a, b):
844
866
  """Synchronize b to a."""
845
867
  if (self.SYNC_SWITCH
846
- and a.frame and b.frame
847
- and a.frame.unit == b.frame.unit
848
- and a.buffer.shape == b.buffer.shape):
849
- b.xlim = a.xlim
850
- b.ylim = a.ylim
851
- b.OnDraw(None)
852
- b.canvas.draw_idle()
868
+ and a.frame and b.frame
869
+ and a.frame.unit == b.frame.unit
870
+ and a.buffer.shape == b.buffer.shape):
871
+ b.xlim = a.xlim
872
+ b.ylim = a.ylim
873
+ b.OnDraw(None)
874
+ b.canvas.draw_idle()
853
875
 
854
876
  def set_title(self, frame):
855
877
  ssn = os.path.basename(self.session_file or '--')
@@ -927,7 +949,7 @@ class Frame(mwx.Frame):
927
949
  ## ドッキング時に再計算される
928
950
  if name == "output" or name is self.output:
929
951
  w, h = self.graph.GetClientSize()
930
- pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
952
+ pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
931
953
 
932
954
  ## Force Layer windows to show.
933
955
  if interactive:
@@ -943,12 +965,8 @@ class Frame(mwx.Frame):
943
965
  pane.Float()
944
966
  show = True
945
967
 
946
- ## Note: We need to distinguish cases whether:
947
- ## - pane.window is AuiNotebook or normal Panel,
948
- ## - pane.window is floating (win.Parent is AuiFloatingFrame) or docked.
949
-
950
- plug = self.get_plug(name) # -> None if pane.window is a Graph
951
- win = pane.window # -> Window (plug / notebook / Graph)
968
+ plug = self.get_plug(name) # -> None if pane.window is a Graph
969
+ win = pane.window
952
970
  try:
953
971
  shown = plug.IsShown()
954
972
  except AttributeError:
@@ -958,7 +976,7 @@ class Frame(mwx.Frame):
958
976
  if isinstance(win, aui.AuiNotebook):
959
977
  j = win.GetPageIndex(plug)
960
978
  if j != win.Selection:
961
- win.Selection = j # the focus moves => EVT_SHOW
979
+ win.Selection = j # the focus moves => EVT_SHOW
962
980
  else:
963
981
  plug.handler('page_shown', plug)
964
982
  else:
@@ -999,7 +1017,7 @@ class Frame(mwx.Frame):
999
1017
  pane.dock_direction = dock
1000
1018
  if not plug.caption:
1001
1019
  pane.CaptionVisible(False) # no caption bar
1002
- pane.Gripper(dock not in (0, 5)) # show a grip when docked
1020
+ pane.Gripper(dock not in (0,5)) # show a grip when docked
1003
1021
  pane.Dockable(dock)
1004
1022
 
1005
1023
  if pane.dock_direction:
@@ -1050,30 +1068,6 @@ class Frame(mwx.Frame):
1050
1068
  elif isinstance(name, LayerInterface):
1051
1069
  return name
1052
1070
 
1053
- @staticmethod
1054
- def register(cls, module=None):
1055
- """Register dummy plug; Add module.Plugin <Layer>.
1056
- """
1057
- if not module:
1058
- module = inspect.getmodule(cls) # rebase module or __main__
1059
-
1060
- if issubclass(cls, LayerInterface):
1061
- cls.__module__ = module.__name__ # __main__ to module
1062
- warn(f"Duplicate iniheritance of LayerInterface by {cls}.")
1063
- module.Plugin = cls
1064
- return cls
1065
-
1066
- class _Plugin(LayerInterface, cls):
1067
- def __init__(self, parent, session=None, **kwargs):
1068
- cls.__init__(self, parent, **kwargs)
1069
- LayerInterface.__init__(self, parent, session)
1070
-
1071
- _Plugin.__module__ = cls.__module__ = module.__name__
1072
- _Plugin.__name__ = cls.__name__ + str("~")
1073
- _Plugin.__doc__ = cls.__doc__
1074
- module.Plugin = _Plugin
1075
- return _Plugin
1076
-
1077
1071
  def load_module(self, root):
1078
1072
  """Load module of plugin (internal use only).
1079
1073
 
@@ -1102,17 +1096,17 @@ class Frame(mwx.Frame):
1102
1096
  print(f"- Unable to load {root!r}.", e)
1103
1097
  return False
1104
1098
 
1105
- ## the module must have a class `Plugin`.
1099
+ ## Check if the module has a class `Plugin`.
1106
1100
  if not hasattr(module, 'Plugin'):
1107
1101
  if isinstance(root, type):
1108
1102
  warn(f"Use dummy plug for debugging {name!r}.")
1109
1103
  module.__dummy_plug__ = root
1110
- self.register(root, module)
1104
+ register(root, module)
1111
1105
  else:
1112
1106
  if hasattr(module, '__dummy_plug__'):
1113
1107
  root = module.__dummy_plug__ # old class (imported)
1114
1108
  cls = getattr(module, root.__name__) # new class (reloaded)
1115
- self.register(cls, module)
1109
+ register(cls, module)
1116
1110
  return module
1117
1111
 
1118
1112
  def load_plug(self, root, force=False, session=None, show=False,
@@ -1159,48 +1153,47 @@ class Frame(mwx.Frame):
1159
1153
 
1160
1154
  module = self.load_module(root)
1161
1155
  if not module:
1162
- return False # failed to import
1156
+ return False
1163
1157
 
1158
+ ## assert name == Plugin.__module__
1164
1159
  try:
1165
- name = module.Plugin.__module__
1166
- title = module.Plugin.category
1167
-
1168
- pane = self._mgr.GetPane(title)
1169
-
1170
- if pane.IsOk(): # <pane:title> is already registered
1171
- nb = pane.window
1172
- if not isinstance(nb, aui.AuiNotebook):
1173
- raise NameError("Notebook name must not be the same as any other plugins")
1160
+ Plugin = module.Plugin # Check if the module has a class `Plugin`.
1161
+ title = Plugin.category # Plugin <LayerInterface>
1174
1162
 
1175
- pane = self.get_pane(name)
1163
+ pane = self._mgr.GetPane(title) # Check if <pane:title> is already registered.
1164
+ if pane.IsOk():
1165
+ if not isinstance(pane.window, aui.AuiNotebook):
1166
+ raise NameError("Notebook name must not be the same as any other plugin")
1176
1167
 
1177
- if pane.IsOk(): # <pane:name> is already registered
1168
+ pane = self.get_pane(name) # Check if <pane:name> is already registered.
1169
+ if pane.IsOk():
1178
1170
  if name not in self.plugins:
1179
- raise NameError("Plugin name must not be the same as any other panes")
1180
-
1181
- show = show or pane.IsShown()
1182
- props.update(
1183
- dock_direction = pane.IsDocked() and pane.dock_direction,
1184
- floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
1185
- floating_size = floating_size or pane.floating_size[:], # copy unloading pane
1186
- )
1171
+ raise NameError("Plugin name must not be the same as any other pane")
1172
+
1187
1173
  except (AttributeError, NameError) as e:
1188
1174
  traceback.print_exc()
1189
- wx.CallAfter(wx.MessageBox,
1175
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1190
1176
  f"{e}\n\n" + traceback.format_exc(),
1191
1177
  f"Error in loading {module.__name__!r}",
1192
1178
  style=wx.ICON_ERROR)
1193
1179
  return False
1194
1180
 
1195
- ## Create and register the plugin
1181
+ ## Unload the plugin if loaded.
1196
1182
  if pane.IsOk():
1197
- self.unload_plug(name) # unload once right here
1183
+ show = show or pane.IsShown()
1184
+ props.update(
1185
+ dock_direction = pane.IsDocked() and pane.dock_direction,
1186
+ floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
1187
+ floating_size = floating_size or pane.floating_size[:], # copy unloading pane
1188
+ )
1189
+ self.unload_plug(name)
1198
1190
 
1191
+ ## Create the plugin object.
1199
1192
  try:
1200
- plug = module.Plugin(self, session, **kwargs)
1193
+ plug = Plugin(self, session, **kwargs)
1201
1194
  except Exception as e:
1202
1195
  traceback.print_exc()
1203
- wx.CallAfter(wx.MessageBox,
1196
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1204
1197
  f"{e}\n\n" + traceback.format_exc(),
1205
1198
  f"Error in loading {name!r}",
1206
1199
  style=wx.ICON_ERROR)
@@ -1209,10 +1202,10 @@ class Frame(mwx.Frame):
1209
1202
  ## Add to the list after the plug is created successfully.
1210
1203
  self.plugins[name] = module
1211
1204
 
1212
- ## set reference of a plug (one module, one plugin)
1205
+ ## Set reference of a plug (one module, one plugin).
1213
1206
  module.__plug__ = plug
1214
1207
 
1215
- ## Create pane or notebook pane
1208
+ ## Create pane or notebook pane.
1216
1209
  caption = plug.caption
1217
1210
  if not isinstance(caption, str):
1218
1211
  caption = name
@@ -1224,7 +1217,7 @@ class Frame(mwx.Frame):
1224
1217
  nb = pane.window
1225
1218
  nb.AddPage(plug, caption)
1226
1219
  else:
1227
- size = plug.GetSize() + (2,30) # padding for notebook
1220
+ size = plug.GetSize() + (2,30) # padding for notebook
1228
1221
  nb = AuiNotebook(self, name=title)
1229
1222
  nb.AddPage(plug, caption)
1230
1223
  self._mgr.AddPane(nb, aui.AuiPaneInfo()
@@ -1246,11 +1239,11 @@ class Frame(mwx.Frame):
1246
1239
  self.update_pane(name, **props)
1247
1240
  self.show_pane(name, show)
1248
1241
 
1249
- ## Create a menu
1242
+ ## Create a menu.
1250
1243
  plug.__Menu_item = None
1251
1244
 
1252
- if not hasattr(module, 'ID_'): # give a unique index to the module
1253
- global __plug_ID__ # cache ID *not* in [ID_LOWEST(4999):ID_HIGHEST(5999)]
1245
+ if not hasattr(module, 'ID_'): # give a unique index to the module
1246
+ global __plug_ID__ # cache ID *not* in [ID_LOWEST(4999):ID_HIGHEST(5999)]
1254
1247
  try:
1255
1248
  __plug_ID__
1256
1249
  except NameError:
@@ -1322,11 +1315,6 @@ class Frame(mwx.Frame):
1322
1315
  traceback.print_exc()
1323
1316
  print("- Failed to save session of", plug)
1324
1317
  self.load_plug(plug.__module__, force=1, session=session)
1325
-
1326
- ## Update shell.target --> new plug
1327
- for shell in self.shellframe.get_all_shells():
1328
- if shell.target is plug:
1329
- shell.handler('shell_activated', shell)
1330
1318
 
1331
1319
  def inspect_plug(self, name):
1332
1320
  """Dive into the process to inspect plugs in the shell.
@@ -1339,10 +1327,11 @@ class Frame(mwx.Frame):
1339
1327
 
1340
1328
  @shell.handler.bind("shell_activated")
1341
1329
  def init(shell):
1330
+ """Called when the plug shell is activated."""
1342
1331
  nonlocal plug
1343
1332
  _plug = self.get_plug(name)
1344
1333
  if _plug is not plug:
1345
- shell.target = _plug or self # reset for loaded/unloaded plug
1334
+ shell.target = _plug or self # Reset the target to the reloaded plug.
1346
1335
  plug = _plug
1347
1336
  init(shell)
1348
1337
  self.shellframe.Show()
@@ -1459,7 +1448,7 @@ class Frame(mwx.Frame):
1459
1448
  return frames
1460
1449
 
1461
1450
  ## --------------------------------
1462
- ## load/save frames and attributes
1451
+ ## load/save frames and attributes
1463
1452
  ## --------------------------------
1464
1453
 
1465
1454
  @classmethod
@@ -1744,14 +1733,18 @@ class Frame(mwx.Frame):
1744
1733
  self._mgr.Update()
1745
1734
  self.menubar.reset()
1746
1735
 
1747
- dirname_ = os.path.dirname(i.name)
1748
- if dirname_:
1749
- os.chdir(dirname_)
1750
-
1751
1736
  ## Reposition the window if it is not on the desktop.
1752
1737
  if wx.Display.GetFromWindow(self) == -1:
1753
1738
  self.Position = (0, 0)
1754
1739
 
1740
+ ## LoadPerspective => 表示状態の不整合.手動でイベントを発生させる.
1741
+ for pane in self._mgr.GetAllPanes():
1742
+ if pane.IsShown():
1743
+ win = pane.window
1744
+ if isinstance(win, aui.AuiNotebook):
1745
+ win = win.CurrentPage
1746
+ win.handler('page_shown', win)
1747
+
1755
1748
  self.message("\b done.")
1756
1749
 
1757
1750
  def save_session_as(self):
@@ -1799,8 +1792,8 @@ class Frame(mwx.Frame):
1799
1792
  o.write(f"self.{view.Name}.unit = {view.unit:g}\n")
1800
1793
  o.write(f"self.load_frame({paths!r}, self.{view.Name})\n")
1801
1794
  try:
1802
- index = paths.index(view.frame.pathname)
1803
- o.write(f"self.{view.Name}.select({index})\n")
1795
+ if view.frame.pathname in paths:
1796
+ o.write(f"self.{view.Name}.select({view.frame.name!r})\n")
1804
1797
  except Exception:
1805
1798
  pass
1806
1799
 
mwx/matplot2.py CHANGED
@@ -11,7 +11,7 @@ from matplotlib.figure import Figure
11
11
  import numpy as np
12
12
 
13
13
  from . import framework as mwx
14
- from .framework import hotkey, regulate_key, pack, Menu, FSM
14
+ from .framework import hotkey, regulate_key, postcall, pack, Menu, FSM
15
15
 
16
16
 
17
17
  ## state constants
@@ -178,8 +178,6 @@ class MatplotPanel(wx.Panel):
178
178
  'axes_enter' : [ None, ],
179
179
  'axes_leave' : [ None, ],
180
180
  'home pressed' : [ None, self.OnHomePosition ],
181
- 'left pressed' : [ None, self.OnBackPosition ],
182
- 'right pressed' : [ None, self.OnForwardPosition ],
183
181
  'Xbutton1 pressed' : [ None, self.OnBackPosition ],
184
182
  'Xbutton2 pressed' : [ None, self.OnForwardPosition ],
185
183
  },
@@ -314,6 +312,8 @@ class MatplotPanel(wx.Panel):
314
312
  self.cursor = Cursor(self.axes, useblit=True, color='grey', linewidth=1)
315
313
  self.cursor.visible = 1
316
314
 
315
+ ## Note: To avoid a wxAssertionError when running in a thread.
316
+ @postcall
317
317
  def draw(self, art=None):
318
318
  """Draw plots.
319
319
  Call each time the drawing should be updated.
@@ -345,22 +345,22 @@ class MatplotPanel(wx.Panel):
345
345
 
346
346
  xbound = property(
347
347
  lambda self: np.array(self.axes.get_xbound()),
348
- lambda self,v: self.axes.set_xbound(v),
348
+ lambda self, v: self.axes.set_xbound(v),
349
349
  doc="X-axis numerical bounds where lowerBound < upperBound)")
350
350
 
351
351
  ybound = property(
352
352
  lambda self: np.array(self.axes.get_ybound()),
353
- lambda self,v: self.axes.set_ybound(v),
353
+ lambda self, v: self.axes.set_ybound(v),
354
354
  doc="Y-axis numerical bounds where lowerBound < upperBound)")
355
355
 
356
356
  xlim = property(
357
357
  lambda self: np.array(self.axes.get_xlim()),
358
- lambda self,v: self.axes.set_xlim(v),
358
+ lambda self, v: self.axes.set_xlim(v),
359
359
  doc="X-axis range [left, right]")
360
360
 
361
361
  ylim = property(
362
362
  lambda self: np.array(self.axes.get_ylim()),
363
- lambda self,v: self.axes.set_ylim(v),
363
+ lambda self, v: self.axes.set_ylim(v),
364
364
  doc="Y-axis range [bottom, top]")
365
365
 
366
366
  @property
@@ -418,7 +418,7 @@ class MatplotPanel(wx.Panel):
418
418
  wx.UIActionSimulator().KeyUp(wx.WXK_ESCAPE)
419
419
 
420
420
  ## --------------------------------
421
- ## External I/O file and clipboard
421
+ ## External I/O file and clipboard
422
422
  ## --------------------------------
423
423
 
424
424
  def copy_to_clipboard(self):
@@ -561,7 +561,7 @@ class MatplotPanel(wx.Panel):
561
561
  ## Overwrite evt.key with modifiers.
562
562
  key = self.__key
563
563
  if evt.button in (1,2,3):
564
- key += 'LMR'[evt.button-1] #{1:L, 2:M, 3:R}
564
+ key += 'LMR'[evt.button-1] # {1:L, 2:M, 3:R}
565
565
  evt.key = key + 'button'
566
566
  elif evt.button in ('up', 'down'):
567
567
  key += 'wheel{}'.format(evt.button) # wheel[up|down]
@@ -621,10 +621,11 @@ class MatplotPanel(wx.Panel):
621
621
  self.p_event = None
622
622
 
623
623
  ## --------------------------------
624
- ## Pan/Zoom actions
624
+ ## Pan/Zoom actions
625
625
  ## --------------------------------
626
626
 
627
- ZOOM_RATIO = 10**0.2
627
+ ZOOM_RATIO = 10 ** 0.2
628
+ ZOOM_LIMIT = 0.1 # logical limit <= epsilon
628
629
 
629
630
  def OnDraw(self, evt):
630
631
  """Called before canvas.draw."""
@@ -663,7 +664,7 @@ class MatplotPanel(wx.Panel):
663
664
  if c is None:
664
665
  c = (lim[1] + lim[0]) / 2
665
666
  y = c - M * (c - lim)
666
- if abs(y[1] - y[0]) > 0.1 or M > 1:
667
+ if abs(y[1] - y[0]) > self.ZOOM_LIMIT:
667
668
  return y
668
669
 
669
670
  def OnZoom(self, evt):