mwxlib 0.99.0__py3-none-any.whl → 1.7.13__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.
mwx/graphman.py CHANGED
@@ -1,74 +1,59 @@
1
1
  #! python3
2
2
  """Graph manager.
3
3
  """
4
+ from contextlib import contextmanager
5
+ from datetime import datetime
4
6
  from functools import wraps
5
7
  from importlib import reload, import_module
6
- from contextlib import contextmanager
7
- from pprint import pformat
8
8
  from bdb import BdbQuit
9
- import subprocess
10
9
  import threading
11
10
  import traceback
12
11
  import inspect
12
+ import types
13
13
  import sys
14
14
  import os
15
15
  import platform
16
- import re
16
+ import json
17
17
  import wx
18
18
  from wx import aui
19
+ from wx import stc
20
+
21
+ import numpy as np
22
+ from numpy import nan, inf # noqa # necessary to eval
19
23
 
20
24
  from matplotlib import cm
21
25
  from matplotlib import colors
22
- ## from matplotlib import pyplot as plt
23
- import numpy as np
24
26
  from PIL import Image
25
27
  from PIL.TiffImagePlugin import TiffImageFile
26
28
 
27
29
  from . import framework as mwx
28
- from .utilus import ignore, warn
29
30
  from .utilus import funcall as _F
31
+ from .utilus import ignore, warn, fix_fnchars
30
32
  from .controls import KnobCtrlPanel, Icon
31
33
  from .framework import CtrlInterface, AuiNotebook, Menu, FSM
32
-
33
- from .matplot2 import MatplotPanel # noqa
34
34
  from .matplot2g import GraphPlot
35
- from .matplot2lg import LinePlot # noqa
36
- from .matplot2lg import LineProfile # noqa
37
35
  from .matplot2lg import Histogram
38
36
 
39
37
 
40
- def split_paths(obj):
41
- """Split obj path into dirname and basename.
42
- The object can be module name:str, module, or class.
43
- """
44
- if hasattr(obj, '__file__'): #<class 'module'>
45
- obj = obj.__file__
46
- elif isinstance(obj, type): #<class 'type'>
47
- obj = inspect.getsourcefile(obj)
48
- if obj.endswith(".py"):
49
- obj, _ = os.path.splitext(obj)
50
- return os.path.split(obj)
51
-
52
-
53
38
  class Thread:
54
39
  """Thread manager for graphman.Layer
55
40
 
56
41
  The worker:thread runs the given target.
57
42
 
58
43
  Attributes:
59
- target : A target method of the Layer.
60
- result : A variable that retains the last retval of f.
61
- worker : Reference of the worker thread.
62
- owner : Reference of the handler owner (was typ. f.__self__).
63
- If None, the thread_event is handled by its own handler.
64
- event : A common event flag to interrupt the process.
44
+ target: A target method of the Layer.
45
+ result: A variable that retains the last retval of f.
46
+ worker: Reference of the worker thread.
47
+ owner: Reference of the handler owner (was typ. f.__self__).
48
+ If None, the thread_event is handled by its own handler.
49
+ event: A common event flag to interrupt the process.
65
50
 
66
51
  There are two flags to check the thread status:
67
52
 
68
- - active : A flag of being kept going.
69
- Check this to see the worker is running and intended being kept going.
70
- - running : A flag of being running now.
71
- Watch this to verify the worker is alive after it has been inactivated.
53
+ - active: A flag of being kept going.
54
+ Check this to see the worker is running and intended being kept going.
55
+ - running: A flag of being running now.
56
+ Watch this to verify the worker is alive after it has been inactivated.
72
57
 
73
58
  The event object can be used to suspend/resume the thread:
74
59
 
@@ -84,7 +69,7 @@ class Thread:
84
69
  if self.worker:
85
70
  return self.worker.is_alive()
86
71
  return False
87
-
72
+
88
73
  def __init__(self, owner=None):
89
74
  self.owner = owner
90
75
  self.worker = None
@@ -98,26 +83,22 @@ class Thread:
98
83
  except AttributeError:
99
84
  self.handler = FSM({ # DNA<Thread>
100
85
  None : {
101
- 'thread_begin' : [ None ], # begin processing
102
- 'thread_end' : [ None ], # end processing
103
- 'thread_quit' : [ None ], # terminated by user
104
- 'thread_error' : [ None ], # failed in error
86
+ 'thread_begin' : [ None ],
87
+ 'thread_end' : [ None ],
105
88
  },
106
89
  })
107
-
90
+
108
91
  def __del__(self):
109
92
  if self.active:
110
93
  self.Stop()
111
-
94
+
112
95
  @contextmanager
113
96
  def entry(self):
114
97
  """Exclusive reentrant lock manager.
115
98
  Allows only this worker (but no other thread) to enter.
116
99
  """
117
100
  frame = inspect.currentframe().f_back.f_back
118
- filename = frame.f_code.co_filename
119
101
  name = frame.f_code.co_name
120
- fname, _ = os.path.splitext(os.path.basename(filename))
121
102
 
122
103
  ## Other threads are not allowed to enter.
123
104
  ct = threading.current_thread()
@@ -125,75 +106,89 @@ class Thread:
125
106
 
126
107
  ## The thread must be activated to enter.
127
108
  ## 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)
136
-
109
+ yield self
110
+
111
+ def enters(self, f):
112
+ """Decorator to add a one-time handler for the enter event.
113
+ The specified function will be called from the main thread.
114
+ """
115
+ return self.handler.binds('thread_begin', _F(f))
116
+
117
+ def exits(self, f):
118
+ """Decorator to add a one-time handler for the exit event.
119
+ The specified function will be called from the main thread.
120
+ """
121
+ return self.handler.binds('thread_end', _F(f))
122
+
137
123
  def wraps(self, f, *args, **kwargs):
138
- """Decorator of thread starter function."""
124
+ """Decorator for a function that starts a new thread."""
139
125
  @wraps(f)
140
126
  def _f(*v, **kw):
141
127
  return self.Start(f, *v, *args, **kw, **kwargs)
142
128
  return _f
143
-
129
+
144
130
  def check(self, timeout=None):
145
131
  """Check the thread event flags."""
146
132
  if not self.running:
147
133
  return None
148
- if not self.event.wait(timeout): # wait until set in time
134
+ if not self.event.wait(timeout): # wait until set in time
149
135
  raise KeyboardInterrupt("timeout")
150
136
  if not self.active:
151
137
  raise KeyboardInterrupt("terminated by user")
152
138
  return True
153
-
154
- def pause(self, msg="Pausing..."):
139
+
140
+ def pause(self, msg=None, caption=wx.MessageBoxCaptionStr):
155
141
  """Pause the thread.
142
+ Confirm whether to terminate the thread.
156
143
 
157
- Use ``check`` method where you want to pause.
144
+ Returns:
145
+ True: [OK] if terminating.
146
+ False: [CANCEL] otherwise.
158
147
 
159
148
  Note:
149
+ Use ``check`` method where you want to pause.
160
150
  Even after the message dialog is displayed, the thread
161
151
  does not suspend until check (or event.wait) is called.
162
152
  """
163
153
  if not self.running:
164
154
  return None
155
+ if not msg:
156
+ msg = ("The thread is running.\n\n"
157
+ "Do you want to terminate the thread?")
165
158
  try:
166
- self.event.clear() # suspend
167
- if wx.MessageBox(msg + "\n\n"
168
- "Press [OK] to continue.\n"
169
- "Press [CANCEL] to terminate the process.",
170
- style=wx.OK|wx.CANCEL|wx.ICON_WARNING) != wx.OK:
159
+ self.event.clear() # suspend
160
+ if wx.MessageBox( # Confirm closing the thread.
161
+ msg, caption,
162
+ style=wx.OK|wx.CANCEL|wx.ICON_WARNING) == wx.OK:
171
163
  self.Stop()
172
- return False
173
- return True
164
+ return True
165
+ return False
174
166
  finally:
175
- self.event.set() # resume
176
-
167
+ self.event.set() # resume
168
+
177
169
  def Start(self, f, *args, **kwargs):
178
- """Start the thread to run the specified function."""
170
+ """Start the thread to run the specified function.
171
+ """
179
172
  @wraps(f)
180
173
  def _f(*v, **kw):
181
174
  try:
182
- self.handler('thread_begin', self)
175
+ wx.CallAfter(self.handler, 'thread_begin', self)
183
176
  self.result = f(*v, **kw)
184
177
  except BdbQuit:
185
178
  pass
186
179
  except KeyboardInterrupt as e:
187
- print("- Thread:execution stopped:", e)
188
- except AssertionError as e:
189
- print("- Thread:execution failed:", e)
180
+ print("- Thread terminated by user:", e)
190
181
  except Exception as e:
182
+ print("- Thread failed in error:", e)
191
183
  traceback.print_exc()
192
- print("- Thread:exception:", e)
193
- self.handler('thread_error', self)
184
+ tbstr = traceback.format_tb(e.__traceback__)
185
+ wx.CallAfter(wx.MessageBox,
186
+ f"{e}\n\n" + tbstr[-1] + f"{type(e).__name__}: {e}",
187
+ f"Error in the thread running {f.__name__!r}\n\n",
188
+ style=wx.ICON_ERROR)
194
189
  finally:
195
190
  self.active = 0
196
- self.handler('thread_end', self)
191
+ wx.CallAfter(self.handler, 'thread_end', self)
197
192
 
198
193
  if self.running:
199
194
  wx.MessageBox("The thread is running (Press [C-g] to quit).",
@@ -207,81 +202,86 @@ class Thread:
207
202
  args=args, kwargs=kwargs, daemon=True)
208
203
  self.worker.start()
209
204
  self.event.set()
210
-
205
+
211
206
  def Stop(self):
212
207
  """Stop the thread.
213
208
 
214
- Use ``check`` method where you want to quit.
209
+ Note:
210
+ Use ``check`` method where you want to stop.
211
+ Even after the message dialog is displayed, the thread
212
+ does not suspend until check (or event.wait) is called.
215
213
  """
216
214
  def _stop():
217
215
  with wx.BusyInfo("One moment please, "
218
216
  "waiting for threads to die..."):
219
- self.handler('thread_quit', self)
220
217
  self.worker.join(1)
221
218
  if self.running:
222
219
  self.active = 0
223
- wx.CallAfter(_stop) # main-thread で終了させる
220
+ wx.CallAfter(_stop) # main thread で終了させる
224
221
 
225
222
 
226
223
  class LayerInterface(CtrlInterface):
227
- """Graphman.Layer interface mixin
224
+ """Graphman.Layer interface mixin.
228
225
 
229
226
  The layer properties can be switched by the following classvars::
230
227
 
231
- menukey : menu item key:str in parent menubar
232
- category : title of notebook holder, otherwise None for single pane
233
- caption : flag to set the pane caption to be visible
234
- a string can also be specified (default is __module__)
235
- dockable : flag to set the pane to be dockable
236
- type: bool or dock:int (1:t, 2:r, 3:b, 4:l, 5:c)
237
- reloadable : flag to set the Layer to be reloadable
238
- unloadable : flag to set the Layer to be unloadable
228
+ menukey: menu item key:str in parent menubar
229
+ category: title of notebook holder, otherwise None for single pane
230
+ caption: flag to set the pane caption to be visible
231
+ a string can also be specified (default is __module__)
232
+ dockable: flag to set the pane to be dockable
233
+ type: bool or dock:int (1:t, 2:r, 3:b, 4:l, 5:c)
234
+ reloadable: flag to set the Layer to be reloadable
235
+ unloadable: flag to set the Layer to be unloadable
239
236
 
240
237
  Note:
241
238
  parent <Frame> is not always equal to Parent when floating.
242
239
  Parent type can be <Frame>, <AuiFloatingFrame>, or <AuiNotebook>.
243
240
  """
244
- MENU = "Plugins" # default menu for Plugins
241
+ MENU = "Plugins" # default menu for Plugins
245
242
  menukey = "Plugins/"
246
243
  caption = True
247
244
  category = None
248
245
  dockable = True
249
- editable = True # deprecated
250
246
  reloadable = True
251
247
  unloadable = True
252
-
248
+
253
249
  graph = property(lambda self: self.parent.graph)
254
250
  output = property(lambda self: self.parent.output)
255
251
  histogram = property(lambda self: self.parent.histogram)
256
252
  selected_view = property(lambda self: self.parent.selected_view)
257
-
253
+
258
254
  message = property(lambda self: self.parent.message)
259
-
255
+
260
256
  ## thread_type = Thread
261
257
  thread = None
262
-
258
+
259
+ ## layout helper function (internal use only)
260
+ pack = mwx.pack
261
+
262
+ ## funcall = interactive_call (internal use only)
263
+ funcall = staticmethod(_F)
264
+
263
265
  ## for debug (internal use only)
264
266
  pane = property(lambda self: self.parent.get_pane(self))
265
-
267
+
266
268
  @property
267
269
  def Arts(self):
268
- """List of arts <matplotlib.artist.Artist>."""
270
+ """List of artists <matplotlib.artist.Artist>."""
269
271
  return self.__artists
270
-
272
+
271
273
  @Arts.setter
272
274
  def Arts(self, arts):
273
- for art in self.__artists[:]:
274
- if art not in arts:
275
- art.remove()
276
- self.__artists.remove(art)
277
- self.__artists = arts
278
-
275
+ for art in (set(self.__artists) - set(arts)):
276
+ art.remove()
277
+ self.__artists = list(arts)
278
+
279
279
  @Arts.deleter
280
280
  def Arts(self):
281
281
  for art in self.__artists:
282
282
  art.remove()
283
283
  self.__artists = []
284
-
284
+
285
285
  def attach_artists(self, axes, *artists):
286
286
  """Attach artists (e.g., patches) to the given axes."""
287
287
  for art in artists:
@@ -289,17 +289,8 @@ class LayerInterface(CtrlInterface):
289
289
  art.remove()
290
290
  art._transformSet = False
291
291
  axes.add_artist(art)
292
- if art not in self.__artists:
293
- self.__artists.append(art)
294
-
295
- def detach_artists(self, *artists):
296
- """Detach artists (e.g., patches) from their axes."""
297
- for art in artists:
298
- if art.axes:
299
- art.remove()
300
- art._transformSet = False
301
- self.__artists.remove(art)
302
-
292
+ self.Arts += [art for art in artists if art not in self.Arts]
293
+
303
294
  def __init__(self, parent, session=None):
304
295
  if hasattr(self, 'handler'):
305
296
  warn(f"Duplicate iniheritance of CtrlInterface by {self}.")
@@ -315,27 +306,28 @@ class LayerInterface(CtrlInterface):
315
306
  except AttributeError:
316
307
  self.parameters = None
317
308
 
318
- def copy_params(**kwargs):
319
- if self.parameters:
320
- return self.copy_to_clipboard(**kwargs)
309
+ def copy_params(evt, checked_only=False):
310
+ if isinstance(evt.EventObject, (wx.TextEntry, stc.StyledTextCtrl)):
311
+ evt.Skip()
312
+ elif self.parameters:
313
+ self.copy_to_clipboard(checked_only)
321
314
 
322
- def paste_params(**kwargs):
323
- if self.parameters:
324
- return self.paste_from_clipboard(**kwargs)
315
+ def paste_params(evt, checked_only=False):
316
+ if isinstance(evt.EventObject, (wx.TextEntry, stc.StyledTextCtrl)):
317
+ evt.Skip()
318
+ elif self.parameters:
319
+ self.paste_from_clipboard(checked_only)
325
320
 
326
- def reset_params(**kwargs):
321
+ def reset_params(evt, checked_only=False):
327
322
  self.Draw(None)
328
323
  if self.parameters:
329
- return self.set_params(**kwargs)
324
+ self.reset_params(checked_only)
330
325
 
331
326
  self.handler.append({ # DNA<Layer>
332
327
  None : {
333
- 'thread_begin' : [ None ], # begin processing
334
- 'thread_end' : [ None ], # end processing
335
- 'thread_quit' : [ None ], # terminated by user
336
- 'thread_error' : [ None ], # failed in error
337
- 'page_shown' : [ None, _F(self.Draw, True) ],
338
- 'page_closed' : [ None, _F(self.Draw, False) ],
328
+ 'page_shown' : [ None, _F(self.Draw, show=True) ],
329
+ 'page_closed' : [ None, _F(self.Draw, show=False) ],
330
+ 'page_hidden' : [ None, _F(self.Draw, show=False) ],
339
331
  },
340
332
  0 : {
341
333
  'C-c pressed' : (0, _F(copy_params)),
@@ -348,43 +340,40 @@ class LayerInterface(CtrlInterface):
348
340
  })
349
341
  self.menu = [
350
342
  (wx.ID_COPY, "&Copy params\t(C-c)", "Copy params",
351
- lambda v: copy_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
343
+ lambda v: copy_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
352
344
  lambda v: v.Enable(bool(self.parameters))),
353
345
 
354
346
  (wx.ID_PASTE, "&Paste params\t(C-v)", "Read params",
355
- lambda v: paste_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
347
+ lambda v: paste_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
356
348
  lambda v: v.Enable(bool(self.parameters))),
357
349
  (),
358
350
  (wx.ID_RESET, "&Reset params\t(C-n)", "Reset params", Icon('-'),
359
- lambda v: reset_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
351
+ lambda v: reset_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
360
352
  lambda v: v.Enable(bool(self.parameters))),
361
353
  (),
362
- (wx.ID_EDIT, "&Edit module", "Edit module", Icon('pen'),
363
- lambda v: self.parent.edit_plug(self.__module__),
364
- lambda v: v.Enable(self.editable)),
365
-
366
- (mwx.ID_(201), "&Reload module", "Reload module", Icon('load'),
367
- lambda v: self.parent.reload_plug(self.__module__),
354
+ (mwx.ID_(201), "&Reload module", "Reload", Icon('load'),
355
+ lambda v: self.parent.reload_plug(self),
368
356
  lambda v: v.Enable(self.reloadable
369
357
  and not (self.thread and self.thread.active))),
370
358
 
371
- (mwx.ID_(202), "&Unload module", "Unload module", Icon('delete'),
372
- lambda v: self.parent.unload_plug(self.__module__),
359
+ (mwx.ID_(202), "&Unload module", "Unload", Icon('delete'),
360
+ lambda v: self.parent.unload_plug(self),
373
361
  lambda v: v.Enable(self.unloadable
374
362
  and not (self.thread and self.thread.active))),
375
363
  (),
376
364
  (mwx.ID_(203), "&Dive into {!r}".format(self.__module__), "dive", Icon('core'),
377
- lambda v: self.parent.inspect_plug(self.__module__)),
365
+ lambda v: self.parent.inspect_plug(self)),
378
366
  ]
379
367
  self.Bind(wx.EVT_CONTEXT_MENU,
380
368
  lambda v: Menu.Popup(self, self.menu))
381
369
 
382
370
  self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
371
+ self.Bind(wx.EVT_SHOW, self.OnShow)
383
372
 
384
373
  try:
385
374
  self.Init()
386
375
  except Exception as e:
387
- traceback.print_exc()
376
+ traceback.print_exc() # Failed to initialize the plug.
388
377
  if parent:
389
378
  bmp = wx.StaticBitmap(self, bitmap=Icon('!!!'))
390
379
  txt = wx.StaticText(self, label="Exception")
@@ -394,59 +383,68 @@ class LayerInterface(CtrlInterface):
394
383
  if session:
395
384
  self.load_session(session)
396
385
  except Exception:
397
- traceback.print_exc()
398
- print("- Failed to load session of", self)
399
-
386
+ traceback.print_exc() # Failed to load the plug session.
387
+
400
388
  def Init(self):
401
389
  """Initialize layout before load_session (to be overridden)."""
402
390
  pass
403
-
391
+
404
392
  def load_session(self, session):
405
393
  """Restore settings from a session file (to be overridden)."""
406
394
  if 'params' in session:
407
395
  self.parameters = session['params']
408
-
396
+
409
397
  def save_session(self, session):
410
398
  """Save settings in a session file (to be overridden)."""
411
399
  if self.parameters:
412
400
  session['params'] = self.parameters
413
-
401
+
414
402
  def OnDestroy(self, evt):
415
403
  if evt.EventObject is self:
416
404
  if self.thread and self.thread.active:
417
- self.thread.active = 0
405
+ # self.thread.active = 0
418
406
  self.thread.Stop()
419
407
  del self.Arts
420
408
  evt.Skip()
421
-
409
+
410
+ def OnShow(self, evt):
411
+ if not self:
412
+ return
413
+ if isinstance(self.Parent, aui.AuiNotebook):
414
+ if evt.IsShown():
415
+ self.handler('page_shown', self)
416
+ else:
417
+ self.handler('page_hidden', self)
418
+ evt.Skip()
419
+
422
420
  Shown = property(
423
421
  lambda self: self.IsShown(),
424
- lambda self,v: self.Show(v))
425
-
422
+ lambda self, v: self.Show(v))
423
+
426
424
  def IsShown(self):
427
- """Returns True if the window is physically visible on the screen.
425
+ """Return True if the window is physically visible on the screen.
428
426
 
429
427
  (override) Equivalent to ``IsShownOnScreen``.
430
428
  Note: The instance could be a page within a notebook.
431
429
  """
432
- ## return self.pane.IsShown()
430
+ # return self.pane.IsShown()
433
431
  return self.IsShownOnScreen()
434
-
435
- def Show(self, show=True):
432
+
433
+ def Show(self, show=True, interactive=False):
436
434
  """Shows or hides the window.
437
435
 
438
436
  (override) Show associated pane window.
439
437
  Note: This might be called from a thread.
440
438
  """
441
- wx.CallAfter(self.parent.show_pane, self, show)
442
-
439
+ wx.CallAfter(self.parent.show_pane, self, show, interactive) # Use main thread.
440
+
443
441
  Drawn = property(
444
442
  lambda self: self.IsDrawn(),
445
- lambda self,v: self.Draw(v))
446
-
443
+ lambda self, v: self.Draw(v))
444
+
447
445
  def IsDrawn(self):
448
446
  return any(art.get_visible() for art in self.Arts)
449
-
447
+
450
448
  def Draw(self, show=None):
451
449
  """Draw artists.
452
450
  If show is None:default, draw only when the pane is visible.
@@ -476,12 +474,33 @@ class Layer(LayerInterface, KnobCtrlPanel):
476
474
  LayerInterface.__init__(self, parent, session)
477
475
 
478
476
 
477
+ def _register__dummy_plug__(cls, module):
478
+ if issubclass(cls, LayerInterface):
479
+ # warn(f"Duplicate iniheritance of LayerInterface by {cls}.")
480
+ module.Plugin = cls
481
+ return cls
482
+
483
+ class _Plugin(cls, LayerInterface):
484
+ def __init__(self, parent, session=None, **kwargs):
485
+ cls.__init__(self, parent, **kwargs)
486
+ LayerInterface.__init__(self, parent, session)
487
+ Show = LayerInterface.Show
488
+ IsShown = LayerInterface.IsShown
489
+
490
+ _Plugin.__module__ = module.__name__
491
+ _Plugin.__qualname__ = cls.__qualname__ + "~"
492
+ _Plugin.__name__ = cls.__name__ + "~"
493
+ _Plugin.__doc__ = cls.__doc__
494
+ module.Plugin = _Plugin
495
+ return _Plugin
496
+
497
+
479
498
  class Graph(GraphPlot):
480
499
  """GraphPlot (override) to better make use for graph manager
481
500
 
482
501
  Attributes:
483
- parent : Parent window (usually mainframe)
484
- loader : mainframe
502
+ parent: Parent window (usually mainframe)
503
+ loader: mainframe
485
504
  """
486
505
  def __init__(self, parent, loader=None, **kwargs):
487
506
  GraphPlot.__init__(self, parent, **kwargs)
@@ -499,76 +518,64 @@ class Graph(GraphPlot):
499
518
  'f5 pressed' : [ None, _F(self.refresh) ],
500
519
  },
501
520
  })
502
- ## ドロップターゲットを許可する
521
+ ## ドロップターゲットを許可する.
503
522
  self.SetDropTarget(MyFileDropLoader(self, self.loader))
504
-
523
+
505
524
  def refresh(self):
506
525
  if self.frame:
507
526
  self.frame.update_buffer()
508
527
  self.draw()
509
-
528
+
510
529
  def toggle_infobar(self):
511
530
  """Toggle infobar (frame.annotation)."""
512
531
  if self.infobar.IsShown():
513
532
  self.infobar.Dismiss()
514
533
  elif self.frame:
515
534
  self.infobar.ShowMessage(self.frame.annotation)
516
-
535
+
517
536
  def update_infobar(self, frame):
518
537
  """Show infobar (frame.annotation)."""
519
538
  if self.infobar.IsShown():
520
539
  self.infobar.ShowMessage(frame.annotation)
521
-
522
- def get_markups_visible(self):
523
- return self.marked.get_visible()
524
-
525
- def set_markups_visible(self, v):
526
- self.selected.set_visible(v)
527
- self.marked.set_visible(v)
528
- self.rected.set_visible(v)
529
- self.update_art_of_mark()
530
-
531
- def remove_markups(self):
532
- del self.Selector
533
- del self.Markers
534
- del self.Region
535
-
540
+
536
541
  def hide_layers(self):
537
- for name in self.parent.plugins:
538
- plug = self.parent.get_plug(name)
542
+ for plug in self.parent.get_all_plugs():
539
543
  for art in plug.Arts:
540
544
  art.set_visible(0)
541
- self.remove_markups()
545
+ del self.selector
546
+ del self.markers
547
+ del self.region
542
548
  self.draw()
543
549
 
544
550
 
545
551
  class MyFileDropLoader(wx.FileDropTarget):
546
- """File Drop interface
552
+ """File Drop interface.
547
553
 
548
554
  Args:
549
- target : target view to drop in, e.g. frame, graph, pane, etc.
550
- loader : mainframe
555
+ target: target view to drop in, e.g. frame, graph, pane, etc.
556
+ loader: mainframe
551
557
  """
552
558
  def __init__(self, target, loader):
553
559
  wx.FileDropTarget.__init__(self)
554
560
 
555
561
  self.view = target
556
562
  self.loader = loader
557
-
563
+
558
564
  def OnDropFiles(self, x, y, filenames):
559
- pos = self.view.ScreenPosition + (x,y)
565
+ pos = self.view.ScreenPosition + (x, y)
560
566
  paths = []
561
567
  for fn in filenames:
562
- name, ext = os.path.splitext(fn)
568
+ _name, ext = os.path.splitext(fn)
563
569
  if ext == '.py' or os.path.isdir(fn):
564
- self.loader.load_plug(fn, show=1, floating_pos=pos,
565
- force=wx.GetKeyState(wx.WXK_ALT))
570
+ self.loader.load_plug(fn, show=1,
571
+ floating_pos=pos,
572
+ force=wx.GetKeyState(wx.WXK_ALT))
566
573
  elif ext == '.jssn':
567
574
  self.loader.load_session(fn)
568
575
  elif ext == '.index':
569
576
  self.loader.load_index(fn, self.view)
570
577
  else:
571
- paths.append(fn) # image file just stacks to be loaded
578
+ paths.append(fn) # image file just stacks to be loaded
572
579
  if paths:
573
580
  self.loader.load_frame(paths, self.view)
574
581
  return True
@@ -586,24 +593,24 @@ class Frame(mwx.Frame):
586
593
  graph = property(lambda self: self.__graph)
587
594
  output = property(lambda self: self.__output)
588
595
  histogram = property(lambda self: self.__histgrm)
589
-
596
+
590
597
  selected_view = property(lambda self: self.__view)
591
-
598
+
592
599
  def select_view(self, view):
593
600
  self.__view = view
594
601
  self.set_title(view.frame)
595
-
602
+
596
603
  @property
597
604
  def graphic_windows(self):
598
605
  """Graphic windows list.
599
606
  [0] graph [1] output [2:] others(user-defined)
600
607
  """
601
608
  return self.__graphic_windows
602
-
609
+
603
610
  @property
604
611
  def graphic_windows_on_screen(self):
605
612
  return [w for w in self.__graphic_windows if w.IsShownOnScreen()]
606
-
613
+
607
614
  def __init__(self, *args, **kwargs):
608
615
  mwx.Frame.__init__(self, *args, **kwargs)
609
616
 
@@ -611,10 +618,10 @@ class Frame(mwx.Frame):
611
618
  self._mgr.SetManagedWindow(self)
612
619
  self._mgr.SetDockSizeConstraint(0.5, 0.5)
613
620
 
614
- self.__plugins = {} # modules in the order of load/save
621
+ self.__plugins = {} # modules in the order of load/save
615
622
 
616
- self.__graph = Graph(self, log=self.message, margin=None, size=(600,600))
617
- self.__output = Graph(self, log=self.message, margin=None, size=(600,600))
623
+ self.__graph = Graph(self, log=self.message, margin=None)
624
+ self.__output = Graph(self, log=self.message, margin=None)
618
625
 
619
626
  self.__histgrm = Histogram(self, log=self.message, margin=None, size=(130,65))
620
627
  self.__histgrm.attach(self.graph)
@@ -632,7 +639,7 @@ class Frame(mwx.Frame):
632
639
  self.histogram.Name = "histogram"
633
640
 
634
641
  self._mgr.AddPane(self.graph,
635
- aui.AuiPaneInfo().CenterPane().CloseButton(1)
642
+ aui.AuiPaneInfo().CenterPane()
636
643
  .Name("graph").Caption("graph").CaptionVisible(1))
637
644
 
638
645
  size = (200, 200)
@@ -664,7 +671,7 @@ class Frame(mwx.Frame):
664
671
  lambda v: v.Enable(self.__view.frame is not None)),
665
672
 
666
673
  (wx.ID_SAVEAS, "&Save as TIFFs", "Save buffers as a multi-page tiff", Icon('saveall'),
667
- lambda v: self.save_buffers_as_tiffs(),
674
+ lambda v: self.save_frames_as_tiff(),
668
675
  lambda v: v.Enable(self.__view.frame is not None)),
669
676
  (),
670
677
  ("Index", (
@@ -687,7 +694,7 @@ class Frame(mwx.Frame):
687
694
  lambda v: self.save_session_as()),
688
695
  )),
689
696
  (),
690
- ("Options", []), # reserved for optional app settings
697
+ ("Options", []), # reserved for optional app settings
691
698
  (),
692
699
  (mwx.ID_(13), "&Graph window\tF9", "Show graph window", wx.ITEM_CHECK,
693
700
  lambda v: self.show_pane(self.graph, v.IsChecked()),
@@ -705,44 +712,37 @@ class Frame(mwx.Frame):
705
712
  (wx.ID_PASTE, "&Paste\t(C-v)", "Paste buffer from clipboard", Icon('paste'),
706
713
  lambda v: self.__view.read_buffer_from_clipboard()),
707
714
  (),
708
- (mwx.ID_(21), "Toggle &Markers", "Show/Hide markups", wx.ITEM_CHECK, Icon('+'),
709
- lambda v: self.__view.set_markups_visible(v.IsChecked()),
710
- lambda v: v.Check(self.__view.get_markups_visible())),
711
-
712
- (mwx.ID_(22), "&Remove Markers", "Remove markups", Icon('-'),
713
- lambda v: self.__view.remove_markups()),
714
- (),
715
- (mwx.ID_(23), "Hide all &Layers", "Hide all layers", Icon('xr'),
715
+ (mwx.ID_(23), "Hide all &layers", "Hide all layers", Icon('xr'),
716
716
  lambda v: self.__view.hide_layers()),
717
717
  (),
718
- (mwx.ID_(24), "&Histogram\tCtrl-h", "Show Histogram window", wx.ITEM_CHECK,
718
+ (mwx.ID_(24), "&Histogram\tCtrl-h", "Show histogram window", wx.ITEM_CHECK,
719
719
  lambda v: self.show_pane(self.histogram, v.IsChecked()),
720
720
  lambda v: v.Check(self.histogram.IsShownOnScreen())),
721
721
 
722
- (mwx.ID_(25), "&Invert Color\t(C-i)", "Invert colormap", wx.ITEM_CHECK,
722
+ (mwx.ID_(25), "&Invert color\t(C-i)", "Invert colormap", wx.ITEM_CHECK,
723
723
  lambda v: self.__view.invert_cmap(),
724
- lambda v: v.Check(self.__view.get_cmap()[-2:] == "_r")),
724
+ lambda v: v.Check(self.__view.get_cmapstr()[-2:] == "_r")),
725
725
  ]
726
726
 
727
727
  def _cmenu(i, name):
728
728
  return (mwx.ID_(30 + i), "&" + name, name, wx.ITEM_CHECK,
729
- lambda v: self.__view.set_cmap(name),
730
- lambda v: v.Check(self.__view.get_cmap() == name
731
- or self.__view.get_cmap() == name+"_r"),
729
+ lambda v: self.__view.set_cmapstr(name),
730
+ lambda v: v.Check(self.__view.get_cmapstr() == name
731
+ or self.__view.get_cmapstr() == name+"_r"),
732
732
  )
733
733
  colours = [c for c in dir(cm) if c[-2:] != "_r"
734
734
  and isinstance(getattr(cm, c), colors.LinearSegmentedColormap)]
735
735
 
736
736
  self.menubar["Edit"] += [
737
737
  (),
738
- ## (mwx.ID_(26), "Default Color", "gray", wx.ITEM_CHECK,
739
- ## lambda v: self.__view.set_cmap('gray'),
740
- ## lambda v: v.Check(self.__view.get_cmap()[:4] == "gray")),
741
- ##
742
- ("Standard Colors",
738
+ # (mwx.ID_(26), "Default Color", "gray", wx.ITEM_CHECK,
739
+ # lambda v: self.__view.set_cmapstr('gray'),
740
+ # lambda v: v.Check(self.__view.get_cmapstr()[:4] == "gray")),
741
+ #
742
+ ("Standard colors",
743
743
  [_cmenu(i, c) for i, c in enumerate(colours) if c.islower()]),
744
744
 
745
- ("Other Colors",
745
+ ("Other colors",
746
746
  [_cmenu(i, c) for i, c in enumerate(colours) if not c.islower()]),
747
747
  ]
748
748
 
@@ -757,7 +757,8 @@ class Frame(mwx.Frame):
757
757
  self.menubar.reset()
758
758
 
759
759
  def show_frameview(frame):
760
- wx.CallAfter(self.show_pane, frame.parent) # Show graph / output
760
+ if not frame.parent.IsShown():
761
+ wx.CallAfter(self.show_pane, frame.parent)
761
762
 
762
763
  self.graph.handler.append({ # DNA<Graph:Frame>
763
764
  None : {
@@ -778,9 +779,9 @@ class Frame(mwx.Frame):
778
779
  },
779
780
  })
780
781
 
781
- ## Add main-menu to context-menu
782
- self.graph.menu += self.menubar["Edit"][2:7]
783
- self.output.menu += self.menubar["Edit"][2:7]
782
+ ## Add main-menu to context-menu.
783
+ self.graph.menu += self.menubar["Edit"][2:4]
784
+ self.output.menu += self.menubar["Edit"][2:4]
784
785
 
785
786
  self._mgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
786
787
 
@@ -796,10 +797,10 @@ class Frame(mwx.Frame):
796
797
  _display(self.graph, show)
797
798
  _display(self.output, show)
798
799
  evt.Skip()
799
- self.Bind(wx.EVT_MOVE_START, lambda v :on_move(v, show=0))
800
- self.Bind(wx.EVT_MOVE_END, lambda v :on_move(v, show=1))
800
+ self.Bind(wx.EVT_MOVE_START, lambda v: on_move(v, show=0))
801
+ self.Bind(wx.EVT_MOVE_END, lambda v: on_move(v, show=1))
801
802
 
802
- ## Custom Key Bindings
803
+ ## Custom Key Bindings.
803
804
  self.define_key('* C-g', self.Quit)
804
805
 
805
806
  @self.shellframe.define_key('* C-g')
@@ -807,36 +808,33 @@ class Frame(mwx.Frame):
807
808
  """Dispatch quit to the main Frame."""
808
809
  self.handler('C-g pressed', evt)
809
810
 
810
- ## Accepts DnD
811
+ ## Accepts DnD.
811
812
  self.SetDropTarget(MyFileDropLoader(self.graph, self))
812
-
813
- ## Script editor for plugins (external call)
814
- self.Editor = "notepad"
815
-
816
- sync_switch = True
817
-
813
+
814
+ SYNC_SWITCH = True
815
+
818
816
  def sync(self, a, b):
819
817
  """Synchronize b to a."""
820
- if (self.sync_switch
821
- and a.frame and b.frame
822
- and a.frame.unit == b.frame.unit
823
- and a.buffer.shape == b.buffer.shape):
824
- b.xlim = a.xlim
825
- b.ylim = a.ylim
826
- b.OnDraw(None)
827
- b.canvas.draw_idle()
828
-
818
+ if (self.SYNC_SWITCH
819
+ and a.frame and b.frame
820
+ and a.frame.unit == b.frame.unit
821
+ and a.buffer.shape == b.buffer.shape):
822
+ b.xlim = a.xlim
823
+ b.ylim = a.ylim
824
+ b.OnDraw(None)
825
+ b.canvas.draw_idle()
826
+
829
827
  def set_title(self, frame):
830
828
  ssn = os.path.basename(self.session_file or '--')
831
829
  ssn, _ = os.path.splitext(ssn)
832
830
  name = (frame.pathname or frame.name) if frame else ''
833
831
  self.SetTitle("{}@{} - [{}] {}".format(self.Name, platform.node(), ssn, name))
834
-
835
- def OnActivate(self, evt): #<wx._core.ActivateEvent>
832
+
833
+ def OnActivate(self, evt): #<wx._core.ActivateEvent>
836
834
  if self and evt.Active:
837
835
  self.set_title(self.selected_view.frame)
838
-
839
- def OnClose(self, evt): #<wx._core.CloseEvent>
836
+
837
+ def OnClose(self, evt): #<wx._core.CloseEvent>
840
838
  ssn = os.path.basename(self.session_file or '--')
841
839
  with wx.MessageDialog(None,
842
840
  "Do you want to save session before closing program?",
@@ -848,66 +846,69 @@ class Frame(mwx.Frame):
848
846
  elif ret == wx.ID_CANCEL:
849
847
  evt.Veto()
850
848
  return
851
- for name in self.plugins:
852
- plug = self.get_plug(name)
853
- if plug.thread and plug.thread.active:
854
- if wx.MessageBox( # Confirm thread close.
855
- "The thread is running.\n\n"
856
- "Enter [q]uit to exit before closing.\n"
857
- "Continue closing?",
858
- "Close {!r}".format(plug.Name),
859
- style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
860
- self.message("The close has been canceled.")
861
- evt.Veto()
862
- return
863
- self.Quit()
864
- break
865
- for frame in self.graph.all_frames:
866
- if frame.pathname is None:
867
- if wx.MessageBox( # Confirm close.
868
- "You are closing unsaved frame.\n\n"
869
- "Continue closing?",
870
- "Close {!r}".format(frame.name),
871
- style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
872
- self.message("The close has been canceled.")
873
- evt.Veto()
874
- return
875
- break
849
+ n = sum(bool(plug.thread and plug.thread.active) for plug in self.get_all_plugs())
850
+ if n:
851
+ s = 's' if n > 1 else ''
852
+ if wx.MessageBox( # Confirm closing the thread.
853
+ f"Currently running {n} thread{s}.\n\n"
854
+ "Continue closing?",
855
+ style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
856
+ self.message("The close has been canceled.")
857
+ evt.Veto()
858
+ return
859
+ self.Quit()
860
+ n = sum(frame.pathname is None for frame in self.graph.all_frames)
861
+ if n:
862
+ s = 's' if n > 1 else ''
863
+ if wx.MessageBox( # Confirm closing the frame.
864
+ f"You are closing {n} unsaved frame{s}.\n\n"
865
+ "Continue closing?",
866
+ style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
867
+ self.message("The close has been canceled.")
868
+ evt.Veto()
869
+ return
876
870
  evt.Skip()
877
-
871
+
878
872
  def Destroy(self):
879
- ## for name in list(self.plugins):
880
- ## self.unload_plug(name) # => plug.Destroy
881
873
  self._mgr.UnInit()
882
874
  return mwx.Frame.Destroy(self)
883
-
875
+
884
876
  ## --------------------------------
885
- ## pane window interface
877
+ ## pane window interface.
886
878
  ## --------------------------------
887
-
879
+
888
880
  def get_pane(self, name):
889
881
  """Get named pane or notebook pane.
890
882
 
891
883
  Args:
892
- name : str or plug object.
884
+ name: plug name or object.
893
885
  """
894
886
  plug = self.get_plug(name)
895
887
  if plug:
896
888
  name = plug.category or plug
897
889
  if name:
898
890
  return self._mgr.GetPane(name)
899
-
891
+
900
892
  def show_pane(self, name, show=True, interactive=False):
901
- """Show named pane or notebook pane."""
893
+ """Show named pane or notebook pane.
894
+
895
+ Args:
896
+ name: plug name or object.
897
+ show: Show or hide the pane window.
898
+ interactive: If True, modifier keys can be used to reset or reload
899
+ the plugin when showing the pane:
900
+ - [S-menu] Reset floating position.
901
+ - [M-S-menu] Reload plugin.
902
+ """
902
903
  pane = self.get_pane(name)
903
904
  if not pane.IsOk():
904
905
  return
905
906
 
906
907
  ## Set the graph and output window sizes to half & half.
907
- ## ドッキング時に再計算される
908
+ ## ドッキング時に再計算される.
908
909
  if name == "output" or name is self.output:
909
910
  w, h = self.graph.GetClientSize()
910
- pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
911
+ pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
911
912
 
912
913
  ## Force Layer windows to show.
913
914
  if interactive:
@@ -923,91 +924,90 @@ class Frame(mwx.Frame):
923
924
  pane.Float()
924
925
  show = True
925
926
 
926
- ## Note: We need to distinguish cases whether:
927
- ## - pane.window is AuiNotebook or normal Panel,
928
- ## - pane.window is floating (win.Parent is AuiFloatingFrame) or docked.
929
-
930
- plug = self.get_plug(name) # -> None if pane.window is a Graph
931
- win = pane.window # -> Window (plug / notebook / Graph)
927
+ plug = self.get_plug(name) # -> None if pane.window is a Graph
928
+ win = pane.window
932
929
  try:
933
930
  shown = plug.IsShown()
934
931
  except AttributeError:
935
932
  shown = pane.IsShown()
933
+
936
934
  if show and not shown:
937
935
  if isinstance(win, aui.AuiNotebook):
938
936
  j = win.GetPageIndex(plug)
939
937
  if j != win.Selection:
940
- win.Selection = j # the focus moves => EVT_SHOW
938
+ win.Selection = j # the focus moves => EVT_SHOW
941
939
  else:
942
940
  plug.handler('page_shown', plug)
943
941
  else:
944
942
  win.handler('page_shown', win)
943
+ if plug:
944
+ plug.SetFocus() # plugins only
945
945
  elif not show and shown:
946
946
  if isinstance(win, aui.AuiNotebook):
947
- for plug in win.all_pages:
947
+ for plug in win.get_pages():
948
948
  plug.handler('page_closed', plug)
949
949
  else:
950
950
  win.handler('page_closed', win)
951
951
 
952
952
  ## Modify the floating position of the pane when displayed.
953
953
  ## Note: This is a known bug in wxWidgets 3.17 -- 3.20,
954
- ## and will be fixed in wxPython 4.2.1.
954
+ ## and will be fixed in wx ver 4.2.1.
955
955
  if wx.Display.GetFromWindow(pane.window) == -1:
956
956
  pane.floating_pos = wx.GetMousePosition()
957
957
 
958
958
  pane.Show(show)
959
959
  self._mgr.Update()
960
960
  return (show != shown)
961
-
962
- def update_pane(self, name, show=False, **kwargs):
963
- """Update the layout of the pane.
961
+
962
+ def update_pane(self, name, **props):
963
+ """Update the layout of the pane (internal use only).
964
964
 
965
965
  Note:
966
966
  This is called automatically from load_plug,
967
967
  and should not be called directly from user.
968
968
  """
969
969
  pane = self.get_pane(name)
970
-
971
- pane.dock_layer = kwargs.get('layer', 0)
972
- pane.dock_pos = kwargs.get('pos', 0)
973
- pane.dock_row = kwargs.get('row', 0)
974
- pane.dock_proportion = kwargs.get('prop') or pane.dock_proportion
975
- pane.floating_pos = kwargs.get('floating_pos') or pane.floating_pos
976
- pane.floating_size = kwargs.get('floating_size') or pane.floating_size
970
+ for k, v in props.items():
971
+ if v is not None:
972
+ setattr(pane, k, v)
977
973
 
978
974
  plug = self.get_plug(name)
979
975
  if plug:
980
976
  dock = plug.dockable
981
- if not isinstance(dock, bool): # prior to kwargs
982
- kwargs.update(dock=dock)
977
+ if not isinstance(dock, bool):
978
+ pane.dock_direction = dock
983
979
  if not plug.caption:
984
980
  pane.CaptionVisible(False) # no caption bar
985
- pane.Gripper(dock not in (0, 5)) # show a grip when docked
981
+ pane.Gripper(dock not in (0,5)) # show a grip when docked
986
982
  pane.Dockable(dock)
987
983
 
988
- dock = kwargs.get('dock')
989
- pane.dock_direction = dock or 0
990
- if dock:
984
+ if pane.dock_direction:
991
985
  pane.Dock()
992
986
  else:
993
987
  pane.Float()
994
- return self.show_pane(name, show)
995
-
996
- def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
988
+
989
+ def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
997
990
  pane = evt.GetPane()
998
991
  win = pane.window
999
992
  if isinstance(win, aui.AuiNotebook):
1000
- for plug in win.all_pages:
993
+ for plug in win.get_pages():
1001
994
  plug.handler('page_closed', plug)
1002
995
  else:
1003
996
  win.handler('page_closed', win)
1004
- evt.Skip()
1005
-
997
+ evt.Skip(False) # Don't skip to avoid being called twice.
998
+
1006
999
  ## --------------------------------
1007
- ## Plugin <Layer> interface
1000
+ ## Plugin <Layer> interface.
1008
1001
  ## --------------------------------
1009
1002
  plugins = property(lambda self: self.__plugins)
1010
-
1003
+
1004
+ def register(self, cls=None, **kwargs):
1005
+ """Decorator of plugin class register."""
1006
+ if cls is None:
1007
+ return lambda f: self.register(f, **kwargs)
1008
+ self.load_plug(cls, force=1, show=1, **kwargs)
1009
+ return cls
1010
+
1011
1011
  def require(self, name):
1012
1012
  """Get named plug window.
1013
1013
  If not found, try to load it once.
@@ -1021,53 +1021,77 @@ class Frame(mwx.Frame):
1021
1021
  if self.load_plug(name) is not False:
1022
1022
  return self.get_plug(name)
1023
1023
  return plug
1024
-
1024
+
1025
1025
  def get_plug(self, name):
1026
- """Get named plug window.
1027
-
1028
- Args:
1029
- name : str or plug object.
1030
- """
1026
+ """Get named plug window."""
1031
1027
  if isinstance(name, str):
1032
1028
  if name.endswith(".py"):
1033
1029
  name, _ = os.path.splitext(os.path.basename(name))
1034
1030
  if name in self.plugins:
1035
1031
  return self.plugins[name].__plug__
1036
1032
  elif isinstance(name, LayerInterface):
1037
- return name
1038
-
1039
- @staticmethod
1040
- def register(cls, module=None):
1041
- """Register dummy plug; Add module.Plugin <Layer>.
1042
- """
1043
- if not module:
1044
- module = inspect.getmodule(cls) # rebase module or __main__
1045
-
1046
- if issubclass(cls, LayerInterface):
1047
- cls.__module__ = module.__name__ # __main__ to module
1048
- warn(f"Duplicate iniheritance of LayerInterface by {cls}.")
1049
- module.Plugin = cls
1050
- return cls
1051
-
1052
- class _Plugin(LayerInterface, cls):
1053
- def __init__(self, parent, session=None, **kwargs):
1054
- cls.__init__(self, parent, **kwargs)
1055
- LayerInterface.__init__(self, parent, session)
1056
-
1057
- _Plugin.__module__ = cls.__module__ = module.__name__
1058
- _Plugin.__name__ = cls.__name__ + str("~")
1059
- _Plugin.__doc__ = cls.__doc__
1060
- module.Plugin = _Plugin
1061
- return _Plugin
1062
-
1063
- def load_module(self, root):
1064
- """Load module of plugin (internal use only).
1033
+ # return name
1034
+ return next((x for x in self.get_all_plugs() if x is name), None)
1035
+
1036
+ def get_all_plugs(self):
1037
+ for name, module in self.plugins.items():
1038
+ yield module.__plug__
1039
+
1040
+ def load_plug(self, root, session=None, force=False, show=False,
1041
+ dock=0, floating_pos=None, floating_size=None,
1042
+ **kwargs):
1043
+ """Load plugin.
1044
+
1045
+ Args:
1046
+ root: Plugin <Layer> module, or name of the module.
1047
+ Any wx.Window object can be specified (as dummy-plug).
1048
+ However, do not use this mode in release versions.
1049
+ session: Conditions for initializing the plug and starting session
1050
+ force: force loading even if it is already loaded
1051
+ show: the pane is shown after loaded
1052
+ dock: dock_direction (1:top, 2:right, 3:bottom, 4:left, 5:center)
1053
+ floating_pos: posision of floating window
1054
+ floating_size: size of floating window
1055
+ **kwargs: keywords for Plugin <Layer>
1056
+
1057
+ Returns:
1058
+ None if succeeded else False
1065
1059
 
1066
1060
  Note:
1067
- This is called automatically from load_plug,
1068
- and should not be called directly from user.
1061
+ The root module must contain a class Plugin <Layer>.
1069
1062
  """
1070
- dirname_, name = split_paths(root)
1063
+ props = dict(dock_direction=dock,
1064
+ floating_pos=floating_pos,
1065
+ floating_size=floating_size)
1066
+
1067
+ if inspect.ismodule(root):
1068
+ name = root.__name__
1069
+ elif inspect.isclass(root):
1070
+ name = root.__module__
1071
+ else:
1072
+ name = root
1073
+
1074
+ dirname_, name = os.path.split(name) # if the name is full-path:str
1075
+ if name.endswith(".py"):
1076
+ name = name[:-3]
1077
+
1078
+ if not force:
1079
+ ## 文字列参照 (full-path) による重複ロードを避ける.
1080
+ module = next((v for v in self.plugins.values() if root == v.__file__), None)
1081
+ if module:
1082
+ plug = module.__plug__
1083
+ else:
1084
+ plug = self.get_plug(name)
1085
+ ## Check if the named plug is already loaded.
1086
+ if plug:
1087
+ self.update_pane(name, **props)
1088
+ self.show_pane(name, show)
1089
+ try:
1090
+ if session:
1091
+ plug.load_session(session)
1092
+ except Exception:
1093
+ traceback.print_exc() # Failed to load the plug session.
1094
+ return None
1071
1095
 
1072
1096
  ## Update the include-path to load the module correctly.
1073
1097
  if os.path.isdir(dirname_):
@@ -1075,135 +1099,81 @@ class Frame(mwx.Frame):
1075
1099
  sys.path.remove(dirname_)
1076
1100
  sys.path.insert(0, dirname_)
1077
1101
  elif dirname_:
1078
- print("- No such directory {!r}".format(dirname_))
1102
+ print(f"- No such directory {dirname_!r}.")
1079
1103
  return False
1080
1104
 
1105
+ ## Load or reload the module, and check whether it contains a class named `Plugin`.
1081
1106
  try:
1082
- if name in sys.modules:
1107
+ ## Check if the module is reloadable.
1108
+ loadable = not name.startswith(("__main__", "builtins")) # no __file__
1109
+ if not loadable:
1110
+ module = types.ModuleType(name) # dummy module (cannot reload)
1111
+ module.__file__ = "<scratch>"
1112
+ elif name in sys.modules:
1083
1113
  module = reload(sys.modules[name])
1084
1114
  else:
1085
1115
  module = import_module(name)
1086
- except Exception as e:
1087
- print(f"- Unable to load {root!r}.", e)
1116
+ except Exception:
1117
+ traceback.print_exc() # Unable to load the module.
1088
1118
  return False
1089
-
1090
- ## the module must have a class `Plugin`.
1091
- if not hasattr(module, 'Plugin'):
1092
- if isinstance(root, type):
1093
- warn(f"Use dummy plug for debugging {name!r}.")
1094
- module.__dummy_plug__ = root
1095
- self.register(root, module)
1096
1119
  else:
1097
- if hasattr(module, '__dummy_plug__'):
1098
- root = module.__dummy_plug__ # old class (imported)
1099
- cls = getattr(module, root.__name__) # new class (reloaded)
1100
- self.register(cls, module)
1101
- return module
1102
-
1103
- def load_plug(self, root, force=False, session=None,
1104
- show=False, dock=False, layer=0, pos=0, row=0, prop=10000,
1105
- floating_pos=None, floating_size=None, **kwargs):
1106
- """Load plugin.
1107
-
1108
- Args:
1109
- root : Plugin <Layer> module, or name of the module.
1110
- Any wx.Window object can be specified (as dummy-plug).
1111
- However, do not use this mode in release versions.
1112
- force : force loading even if it is already loaded
1113
- session : Conditions for initializing the plug and starting session
1114
- show : the pane is shown after loaded
1115
- dock : dock_direction (1:top, 2:right, 3:bottom, 4:left, 5:center)
1116
- layer : dock_layer
1117
- pos : dock_pos
1118
- row : dock_row position
1119
- prop : dock_proportion < 1e6 ?
1120
- floating_pos: posision of floating window
1121
- floating_size: size of floating window
1122
-
1123
- **kwargs: keywords for Plugin <Layer>
1124
-
1125
- Returns:
1126
- None if succeeded else False
1127
-
1128
- Note:
1129
- The root module must have a class Plugin <Layer>
1130
- """
1131
- props = dict(show=show, dock=dock, layer=layer, pos=pos, row=row, prop=prop,
1132
- floating_pos=floating_pos, floating_size=floating_size)
1133
-
1134
- _dirname, name = split_paths(root)
1135
-
1136
- plug = self.get_plug(name)
1137
- if plug and not force:
1138
- self.update_pane(name, **props)
1139
- try:
1140
- if session:
1141
- plug.load_session(session)
1142
- except Exception:
1143
- traceback.print_exc()
1144
- print("- Failed to load session of", plug)
1145
- return None
1146
-
1147
- module = self.load_module(root)
1148
- if not module:
1149
- return False # failed to import
1120
+ ## Register dummy plug; Add module.Plugin <Layer>.
1121
+ if not hasattr(module, 'Plugin'):
1122
+ if inspect.isclass(root):
1123
+ module.__dummy_plug__ = root.__name__
1124
+ root.reloadable = loadable
1125
+ _register__dummy_plug__(root, module)
1126
+ else:
1127
+ if hasattr(module, '__dummy_plug__'):
1128
+ root = getattr(module, module.__dummy_plug__)
1129
+ root.reloadable = loadable
1130
+ _register__dummy_plug__(root, module)
1150
1131
 
1132
+ ## Note: name (module.__name__) != Plugin.__module__ if module is a package.
1151
1133
  try:
1152
- name = module.Plugin.__module__
1153
- title = module.Plugin.category
1134
+ Plugin = module.Plugin # Check if the module has a class `Plugin`.
1135
+ title = Plugin.category # Plugin <LayerInterface>
1154
1136
 
1155
- pane = self._mgr.GetPane(title)
1156
-
1157
- if pane.IsOk(): # <pane:title> is already registered
1158
- nb = pane.window
1159
- if not isinstance(nb, aui.AuiNotebook):
1160
- raise NameError("Notebook name must not be the same as any other plugins")
1161
-
1162
- pane = self.get_pane(name)
1137
+ pane = self._mgr.GetPane(title) # Check if <pane:title> is already registered.
1138
+ if pane.IsOk():
1139
+ if not isinstance(pane.window, aui.AuiNotebook):
1140
+ raise NameError("Notebook name must not be the same as any other plugin")
1163
1141
 
1164
- if pane.IsOk(): # <pane:name> is already registered
1142
+ pane = self.get_pane(name) # Check if <pane:name> is already registered.
1143
+ if pane.IsOk():
1165
1144
  if name not in self.plugins:
1166
- raise NameError("Plugin name must not be the same as any other panes")
1167
-
1168
- props.update(
1169
- show = show or pane.IsShown(),
1170
- dock = pane.IsDocked() and pane.dock_direction,
1171
- layer = pane.dock_layer,
1172
- pos = pane.dock_pos,
1173
- row = pane.dock_row,
1174
- prop = pane.dock_proportion,
1175
- floating_pos = floating_pos or pane.floating_pos[:], # copy (pane unloaded)
1176
- floating_size = floating_size or pane.floating_size[:], # copy
1177
- )
1145
+ raise NameError("Plugin name must not be the same as any other pane")
1146
+
1178
1147
  except (AttributeError, NameError) as e:
1179
1148
  traceback.print_exc()
1180
- wx.CallAfter(wx.MessageBox,
1149
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1181
1150
  f"{e}\n\n" + traceback.format_exc(),
1182
1151
  f"Error in loading {module.__name__!r}",
1183
1152
  style=wx.ICON_ERROR)
1184
1153
  return False
1185
1154
 
1186
- ## Create and register the plugin
1155
+ ## Unload the plugin if loaded.
1187
1156
  if pane.IsOk():
1188
- self.unload_plug(name) # unload once right here
1157
+ show = show or pane.IsShown()
1158
+ props.update(
1159
+ dock_direction = pane.IsDocked() and pane.dock_direction,
1160
+ floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
1161
+ floating_size = floating_size or pane.floating_size[:], # copy unloading pane
1162
+ )
1163
+ self.unload_plug(name)
1189
1164
 
1165
+ ## Create the plugin object.
1190
1166
  try:
1191
- plug = module.Plugin(self, session, **kwargs)
1167
+ plug = Plugin(self, session, **kwargs)
1192
1168
  except Exception as e:
1193
1169
  traceback.print_exc()
1194
- wx.CallAfter(wx.MessageBox,
1170
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1195
1171
  f"{e}\n\n" + traceback.format_exc(),
1196
1172
  f"Error in loading {name!r}",
1197
1173
  style=wx.ICON_ERROR)
1198
1174
  return False
1199
1175
 
1200
- ## Add to the list after the plug is created successfully.
1201
- self.plugins[name] = module
1202
-
1203
- ## set reference of a plug (one module, one plugin)
1204
- module.__plug__ = plug
1205
-
1206
- ## Create pane or notebook pane
1176
+ ## Create pane or notebook pane.
1207
1177
  caption = plug.caption
1208
1178
  if not isinstance(caption, str):
1209
1179
  caption = name
@@ -1215,7 +1185,7 @@ class Frame(mwx.Frame):
1215
1185
  nb = pane.window
1216
1186
  nb.AddPage(plug, caption)
1217
1187
  else:
1218
- size = plug.GetSize() + (2,30) # padding for notebook
1188
+ size = plug.GetSize() + (2,30) # padding for notebook
1219
1189
  nb = AuiNotebook(self, name=title)
1220
1190
  nb.AddPage(plug, caption)
1221
1191
  self._mgr.AddPane(nb, aui.AuiPaneInfo()
@@ -1231,16 +1201,23 @@ class Frame(mwx.Frame):
1231
1201
  .Name(name).Caption(caption)
1232
1202
  .FloatingSize(size).MinSize(size).Show(0))
1233
1203
 
1204
+ ## Add to the list after the plug is created successfully.
1205
+ self.plugins[name] = module
1206
+
1207
+ ## Set reference of a plug (one module, one plugin).
1208
+ module.__plug__ = plug
1209
+
1234
1210
  ## Set winow.Name for inspection.
1235
1211
  plug.Name = name
1236
1212
 
1237
1213
  self.update_pane(name, **props)
1214
+ self.show_pane(name, show)
1238
1215
 
1239
- ## Create a menu
1216
+ ## Create a menu.
1240
1217
  plug.__Menu_item = None
1241
1218
 
1242
- if not hasattr(module, 'ID_'): # give a unique index to the module
1243
- global __plug_ID__ # cache ID *not* in [ID_LOWEST(4999):ID_HIGHEST(5999)]
1219
+ if not hasattr(module, 'ID_'): # give a unique index to the module
1220
+ global __plug_ID__ # cache ID *not* in [ID_LOWEST(4999):ID_HIGHEST(5999)]
1244
1221
  try:
1245
1222
  __plug_ID__
1246
1223
  except NameError:
@@ -1255,26 +1232,26 @@ class Frame(mwx.Frame):
1255
1232
  hint = (plug.__doc__ or name).strip().splitlines()[0]
1256
1233
  plug.__Menu_item = (
1257
1234
  module.ID_, text, hint, wx.ITEM_CHECK,
1258
- lambda v: self.show_pane(name, v.IsChecked(), interactive=1),
1235
+ lambda v: (self.update_pane(name),
1236
+ self.show_pane(name, v.IsChecked(), interactive=1)),
1259
1237
  lambda v: v.Check(plug.IsShown()),
1260
1238
  )
1261
1239
  if menu not in self.menubar:
1262
1240
  self.menubar[menu] = []
1263
1241
  self.menubar[menu] += [plug.__Menu_item]
1264
1242
  self.menubar.update(menu)
1243
+
1244
+ self.handler('plug_loaded', plug)
1265
1245
  return None
1266
-
1246
+
1267
1247
  def unload_plug(self, name):
1268
1248
  """Unload plugin and detach the pane from UI manager."""
1269
1249
  plug = self.get_plug(name)
1270
1250
  if not plug:
1251
+ print(f"- {name!r} is not listed in plugins.")
1271
1252
  return
1272
1253
 
1273
- name = plug.__module__
1274
- if name not in self.plugins:
1275
- return
1276
-
1277
- del self.plugins[name]
1254
+ del self.plugins[plug.Name]
1278
1255
 
1279
1256
  if plug.__Menu_item:
1280
1257
  menu, sep, tail = plug.menukey.rpartition('/')
@@ -1285,94 +1262,87 @@ class Frame(mwx.Frame):
1285
1262
  if isinstance(plug.Parent, aui.AuiNotebook):
1286
1263
  nb = plug.Parent
1287
1264
  j = nb.GetPageIndex(plug)
1288
- nb.RemovePage(j) # just remove page
1289
- ## nb.DeletePage(j) # Destroys plug object too.
1265
+ nb.RemovePage(j) # just remove page
1266
+ # nb.DeletePage(j) # Destroys plug object too.
1290
1267
  else:
1291
1268
  nb = None
1292
1269
  self._mgr.DetachPane(plug)
1293
1270
  self._mgr.Update()
1294
1271
 
1295
- plug.handler('page_closed', plug) # (even if not shown)
1272
+ self.handler('plug_unloaded', plug)
1273
+ plug.handler('page_closed', plug) # (even if not shown)
1296
1274
  plug.Destroy()
1297
1275
 
1298
1276
  if nb and not nb.PageCount:
1299
- self._mgr.DetachPane(nb) # detach notebook pane
1277
+ self._mgr.DetachPane(nb) # detach notebook pane
1300
1278
  self._mgr.Update()
1301
1279
  nb.Destroy()
1302
-
1280
+
1303
1281
  def reload_plug(self, name):
1282
+ """Reload plugin."""
1304
1283
  plug = self.get_plug(name)
1305
- if not plug or not plug.reloadable:
1284
+ if not plug:
1285
+ print(f"- {name!r} is not listed in plugins.")
1306
1286
  return
1287
+
1307
1288
  session = {}
1308
1289
  try:
1309
- print("Reloading {}...".format(plug))
1290
+ print(f"Reloading {plug}...")
1310
1291
  plug.save_session(session)
1311
1292
  except Exception:
1312
- traceback.print_exc()
1313
- print("- Failed to save session of", plug)
1293
+ traceback.print_exc() # Failed to save the plug session.
1294
+
1314
1295
  self.load_plug(plug.__module__, force=1, session=session)
1315
1296
 
1316
- ## Update shell.target --> new plug
1317
- for shell in self.shellframe.all_shells:
1297
+ ## Update shell.target --> new plug.
1298
+ for shell in self.shellframe.get_all_shells():
1318
1299
  if shell.target is plug:
1319
1300
  shell.handler('shell_activated', shell)
1320
-
1321
- @ignore(ResourceWarning)
1322
- def edit_plug(self, name):
1323
- plug = self.get_plug(name)
1324
- if not plug:
1325
- return
1326
- ## this = inspect.getmodule(plug)
1327
- this = self.plugins[plug.__module__]
1328
- cmd = '{} "{}"'.format(self.Editor, this.__file__)
1329
- subprocess.Popen(cmd)
1330
- self.message(cmd)
1331
-
1301
+
1332
1302
  def inspect_plug(self, name):
1333
- """Dive into the process to inspect plugs in the shell.
1334
- """
1303
+ """Dive into the process to inspect plugs in the shell."""
1335
1304
  plug = self.get_plug(name)
1336
1305
  if not plug:
1306
+ print(f"- {name!r} is not listed in plugins.")
1337
1307
  return
1338
1308
 
1339
1309
  shell = self.shellframe.clone_shell(plug)
1310
+ name = plug.Name # init(shell) で名前を参照するため再定義する
1340
1311
 
1341
1312
  @shell.handler.bind("shell_activated")
1342
1313
  def init(shell):
1314
+ """Called when the plug shell is activated."""
1343
1315
  nonlocal plug
1344
1316
  _plug = self.get_plug(name)
1345
1317
  if _plug is not plug:
1346
- shell.target = _plug or self # reset for loaded/unloaded plug
1318
+ shell.target = _plug or self # Reset the target to the reloaded plug.
1347
1319
  plug = _plug
1348
1320
  init(shell)
1349
1321
  self.shellframe.Show()
1350
- if wx.GetKeyState(wx.WXK_SHIFT):
1322
+ if wx.GetKeyState(wx.WXK_SHIFT): # open the source code.
1351
1323
  self.shellframe.load(plug)
1352
-
1324
+
1353
1325
  def OnLoadPlugins(self, evt):
1354
1326
  with wx.FileDialog(self, "Load a plugin file",
1355
1327
  wildcard="Python file (*.py)|*.py",
1356
- style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST
1357
- |wx.FD_MULTIPLE) as dlg:
1328
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE) as dlg:
1358
1329
  if dlg.ShowModal() == wx.ID_OK:
1359
1330
  for path in dlg.Paths:
1360
1331
  self.load_plug(path)
1361
-
1332
+
1362
1333
  def Quit(self, evt=None):
1363
1334
  """Stop all Layer threads."""
1364
- for name in self.plugins:
1365
- plug = self.get_plug(name)
1335
+ for plug in self.get_all_plugs():
1366
1336
  thread = plug.thread # Note: thread can be None or shared.
1367
1337
  if thread and thread.active:
1368
- thread.active = 0
1338
+ # thread.active = 0
1369
1339
  thread.Stop()
1370
-
1340
+
1371
1341
  ## --------------------------------
1372
- ## load/save index file
1342
+ ## load/save index file.
1373
1343
  ## --------------------------------
1374
- ATTRIBUTESFILE = "results.index"
1375
-
1344
+ INDEXFILE = "results.index"
1345
+
1376
1346
  def load_index(self, filename=None, view=None):
1377
1347
  """Load frames :ref to the Index file.
1378
1348
 
@@ -1382,10 +1352,10 @@ class Frame(mwx.Frame):
1382
1352
  view = self.selected_view
1383
1353
 
1384
1354
  if not filename:
1385
- fn = view.frame.pathname if view.frame else ''
1386
- with wx.FileDialog(self, "Select index file to import",
1387
- defaultDir=os.path.dirname(fn or ''),
1388
- defaultFile=self.ATTRIBUTESFILE,
1355
+ default_path = view.frame.pathname if view.frame else None
1356
+ with wx.FileDialog(self, "Select index file to load",
1357
+ defaultDir=os.path.dirname(default_path or ''),
1358
+ defaultFile=self.INDEXFILE,
1389
1359
  wildcard="Index (*.index)|*.index|"
1390
1360
  "ALL files (*.*)|*.*",
1391
1361
  style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST) as dlg:
@@ -1399,16 +1369,16 @@ class Frame(mwx.Frame):
1399
1369
  frames = self.load_buffer(paths, view)
1400
1370
  if frames:
1401
1371
  for frame in frames:
1402
- frame.update_attributes(res.get(frame.name))
1372
+ frame.update_attr(res.get(frame.name))
1403
1373
 
1404
1374
  n = len(frames)
1405
- self.message(
1375
+ print(self.message(
1406
1376
  "{} frames were imported, "
1407
1377
  "{} files were skipped, "
1408
- "{} files are missing.".format(n, len(res)-n, len(mis)))
1409
- print(self.message.read())
1378
+ "{} files are missing.".format(n, len(res)-n, len(mis))
1379
+ ))
1410
1380
  return frames
1411
-
1381
+
1412
1382
  def save_index(self, filename=None, frames=None):
1413
1383
  """Save frames :ref to the Index file.
1414
1384
  """
@@ -1419,10 +1389,10 @@ class Frame(mwx.Frame):
1419
1389
  return None
1420
1390
 
1421
1391
  if not filename:
1422
- fn = view.frame.pathname if view.frame else ''
1392
+ default_path = view.frame.pathname if view.frame else None
1423
1393
  with wx.FileDialog(self, "Select index file to export",
1424
- defaultDir=os.path.dirname(fn or ''),
1425
- defaultFile=self.ATTRIBUTESFILE,
1394
+ defaultDir=os.path.dirname(default_path or ''),
1395
+ defaultFile=self.INDEXFILE,
1426
1396
  wildcard="Index (*.index)|*.index",
1427
1397
  style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1428
1398
  if dlg.ShowModal() != wx.ID_OK:
@@ -1435,234 +1405,300 @@ class Frame(mwx.Frame):
1435
1405
  try:
1436
1406
  self.message("Export index of {!r}...".format(frame.name))
1437
1407
  fn = frame.pathname
1438
- if not fn:
1439
- fn = os.path.join(savedir, frame.name) # new file
1408
+ if not fn or fn.endswith('>'): # <dummy-path>
1409
+ fn = os.path.join(savedir, fix_fnchars(frame.name))
1440
1410
  if not os.path.exists(fn):
1441
1411
  if not fn.endswith('.tif'):
1442
1412
  fn += '.tif'
1443
1413
  self.write_buffer(fn, frame.buffer)
1444
1414
  frame.pathname = fn
1445
- frame.name = os.path.basename(fn) # new name and pathname
1415
+ frame.name = os.path.basename(fn) # new name and pathname
1416
+ print(' ', self.message("\b done."))
1417
+ else:
1418
+ print(' ', self.message("\b skipped."))
1446
1419
  output_frames.append(frame)
1447
- print(' ', self.message("\b done."))
1448
- except (PermissionError, OSError) as e:
1420
+ except OSError as e:
1449
1421
  print('-', self.message("\b failed.", e))
1450
1422
 
1451
1423
  frames = output_frames
1452
1424
  res, mis = self.write_attributes(filename, frames)
1453
1425
  n = len(frames)
1454
- self.message(
1426
+ print(self.message(
1455
1427
  "{} frames were exported, "
1456
1428
  "{} files were skipped, "
1457
- "{} files are missing.".format(n, len(res)-n, len(mis)))
1458
- print(self.message.read())
1429
+ "{} files are missing.".format(n, len(res)-n, len(mis))
1430
+ ))
1459
1431
  return frames
1460
-
1432
+
1461
1433
  ## --------------------------------
1462
- ## load/save frames and attributes
1434
+ ## load/save frames and attributes.
1463
1435
  ## --------------------------------
1464
-
1436
+
1465
1437
  @classmethod
1466
- def read_attributes(self, filename):
1467
- """Read attributes file."""
1468
- from numpy import nan, inf # noqa: necessary to eval
1469
- import datetime # noqa: necessary to eval
1438
+ def read_attributes(self, filename, check_path=True):
1439
+ """Read attributes file.
1440
+
1441
+ Returns:
1442
+ res: <dict> Successfully loaded attribute information.
1443
+ mis: <dict> Attributes whose file paths were missing.
1444
+ """
1445
+ def dt_parser(dct):
1446
+ for k, v in dct.items():
1447
+ if isinstance(v, str):
1448
+ try:
1449
+ dct[k] = datetime.fromisoformat(v)
1450
+ except Exception:
1451
+ pass
1452
+ return dct
1470
1453
  try:
1471
1454
  res = {}
1472
1455
  mis = {}
1473
1456
  savedir = os.path.dirname(filename)
1474
1457
  with open(filename) as i:
1475
- res.update(eval(i.read())) # read res <dict>
1458
+ s = i.read()
1459
+ try:
1460
+ res.update(json.loads(s, object_hook=dt_parser)) # Read res safely.
1461
+ except json.decoder.JSONDecodeError:
1462
+ res.update(eval(s)) # Read res <dict> for backward compatibility.
1476
1463
 
1477
- for name, attr in tuple(res.items()):
1478
- fn = os.path.join(savedir, name)
1479
- if not os.path.exists(fn): # search by relpath (dir+name)
1480
- fn = attr.get('pathname')
1481
- if not os.path.exists(fn): # check & pop missing files
1482
- res.pop(name)
1483
- mis.update({name:attr})
1484
- else:
1485
- attr.update(pathname=fn)
1464
+ if check_path:
1465
+ for name, attr in tuple(res.items()):
1466
+ fn = os.path.join(savedir, name) # search by relpath (saved dir/name)
1467
+ if os.path.exists(fn):
1468
+ attr['pathname'] = fn # If found, update pathname.
1469
+ else:
1470
+ fn = attr.get('pathname') # If not found, check for the recorded path.
1471
+ if not fn or not os.path.exists(fn):
1472
+ mis[name] = res.pop(name) # pop missing items
1486
1473
  except FileNotFoundError:
1487
1474
  pass
1488
1475
  except Exception as e:
1489
1476
  print("- Failed to read attributes.", e)
1490
1477
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1491
- finally:
1492
- return res, mis # finally raises no exception
1493
-
1478
+ return res, mis
1479
+
1494
1480
  @classmethod
1495
- def write_attributes(self, filename, frames):
1496
- """Write attributes file."""
1481
+ def write_attributes(self, filename, frames, merge_data=True):
1482
+ """Write attributes file.
1483
+
1484
+ Returns:
1485
+ res: <dict> Successfully loaded attribute information.
1486
+ mis: <dict> Attributes whose file paths were missing.
1487
+ """
1488
+ def dt_converter(o):
1489
+ if isinstance(o, datetime):
1490
+ return o.isoformat()
1497
1491
  try:
1498
- res, mis = self.read_attributes(filename)
1499
- new = dict((x.name, x.attributes) for x in frames)
1500
-
1501
- ## `res` order may differ from that of given frames,
1502
- ## so we take a few steps to merge `new` to be exported.
1503
-
1504
- res.update(new) # res updates to new info,
1505
- new.update(res) # copy res back keeping new order.
1492
+ new = dict((frame.name, frame.attributes) for frame in frames)
1493
+ mis = {}
1494
+ if merge_data:
1495
+ res, mis = self.read_attributes(filename)
1496
+ ## Merge existing attributes from `res` to `new`,
1497
+ ## while keeping the order and values from `frames` (new) priority.
1498
+ for name, attr in res.items():
1499
+ if name not in new:
1500
+ new[name] = attr
1506
1501
 
1507
1502
  with open(filename, 'w') as o:
1508
- print(pformat(tuple(new.items())), file=o)
1509
-
1503
+ # print(pformat(tuple(new.items())), file=o) # tuple with pformat is deprecated.
1504
+ json.dump(new, o, indent=2, default=dt_converter)
1510
1505
  except Exception as e:
1511
1506
  print("- Failed to write attributes.", e)
1512
1507
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1513
- finally:
1514
- return new, mis # finally raises no exception
1515
-
1508
+ return new, mis
1509
+
1516
1510
  def load_frame(self, paths=None, view=None):
1517
- """Load frames from files to the view window.
1511
+ """Load frames and the attributes from files to the view window."""
1512
+ if not view:
1513
+ view = self.selected_view
1514
+
1515
+ if isinstance(paths, str): # for single frame
1516
+ paths = [paths]
1517
+
1518
+ if paths is None:
1519
+ default_path = view.frame.pathname if view.frame else None
1520
+ with wx.FileDialog(self, "Open image files",
1521
+ defaultDir=os.path.dirname(default_path or ''),
1522
+ defaultFile='',
1523
+ wildcard='|'.join(self.wildcards),
1524
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE) as dlg:
1525
+ if dlg.ShowModal() != wx.ID_OK:
1526
+ return None
1527
+ paths = dlg.Paths
1518
1528
 
1519
- Load buffer and the attributes of the frame.
1520
- If the file names duplicate, the latter takes priority.
1521
- """
1522
1529
  frames = self.load_buffer(paths, view)
1523
1530
  if frames:
1524
- savedirs = {}
1531
+ saved_results = {}
1525
1532
  for frame in frames:
1533
+ if frame.pathname.endswith('>'): # <dummy-path>
1534
+ ## Attributes are compiled in load_buffer.
1535
+ continue
1536
+ ## Compile attributes from index files located in each frame path.
1526
1537
  savedir = os.path.dirname(frame.pathname)
1527
- if savedir not in savedirs:
1528
- fn = os.path.join(savedir, self.ATTRIBUTESFILE)
1538
+ if savedir not in saved_results:
1539
+ fn = os.path.join(savedir, self.INDEXFILE)
1529
1540
  res, mis = self.read_attributes(fn)
1530
- savedirs[savedir] = res
1531
- results = savedirs[savedir]
1532
- frame.update_attributes(results.get(frame.name))
1541
+ saved_results[savedir] = res
1542
+ res = saved_results[savedir]
1543
+ frame.update_attr(res.get(frame.name))
1533
1544
  return frames
1534
-
1545
+
1535
1546
  def save_frame(self, path=None, frame=None):
1536
- """Save frame to a file.
1547
+ """Save frame and the attributes to a file."""
1548
+ view = self.selected_view
1549
+
1550
+ if not frame:
1551
+ frame = view.frame
1552
+ if not frame:
1553
+ return None
1554
+
1555
+ if not path:
1556
+ default_path = view.frame.pathname if view.frame else None
1557
+ with wx.FileDialog(self, "Save buffer as",
1558
+ defaultDir=os.path.dirname(default_path or ''),
1559
+ defaultFile=fix_fnchars(frame.name),
1560
+ wildcard='|'.join(self.wildcards),
1561
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1562
+ if dlg.ShowModal() != wx.ID_OK:
1563
+ return None
1564
+ path = dlg.Path
1537
1565
 
1538
- Save buffer and the attributes of the frame.
1539
- """
1540
1566
  frame = self.save_buffer(path, frame)
1541
1567
  if frame:
1542
1568
  savedir = os.path.dirname(frame.pathname)
1543
- fn = os.path.join(savedir, self.ATTRIBUTESFILE)
1569
+ fn = os.path.join(savedir, self.INDEXFILE)
1544
1570
  res, mis = self.write_attributes(fn, [frame])
1545
1571
  return frame
1546
-
1572
+
1573
+ def save_frames_as_tiff(self, path=None, frames=None):
1574
+ """Save frames to a multi-page tiff."""
1575
+ if not frames:
1576
+ frames = self.selected_view.all_frames
1577
+ if not frames:
1578
+ return None
1579
+
1580
+ if not path:
1581
+ with wx.FileDialog(self, "Save buffers as a multi-page tiff",
1582
+ defaultFile="Stack-image",
1583
+ wildcard="TIF file (*.tif)|*.tif",
1584
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1585
+ if dlg.ShowModal() != wx.ID_OK:
1586
+ return None
1587
+ path = dlg.Path
1588
+ _name, ext = os.path.splitext(path)
1589
+ if ext != ".tif":
1590
+ path += ".tif"
1591
+ try:
1592
+ name = os.path.basename(path)
1593
+ self.message("Saving {!r}...".format(name))
1594
+ with wx.BusyInfo(f"One moment please, saving {name!r}..."):
1595
+ stack = [Image.fromarray(frame.buffer) for frame in frames]
1596
+ stack[0].save(path,
1597
+ save_all=True,
1598
+ compression="tiff_deflate", # cf. tiff_lzw
1599
+ append_images=stack[1:])
1600
+ n = len(frames)
1601
+ d = len(str(n))
1602
+ for j, frame in enumerate(frames):
1603
+ frame.pathname = path + f"<{j:0{d}}>" # <dummy-path>
1604
+ ## multi-page tiff: 同名のインデクスファイルに属性を書き出す.
1605
+ self.write_attributes(path[:-4] + ".index", frames, merge_data=False)
1606
+ self.message("\b done.")
1607
+ return True
1608
+ except Exception as e:
1609
+ self.message("\b failed.")
1610
+ wx.MessageBox(str(e), style=wx.ICON_ERROR)
1611
+ return False
1612
+
1547
1613
  ## --------------------------------
1548
- ## load/save images
1614
+ ## load/save images.
1549
1615
  ## --------------------------------
1550
1616
  wildcards = [
1551
1617
  "TIF file (*.tif)|*.tif",
1552
1618
  "ALL files (*.*)|*.*",
1553
1619
  ]
1554
-
1620
+
1555
1621
  @staticmethod
1556
1622
  def read_buffer(path):
1557
1623
  """Read buffer from a file (to be overridden)."""
1558
1624
  buf = Image.open(path)
1559
1625
  info = {}
1560
- if buf.mode[:3] == 'RGB': # 今のところカラー画像には対応する気はない▼
1561
- buf = buf.convert('L') # ここでグレースケールに変換する
1562
- ## return np.asarray(buf), info # ref
1563
- ## return np.array(buf), info # copy
1626
+ if buf.mode[:3] == 'RGB': # 今のところカラー画像には対応する気はない▼
1627
+ buf = buf.convert('L') # ここでグレースケールに変換する
1628
+ # return np.asarray(buf), info # ref
1629
+ # return np.array(buf), info # copy
1564
1630
  return buf, info
1565
-
1631
+
1566
1632
  @staticmethod
1567
1633
  def write_buffer(path, buf):
1568
1634
  """Write buffer to a file (to be overridden)."""
1569
1635
  try:
1570
1636
  img = Image.fromarray(buf)
1571
- img.save(path) # PIL saves as L, I, F, and RGB.
1637
+ img.save(path) # PIL saves as L, I, F, and RGB.
1572
1638
  except PermissionError:
1573
1639
  raise
1574
- except OSError: # cannot write mode L, I, F as BMP, etc.
1640
+ except OSError: # cannot write mode L, I, F as BMP, etc.
1575
1641
  if os.path.exists(path):
1576
1642
  os.remove(path)
1577
1643
  raise
1578
-
1644
+
1579
1645
  @ignore(ResourceWarning)
1580
- def load_buffer(self, paths=None, view=None):
1581
- """Load buffers from paths to the view window.
1582
-
1583
- If no view given, the currently selected view is chosen.
1646
+ def load_buffer(self, paths, view):
1647
+ """Load buffers from paths to the view window (internal use only).
1584
1648
  """
1585
- if not view:
1586
- view = self.selected_view
1587
-
1588
- if isinstance(paths, str): # for single frame
1589
- paths = [paths]
1590
-
1591
- if paths is None:
1592
- fn = view.frame.pathname if view.frame else ''
1593
- with wx.FileDialog(self, "Open image files",
1594
- defaultDir=os.path.dirname(fn or ''),
1595
- defaultFile='',
1596
- wildcard='|'.join(self.wildcards),
1597
- style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST
1598
- |wx.FD_MULTIPLE) as dlg:
1599
- if dlg.ShowModal() != wx.ID_OK:
1600
- return None
1601
- paths = dlg.Paths
1602
1649
  frames = []
1603
1650
  frame = None
1651
+ paths = list(dict.fromkeys(paths)) # 順序を保って重複を除く.
1604
1652
  try:
1605
1653
  for i, path in enumerate(paths):
1606
- fn = os.path.basename(path)
1607
- self.message("Loading {!r} ({} of {})...".format(fn, i+1, len(paths)))
1654
+ name = os.path.basename(path)
1655
+ self.message("Loading {!r} ({} of {})...".format(name, i+1, len(paths)))
1608
1656
  try:
1609
1657
  buf, info = self.read_buffer(path)
1610
1658
  except Image.UnidentifiedImageError:
1611
1659
  retvals = self.handler('unknown_format', path)
1612
1660
  if retvals and any(retvals):
1613
1661
  continue
1614
- raise # no context or no handlers or cannot identify image file
1662
+ raise # no context or no handlers or cannot identify image file
1615
1663
  except FileNotFoundError as e:
1616
1664
  print(e)
1617
1665
  continue
1618
1666
 
1619
- if isinstance(buf, TiffImageFile) and buf.n_frames > 1: # multi-page tiff
1667
+ if isinstance(buf, TiffImageFile) and buf.n_frames > 1:
1668
+ ## multi-page tiff: 同名のインデクスファイルから属性を読み出す.
1669
+ res, mis = self.read_attributes(path[:-4] + ".index", check_path=False)
1670
+ items = list({**res, **mis}.items())
1620
1671
  n = buf.n_frames
1621
1672
  d = len(str(n))
1622
1673
  for j in range(n):
1623
- self.message("Loading {!r} [{} of {} pages]...".format(fn, j+1, n))
1674
+ self.message("Loading {!r} [{} of {} pages]...".format(name, j+1, n))
1624
1675
  buf.seek(j)
1625
- name = "{:0{d}}-{}".format(j, fn, d=d)
1626
- frame = view.load(buf, name, show=0)
1676
+ if items:
1677
+ page_name, info = items[j] # original buffer name and attributes
1678
+ else:
1679
+ page_name = name + f"<{j:0{d}}>" # default buffer name
1680
+ info['pathname'] = path + f"<{j:0{d}}>" # <dummy-path>
1681
+ frame = view.load(buf, page_name, show=0, **info)
1682
+ frames.append(frame)
1627
1683
  else:
1628
- frame = view.load(buf, fn, show=0, pathname=path, **info)
1684
+ frame = view.load(buf, name, show=0, pathname=path, **info)
1629
1685
  frames.append(frame)
1630
1686
  self.message("\b done.")
1631
1687
  except Exception as e:
1632
1688
  self.message("\b failed.")
1633
1689
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1634
1690
 
1635
- if frame:
1636
- view.select(frame)
1691
+ view.select(frame)
1637
1692
  return frames
1638
-
1639
- def save_buffer(self, path=None, frame=None):
1640
- """Save buffer of the frame to a file.
1641
- """
1642
- view = self.selected_view
1643
- if not frame:
1644
- frame = view.frame
1645
- if not frame:
1646
- return None
1647
-
1648
- if not path:
1649
- fn = view.frame.pathname if view.frame else ''
1650
- with wx.FileDialog(self, "Save buffer as",
1651
- defaultDir=os.path.dirname(fn or ''),
1652
- defaultFile=re.sub(r'[\/:*?"<>|]', '_', frame.name),
1653
- wildcard='|'.join(self.wildcards),
1654
- style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1655
- if dlg.ShowModal() != wx.ID_OK:
1656
- return None
1657
- path = dlg.Path
1693
+
1694
+ def save_buffer(self, path, frame):
1695
+ """Save buffer of the frame to a file (internal use only)."""
1658
1696
  try:
1659
1697
  name = os.path.basename(path)
1660
1698
  self.message("Saving {!r}...".format(name))
1661
-
1662
1699
  self.write_buffer(path, frame.buffer)
1663
1700
  frame.name = name
1664
1701
  frame.pathname = path
1665
-
1666
1702
  self.message("\b done.")
1667
1703
  return frame
1668
1704
  except ValueError:
@@ -1674,58 +1710,24 @@ class Frame(mwx.Frame):
1674
1710
  self.message("\b failed.")
1675
1711
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1676
1712
  return None
1677
-
1678
- def save_buffers_as_tiffs(self, path=None, frames=None):
1679
- """Save buffers to a file as a multi-page tiff."""
1680
- if not frames:
1681
- frames = self.selected_view.all_frames
1682
- if not frames:
1683
- return None
1684
-
1685
- if not path:
1686
- with wx.FileDialog(self, "Save buffers as a multi-page tiff",
1687
- defaultFile="Stack-image",
1688
- wildcard="TIF file (*.tif)|*.tif",
1689
- style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1690
- if dlg.ShowModal() != wx.ID_OK:
1691
- return None
1692
- path = dlg.Path
1693
- try:
1694
- name = os.path.basename(path)
1695
- self.message("Saving {!r}...".format(name))
1696
- with wx.BusyInfo("One moment please, "
1697
- "now saving {!r}...".format(name)):
1698
- stack = [Image.fromarray(x.buffer.astype(int)) for x in frames]
1699
- stack[0].save(path,
1700
- save_all=True,
1701
- compression="tiff_deflate", # cf. tiff_lzw
1702
- append_images=stack[1:])
1703
- self.message("\b done.")
1704
- wx.MessageBox("{} files successfully saved into\n{!r}.".format(len(stack), path))
1705
- return True
1706
- except Exception as e:
1707
- self.message("\b failed.")
1708
- wx.MessageBox(str(e), style=wx.ICON_ERROR)
1709
- return False
1710
-
1713
+
1711
1714
  ## --------------------------------
1712
- ## load/save session
1715
+ ## load/save session.
1713
1716
  ## --------------------------------
1714
1717
  session_file = None
1715
-
1718
+
1716
1719
  def load_session(self, filename=None, flush=True):
1717
1720
  """Load session from file."""
1718
1721
  if not filename:
1719
- with wx.FileDialog(self, 'Load session',
1722
+ with wx.FileDialog(self, "Load session",
1720
1723
  wildcard="Session file (*.jssn)|*.jssn",
1721
- style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST
1722
- |wx.FD_CHANGE_DIR) as dlg:
1724
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_CHANGE_DIR) as dlg:
1723
1725
  if dlg.ShowModal() != wx.ID_OK:
1724
1726
  return
1725
1727
  filename = dlg.Path
1726
1728
 
1727
1729
  if flush:
1728
- for name in list(self.plugins): # plugins:dict mutates during iteration
1730
+ for name in list(self.plugins): # plugins:dict mutates during iteration
1729
1731
  self.unload_plug(name)
1730
1732
  del self.graph[:]
1731
1733
  del self.output[:]
@@ -1745,28 +1747,31 @@ class Frame(mwx.Frame):
1745
1747
  self._mgr.Update()
1746
1748
  self.menubar.reset()
1747
1749
 
1748
- dirname_ = os.path.dirname(i.name)
1749
- if dirname_:
1750
- os.chdir(dirname_)
1751
-
1752
1750
  ## Reposition the window if it is not on the desktop.
1753
1751
  if wx.Display.GetFromWindow(self) == -1:
1754
1752
  self.Position = (0, 0)
1755
1753
 
1754
+ ## LoadPerspective => 表示状態の不整合.手動でイベントを発生させる.
1755
+ for pane in self._mgr.GetAllPanes():
1756
+ if pane.IsShown():
1757
+ win = pane.window
1758
+ if isinstance(win, aui.AuiNotebook):
1759
+ win = win.CurrentPage
1760
+ win.handler('page_shown', win)
1761
+
1756
1762
  self.message("\b done.")
1757
-
1763
+
1758
1764
  def save_session_as(self):
1759
1765
  """Save session as a new file."""
1760
1766
  with wx.FileDialog(self, "Save session as",
1761
1767
  defaultDir=os.path.dirname(self.session_file or ''),
1762
1768
  defaultFile=os.path.basename(self.session_file or ''),
1763
1769
  wildcard="Session file (*.jssn)|*.jssn",
1764
- style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT
1765
- |wx.FD_CHANGE_DIR) as dlg:
1770
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT|wx.FD_CHANGE_DIR) as dlg:
1766
1771
  if dlg.ShowModal() == wx.ID_OK:
1767
1772
  self.session_file = dlg.Path
1768
1773
  self.save_session()
1769
-
1774
+
1770
1775
  def save_session(self):
1771
1776
  """Save session to file."""
1772
1777
  if not self.session_file:
@@ -1775,34 +1780,35 @@ class Frame(mwx.Frame):
1775
1780
  self.message("Saving session to {!r}...".format(self.session_file))
1776
1781
 
1777
1782
  with open(self.session_file, 'w') as o,\
1778
- np.printoptions(threshold=np.inf): # printing all(inf) elements
1783
+ np.printoptions(threshold=np.inf): # printing all(inf) elements
1779
1784
  o.write("#! Session file (This file is generated automatically)\n")
1780
1785
  o.write("self.SetSize({})\n".format(self.Size))
1781
1786
  o.write("self.SetPosition({})\n".format(self.Position))
1782
1787
 
1783
1788
  for name, module in self.plugins.items():
1784
1789
  plug = self.get_plug(name)
1785
- path = os.path.abspath(module.__file__)
1786
- basename = os.path.basename(path)
1787
- if basename == "__init__.py": # is module package?
1788
- path = path[:-12]
1790
+ name = module.__file__ # Replace the name with full-path.
1791
+ if not plug or not os.path.exists(name):
1792
+ print(f"Skipping dummy plugin {name!r}...")
1793
+ continue
1794
+ if hasattr(module, '__path__'): # is the module a package?
1795
+ name = os.path.dirname(name)
1789
1796
  session = {}
1790
1797
  try:
1791
1798
  plug.save_session(session)
1792
1799
  except Exception:
1793
- traceback.print_exc()
1794
- print("- Failed to save session of", plug)
1795
- o.write("self.load_plug({!r}, session={})\n".format(path, session or None))
1800
+ traceback.print_exc() # Failed to save the plug session.
1801
+ o.write("self.load_plug({!r}, session={})\n".format(name, session))
1796
1802
  o.write("self._mgr.LoadPerspective({!r})\n".format(self._mgr.SavePerspective()))
1797
1803
 
1798
1804
  def _save(view):
1799
- name = view.Name
1800
- paths = [x.pathname for x in view.all_frames if x.pathname]
1801
- o.write(f"self.{name}.unit = {view.unit:g}\n")
1802
- o.write(f"self.load_frame({paths!r}, self.{name})\n")
1805
+ paths = [frame.pathname for frame in view.all_frames if frame.pathname]
1806
+ paths = [fn for fn in paths if not fn.endswith('>')] # <dummy-path> 除外
1807
+ o.write(f"self.{view.Name}.unit = {view.unit:g}\n")
1808
+ o.write(f"self.load_frame({paths!r}, self.{view.Name})\n")
1803
1809
  try:
1804
- index = paths.index(view.frame.pathname)
1805
- o.write(f"self.{name}.select({index})\n")
1810
+ if view.frame.pathname in paths:
1811
+ o.write(f"self.{view.Name}.select({view.frame.name!r})\n")
1806
1812
  except Exception:
1807
1813
  pass
1808
1814