mwxlib 1.0.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
8
  from bdb import BdbQuit
8
- from pprint import pformat
9
- from subprocess import Popen
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,76 +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
165
- if '\n\n' not in msg:
166
- msg += '\n\n'
155
+ if not msg:
156
+ msg = ("The thread is running.\n\n"
157
+ "Do you want to terminate the thread?")
167
158
  try:
168
- self.event.clear() # suspend
169
- if wx.MessageBox( # Confirm terminatation.
170
- msg + "Do you want to terminate the process?",
159
+ self.event.clear() # suspend
160
+ if wx.MessageBox( # Confirm closing the thread.
161
+ msg, caption,
171
162
  style=wx.OK|wx.CANCEL|wx.ICON_WARNING) == wx.OK:
172
163
  self.Stop()
173
- return False
174
- return True
164
+ return True
165
+ return False
175
166
  finally:
176
- self.event.set() # resume
177
-
167
+ self.event.set() # resume
168
+
178
169
  def Start(self, f, *args, **kwargs):
179
- """Start the thread to run the specified function."""
170
+ """Start the thread to run the specified function.
171
+ """
180
172
  @wraps(f)
181
173
  def _f(*v, **kw):
182
174
  try:
183
- self.handler('thread_begin', self)
175
+ wx.CallAfter(self.handler, 'thread_begin', self)
184
176
  self.result = f(*v, **kw)
185
177
  except BdbQuit:
186
178
  pass
187
179
  except KeyboardInterrupt as e:
188
- print("- Thread:execution stopped:", e)
189
- except AssertionError as e:
190
- print("- Thread:execution failed:", e)
180
+ print("- Thread terminated by user:", e)
191
181
  except Exception as e:
182
+ print("- Thread failed in error:", e)
192
183
  traceback.print_exc()
193
- print("- Thread:exception:", e)
194
- 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)
195
189
  finally:
196
190
  self.active = 0
197
- self.handler('thread_end', self)
191
+ wx.CallAfter(self.handler, 'thread_end', self)
198
192
 
199
193
  if self.running:
200
194
  wx.MessageBox("The thread is running (Press [C-g] to quit).",
@@ -208,81 +202,86 @@ class Thread:
208
202
  args=args, kwargs=kwargs, daemon=True)
209
203
  self.worker.start()
210
204
  self.event.set()
211
-
205
+
212
206
  def Stop(self):
213
207
  """Stop the thread.
214
208
 
215
- 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.
216
213
  """
217
214
  def _stop():
218
215
  with wx.BusyInfo("One moment please, "
219
216
  "waiting for threads to die..."):
220
- self.handler('thread_quit', self)
221
217
  self.worker.join(1)
222
218
  if self.running:
223
219
  self.active = 0
224
- wx.CallAfter(_stop) # main-thread で終了させる
220
+ wx.CallAfter(_stop) # main thread で終了させる
225
221
 
226
222
 
227
223
  class LayerInterface(CtrlInterface):
228
- """Graphman.Layer interface mixin
224
+ """Graphman.Layer interface mixin.
229
225
 
230
226
  The layer properties can be switched by the following classvars::
231
227
 
232
- menukey : menu item key:str in parent menubar
233
- category : title of notebook holder, otherwise None for single pane
234
- caption : flag to set the pane caption to be visible
235
- a string can also be specified (default is __module__)
236
- dockable : flag to set the pane to be dockable
237
- type: bool or dock:int (1:t, 2:r, 3:b, 4:l, 5:c)
238
- reloadable : flag to set the Layer to be reloadable
239
- 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
240
236
 
241
237
  Note:
242
238
  parent <Frame> is not always equal to Parent when floating.
243
239
  Parent type can be <Frame>, <AuiFloatingFrame>, or <AuiNotebook>.
244
240
  """
245
- MENU = "Plugins" # default menu for Plugins
241
+ MENU = "Plugins" # default menu for Plugins
246
242
  menukey = "Plugins/"
247
243
  caption = True
248
244
  category = None
249
245
  dockable = True
250
- editable = True # deprecated
251
246
  reloadable = True
252
247
  unloadable = True
253
-
248
+
254
249
  graph = property(lambda self: self.parent.graph)
255
250
  output = property(lambda self: self.parent.output)
256
251
  histogram = property(lambda self: self.parent.histogram)
257
252
  selected_view = property(lambda self: self.parent.selected_view)
258
-
253
+
259
254
  message = property(lambda self: self.parent.message)
260
-
255
+
261
256
  ## thread_type = Thread
262
257
  thread = None
263
-
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
+
264
265
  ## for debug (internal use only)
265
266
  pane = property(lambda self: self.parent.get_pane(self))
266
-
267
+
267
268
  @property
268
269
  def Arts(self):
269
- """List of arts <matplotlib.artist.Artist>."""
270
+ """List of artists <matplotlib.artist.Artist>."""
270
271
  return self.__artists
271
-
272
+
272
273
  @Arts.setter
273
274
  def Arts(self, arts):
274
- for art in self.__artists[:]:
275
- if art not in arts:
276
- art.remove()
277
- self.__artists.remove(art)
278
- self.__artists = arts
279
-
275
+ for art in (set(self.__artists) - set(arts)):
276
+ art.remove()
277
+ self.__artists = list(arts)
278
+
280
279
  @Arts.deleter
281
280
  def Arts(self):
282
281
  for art in self.__artists:
283
282
  art.remove()
284
283
  self.__artists = []
285
-
284
+
286
285
  def attach_artists(self, axes, *artists):
287
286
  """Attach artists (e.g., patches) to the given axes."""
288
287
  for art in artists:
@@ -290,17 +289,8 @@ class LayerInterface(CtrlInterface):
290
289
  art.remove()
291
290
  art._transformSet = False
292
291
  axes.add_artist(art)
293
- if art not in self.__artists:
294
- self.__artists.append(art)
295
-
296
- def detach_artists(self, *artists):
297
- """Detach artists (e.g., patches) from their axes."""
298
- for art in artists:
299
- if art.axes:
300
- art.remove()
301
- art._transformSet = False
302
- self.__artists.remove(art)
303
-
292
+ self.Arts += [art for art in artists if art not in self.Arts]
293
+
304
294
  def __init__(self, parent, session=None):
305
295
  if hasattr(self, 'handler'):
306
296
  warn(f"Duplicate iniheritance of CtrlInterface by {self}.")
@@ -316,28 +306,28 @@ class LayerInterface(CtrlInterface):
316
306
  except AttributeError:
317
307
  self.parameters = None
318
308
 
319
- def copy_params(**kwargs):
320
- if self.parameters:
321
- 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)
322
314
 
323
- def paste_params(**kwargs):
324
- if self.parameters:
325
- 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)
326
320
 
327
- def reset_params(**kwargs):
321
+ def reset_params(evt, checked_only=False):
328
322
  self.Draw(None)
329
323
  if self.parameters:
330
- return self.set_params(**kwargs)
324
+ self.reset_params(checked_only)
331
325
 
332
326
  self.handler.append({ # DNA<Layer>
333
327
  None : {
334
- 'thread_begin' : [ None ], # begin processing
335
- 'thread_end' : [ None ], # end processing
336
- 'thread_quit' : [ None ], # terminated by user
337
- 'thread_error' : [ None ], # failed in error
338
- 'page_shown' : [ None, _F(self.Draw, True) ],
339
- 'page_closed' : [ None, _F(self.Draw, False) ],
340
- 'page_hidden' : [ 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) ],
341
331
  },
342
332
  0 : {
343
333
  'C-c pressed' : (0, _F(copy_params)),
@@ -350,33 +340,29 @@ class LayerInterface(CtrlInterface):
350
340
  })
351
341
  self.menu = [
352
342
  (wx.ID_COPY, "&Copy params\t(C-c)", "Copy params",
353
- lambda v: copy_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
343
+ lambda v: copy_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
354
344
  lambda v: v.Enable(bool(self.parameters))),
355
345
 
356
346
  (wx.ID_PASTE, "&Paste params\t(C-v)", "Read params",
357
- lambda v: paste_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
347
+ lambda v: paste_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
358
348
  lambda v: v.Enable(bool(self.parameters))),
359
349
  (),
360
350
  (wx.ID_RESET, "&Reset params\t(C-n)", "Reset params", Icon('-'),
361
- lambda v: reset_params(checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
351
+ lambda v: reset_params(v, checked_only=wx.GetKeyState(wx.WXK_SHIFT)),
362
352
  lambda v: v.Enable(bool(self.parameters))),
363
353
  (),
364
- (wx.ID_EDIT, "&Edit module", "Edit module", Icon('pen'),
365
- lambda v: self.parent.edit_plug(self.__module__),
366
- lambda v: v.Enable(self.editable)),
367
-
368
- (mwx.ID_(201), "&Reload module", "Reload module", Icon('load'),
369
- 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),
370
356
  lambda v: v.Enable(self.reloadable
371
357
  and not (self.thread and self.thread.active))),
372
358
 
373
- (mwx.ID_(202), "&Unload module", "Unload module", Icon('delete'),
374
- 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),
375
361
  lambda v: v.Enable(self.unloadable
376
362
  and not (self.thread and self.thread.active))),
377
363
  (),
378
364
  (mwx.ID_(203), "&Dive into {!r}".format(self.__module__), "dive", Icon('core'),
379
- lambda v: self.parent.inspect_plug(self.__module__)),
365
+ lambda v: self.parent.inspect_plug(self)),
380
366
  ]
381
367
  self.Bind(wx.EVT_CONTEXT_MENU,
382
368
  lambda v: Menu.Popup(self, self.menu))
@@ -387,7 +373,7 @@ class LayerInterface(CtrlInterface):
387
373
  try:
388
374
  self.Init()
389
375
  except Exception as e:
390
- traceback.print_exc()
376
+ traceback.print_exc() # Failed to initialize the plug.
391
377
  if parent:
392
378
  bmp = wx.StaticBitmap(self, bitmap=Icon('!!!'))
393
379
  txt = wx.StaticText(self, label="Exception")
@@ -397,31 +383,30 @@ class LayerInterface(CtrlInterface):
397
383
  if session:
398
384
  self.load_session(session)
399
385
  except Exception:
400
- traceback.print_exc()
401
- print("- Failed to load session of", self)
402
-
386
+ traceback.print_exc() # Failed to load the plug session.
387
+
403
388
  def Init(self):
404
389
  """Initialize layout before load_session (to be overridden)."""
405
390
  pass
406
-
391
+
407
392
  def load_session(self, session):
408
393
  """Restore settings from a session file (to be overridden)."""
409
394
  if 'params' in session:
410
395
  self.parameters = session['params']
411
-
396
+
412
397
  def save_session(self, session):
413
398
  """Save settings in a session file (to be overridden)."""
414
399
  if self.parameters:
415
400
  session['params'] = self.parameters
416
-
401
+
417
402
  def OnDestroy(self, evt):
418
403
  if evt.EventObject is self:
419
404
  if self.thread and self.thread.active:
420
- self.thread.active = 0
405
+ # self.thread.active = 0
421
406
  self.thread.Stop()
422
407
  del self.Arts
423
408
  evt.Skip()
424
-
409
+
425
410
  def OnShow(self, evt):
426
411
  if not self:
427
412
  return
@@ -431,35 +416,35 @@ class LayerInterface(CtrlInterface):
431
416
  else:
432
417
  self.handler('page_hidden', self)
433
418
  evt.Skip()
434
-
419
+
435
420
  Shown = property(
436
421
  lambda self: self.IsShown(),
437
- lambda self,v: self.Show(v))
438
-
422
+ lambda self, v: self.Show(v))
423
+
439
424
  def IsShown(self):
440
- """Returns True if the window is physically visible on the screen.
425
+ """Return True if the window is physically visible on the screen.
441
426
 
442
427
  (override) Equivalent to ``IsShownOnScreen``.
443
428
  Note: The instance could be a page within a notebook.
444
429
  """
445
- ## return self.pane.IsShown()
430
+ # return self.pane.IsShown()
446
431
  return self.IsShownOnScreen()
447
-
448
- def Show(self, show=True):
432
+
433
+ def Show(self, show=True, interactive=False):
449
434
  """Shows or hides the window.
450
435
 
451
436
  (override) Show associated pane window.
452
437
  Note: This might be called from a thread.
453
438
  """
454
- wx.CallAfter(self.parent.show_pane, self, show)
455
-
439
+ wx.CallAfter(self.parent.show_pane, self, show, interactive) # Use main thread.
440
+
456
441
  Drawn = property(
457
442
  lambda self: self.IsDrawn(),
458
- lambda self,v: self.Draw(v))
459
-
443
+ lambda self, v: self.Draw(v))
444
+
460
445
  def IsDrawn(self):
461
446
  return any(art.get_visible() for art in self.Arts)
462
-
447
+
463
448
  def Draw(self, show=None):
464
449
  """Draw artists.
465
450
  If show is None:default, draw only when the pane is visible.
@@ -489,12 +474,33 @@ class Layer(LayerInterface, KnobCtrlPanel):
489
474
  LayerInterface.__init__(self, parent, session)
490
475
 
491
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
+
492
498
  class Graph(GraphPlot):
493
499
  """GraphPlot (override) to better make use for graph manager
494
500
 
495
501
  Attributes:
496
- parent : Parent window (usually mainframe)
497
- loader : mainframe
502
+ parent: Parent window (usually mainframe)
503
+ loader: mainframe
498
504
  """
499
505
  def __init__(self, parent, loader=None, **kwargs):
500
506
  GraphPlot.__init__(self, parent, **kwargs)
@@ -512,67 +518,54 @@ class Graph(GraphPlot):
512
518
  'f5 pressed' : [ None, _F(self.refresh) ],
513
519
  },
514
520
  })
515
- ## ドロップターゲットを許可する
521
+ ## ドロップターゲットを許可する.
516
522
  self.SetDropTarget(MyFileDropLoader(self, self.loader))
517
-
523
+
518
524
  def refresh(self):
519
525
  if self.frame:
520
526
  self.frame.update_buffer()
521
527
  self.draw()
522
-
528
+
523
529
  def toggle_infobar(self):
524
530
  """Toggle infobar (frame.annotation)."""
525
531
  if self.infobar.IsShown():
526
532
  self.infobar.Dismiss()
527
533
  elif self.frame:
528
534
  self.infobar.ShowMessage(self.frame.annotation)
529
-
535
+
530
536
  def update_infobar(self, frame):
531
537
  """Show infobar (frame.annotation)."""
532
538
  if self.infobar.IsShown():
533
539
  self.infobar.ShowMessage(frame.annotation)
534
-
535
- def get_markups_visible(self):
536
- return self.marked.get_visible()
537
-
538
- def set_markups_visible(self, v):
539
- self.selected.set_visible(v)
540
- self.marked.set_visible(v)
541
- self.rected.set_visible(v)
542
- self.update_art_of_mark()
543
-
544
- def remove_markups(self):
545
- del self.Selector
546
- del self.Markers
547
- del self.Region
548
-
540
+
549
541
  def hide_layers(self):
550
- for name in self.parent.plugins:
551
- plug = self.parent.get_plug(name)
542
+ for plug in self.parent.get_all_plugs():
552
543
  for art in plug.Arts:
553
544
  art.set_visible(0)
554
- self.remove_markups()
545
+ del self.selector
546
+ del self.markers
547
+ del self.region
555
548
  self.draw()
556
549
 
557
550
 
558
551
  class MyFileDropLoader(wx.FileDropTarget):
559
- """File Drop interface
552
+ """File Drop interface.
560
553
 
561
554
  Args:
562
- target : target view to drop in, e.g. frame, graph, pane, etc.
563
- loader : mainframe
555
+ target: target view to drop in, e.g. frame, graph, pane, etc.
556
+ loader: mainframe
564
557
  """
565
558
  def __init__(self, target, loader):
566
559
  wx.FileDropTarget.__init__(self)
567
560
 
568
561
  self.view = target
569
562
  self.loader = loader
570
-
563
+
571
564
  def OnDropFiles(self, x, y, filenames):
572
- pos = self.view.ScreenPosition + (x,y)
565
+ pos = self.view.ScreenPosition + (x, y)
573
566
  paths = []
574
567
  for fn in filenames:
575
- name, ext = os.path.splitext(fn)
568
+ _name, ext = os.path.splitext(fn)
576
569
  if ext == '.py' or os.path.isdir(fn):
577
570
  self.loader.load_plug(fn, show=1,
578
571
  floating_pos=pos,
@@ -582,7 +575,7 @@ class MyFileDropLoader(wx.FileDropTarget):
582
575
  elif ext == '.index':
583
576
  self.loader.load_index(fn, self.view)
584
577
  else:
585
- paths.append(fn) # image file just stacks to be loaded
578
+ paths.append(fn) # image file just stacks to be loaded
586
579
  if paths:
587
580
  self.loader.load_frame(paths, self.view)
588
581
  return True
@@ -600,24 +593,24 @@ class Frame(mwx.Frame):
600
593
  graph = property(lambda self: self.__graph)
601
594
  output = property(lambda self: self.__output)
602
595
  histogram = property(lambda self: self.__histgrm)
603
-
596
+
604
597
  selected_view = property(lambda self: self.__view)
605
-
598
+
606
599
  def select_view(self, view):
607
600
  self.__view = view
608
601
  self.set_title(view.frame)
609
-
602
+
610
603
  @property
611
604
  def graphic_windows(self):
612
605
  """Graphic windows list.
613
606
  [0] graph [1] output [2:] others(user-defined)
614
607
  """
615
608
  return self.__graphic_windows
616
-
609
+
617
610
  @property
618
611
  def graphic_windows_on_screen(self):
619
612
  return [w for w in self.__graphic_windows if w.IsShownOnScreen()]
620
-
613
+
621
614
  def __init__(self, *args, **kwargs):
622
615
  mwx.Frame.__init__(self, *args, **kwargs)
623
616
 
@@ -625,7 +618,7 @@ class Frame(mwx.Frame):
625
618
  self._mgr.SetManagedWindow(self)
626
619
  self._mgr.SetDockSizeConstraint(0.5, 0.5)
627
620
 
628
- self.__plugins = {} # modules in the order of load/save
621
+ self.__plugins = {} # modules in the order of load/save
629
622
 
630
623
  self.__graph = Graph(self, log=self.message, margin=None)
631
624
  self.__output = Graph(self, log=self.message, margin=None)
@@ -646,7 +639,7 @@ class Frame(mwx.Frame):
646
639
  self.histogram.Name = "histogram"
647
640
 
648
641
  self._mgr.AddPane(self.graph,
649
- aui.AuiPaneInfo().CenterPane().CloseButton(1)
642
+ aui.AuiPaneInfo().CenterPane()
650
643
  .Name("graph").Caption("graph").CaptionVisible(1))
651
644
 
652
645
  size = (200, 200)
@@ -678,7 +671,7 @@ class Frame(mwx.Frame):
678
671
  lambda v: v.Enable(self.__view.frame is not None)),
679
672
 
680
673
  (wx.ID_SAVEAS, "&Save as TIFFs", "Save buffers as a multi-page tiff", Icon('saveall'),
681
- lambda v: self.save_buffers_as_tiffs(),
674
+ lambda v: self.save_frames_as_tiff(),
682
675
  lambda v: v.Enable(self.__view.frame is not None)),
683
676
  (),
684
677
  ("Index", (
@@ -701,7 +694,7 @@ class Frame(mwx.Frame):
701
694
  lambda v: self.save_session_as()),
702
695
  )),
703
696
  (),
704
- ("Options", []), # reserved for optional app settings
697
+ ("Options", []), # reserved for optional app settings
705
698
  (),
706
699
  (mwx.ID_(13), "&Graph window\tF9", "Show graph window", wx.ITEM_CHECK,
707
700
  lambda v: self.show_pane(self.graph, v.IsChecked()),
@@ -719,44 +712,37 @@ class Frame(mwx.Frame):
719
712
  (wx.ID_PASTE, "&Paste\t(C-v)", "Paste buffer from clipboard", Icon('paste'),
720
713
  lambda v: self.__view.read_buffer_from_clipboard()),
721
714
  (),
722
- (mwx.ID_(21), "Toggle &Markers", "Show/Hide markups", wx.ITEM_CHECK, Icon('+'),
723
- lambda v: self.__view.set_markups_visible(v.IsChecked()),
724
- lambda v: v.Check(self.__view.get_markups_visible())),
725
-
726
- (mwx.ID_(22), "&Remove Markers", "Remove markups", Icon('-'),
727
- lambda v: self.__view.remove_markups()),
728
- (),
729
- (mwx.ID_(23), "Hide all &Layers", "Hide all layers", Icon('xr'),
715
+ (mwx.ID_(23), "Hide all &layers", "Hide all layers", Icon('xr'),
730
716
  lambda v: self.__view.hide_layers()),
731
717
  (),
732
- (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,
733
719
  lambda v: self.show_pane(self.histogram, v.IsChecked()),
734
720
  lambda v: v.Check(self.histogram.IsShownOnScreen())),
735
721
 
736
- (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,
737
723
  lambda v: self.__view.invert_cmap(),
738
- lambda v: v.Check(self.__view.get_cmap()[-2:] == "_r")),
724
+ lambda v: v.Check(self.__view.get_cmapstr()[-2:] == "_r")),
739
725
  ]
740
726
 
741
727
  def _cmenu(i, name):
742
728
  return (mwx.ID_(30 + i), "&" + name, name, wx.ITEM_CHECK,
743
- lambda v: self.__view.set_cmap(name),
744
- lambda v: v.Check(self.__view.get_cmap() == name
745
- 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"),
746
732
  )
747
733
  colours = [c for c in dir(cm) if c[-2:] != "_r"
748
734
  and isinstance(getattr(cm, c), colors.LinearSegmentedColormap)]
749
735
 
750
736
  self.menubar["Edit"] += [
751
737
  (),
752
- ## (mwx.ID_(26), "Default Color", "gray", wx.ITEM_CHECK,
753
- ## lambda v: self.__view.set_cmap('gray'),
754
- ## lambda v: v.Check(self.__view.get_cmap()[:4] == "gray")),
755
- ##
756
- ("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",
757
743
  [_cmenu(i, c) for i, c in enumerate(colours) if c.islower()]),
758
744
 
759
- ("Other Colors",
745
+ ("Other colors",
760
746
  [_cmenu(i, c) for i, c in enumerate(colours) if not c.islower()]),
761
747
  ]
762
748
 
@@ -771,7 +757,8 @@ class Frame(mwx.Frame):
771
757
  self.menubar.reset()
772
758
 
773
759
  def show_frameview(frame):
774
- wx.CallAfter(self.show_pane, frame.parent) # Show graph / output
760
+ if not frame.parent.IsShown():
761
+ wx.CallAfter(self.show_pane, frame.parent)
775
762
 
776
763
  self.graph.handler.append({ # DNA<Graph:Frame>
777
764
  None : {
@@ -792,9 +779,9 @@ class Frame(mwx.Frame):
792
779
  },
793
780
  })
794
781
 
795
- ## Add main-menu to context-menu
796
- self.graph.menu += self.menubar["Edit"][2:7]
797
- 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]
798
785
 
799
786
  self._mgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
800
787
 
@@ -810,10 +797,10 @@ class Frame(mwx.Frame):
810
797
  _display(self.graph, show)
811
798
  _display(self.output, show)
812
799
  evt.Skip()
813
- self.Bind(wx.EVT_MOVE_START, lambda v :on_move(v, show=0))
814
- 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))
815
802
 
816
- ## Custom Key Bindings
803
+ ## Custom Key Bindings.
817
804
  self.define_key('* C-g', self.Quit)
818
805
 
819
806
  @self.shellframe.define_key('* C-g')
@@ -821,36 +808,33 @@ class Frame(mwx.Frame):
821
808
  """Dispatch quit to the main Frame."""
822
809
  self.handler('C-g pressed', evt)
823
810
 
824
- ## Accepts DnD
811
+ ## Accepts DnD.
825
812
  self.SetDropTarget(MyFileDropLoader(self.graph, self))
826
-
827
- ## Script editor for plugins (external call)
828
- EDITOR = "notepad"
829
-
813
+
830
814
  SYNC_SWITCH = True
831
-
815
+
832
816
  def sync(self, a, b):
833
817
  """Synchronize b to a."""
834
818
  if (self.SYNC_SWITCH
835
- and a.frame and b.frame
836
- and a.frame.unit == b.frame.unit
837
- and a.buffer.shape == b.buffer.shape):
838
- b.xlim = a.xlim
839
- b.ylim = a.ylim
840
- b.OnDraw(None)
841
- b.canvas.draw_idle()
842
-
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
+
843
827
  def set_title(self, frame):
844
828
  ssn = os.path.basename(self.session_file or '--')
845
829
  ssn, _ = os.path.splitext(ssn)
846
830
  name = (frame.pathname or frame.name) if frame else ''
847
831
  self.SetTitle("{}@{} - [{}] {}".format(self.Name, platform.node(), ssn, name))
848
-
849
- def OnActivate(self, evt): #<wx._core.ActivateEvent>
832
+
833
+ def OnActivate(self, evt): #<wx._core.ActivateEvent>
850
834
  if self and evt.Active:
851
835
  self.set_title(self.selected_view.frame)
852
-
853
- def OnClose(self, evt): #<wx._core.CloseEvent>
836
+
837
+ def OnClose(self, evt): #<wx._core.CloseEvent>
854
838
  ssn = os.path.basename(self.session_file or '--')
855
839
  with wx.MessageDialog(None,
856
840
  "Do you want to save session before closing program?",
@@ -862,66 +846,69 @@ class Frame(mwx.Frame):
862
846
  elif ret == wx.ID_CANCEL:
863
847
  evt.Veto()
864
848
  return
865
- for name in self.plugins:
866
- plug = self.get_plug(name)
867
- if plug.thread and plug.thread.active:
868
- if wx.MessageBox( # Confirm thread close.
869
- "The thread is running.\n\n"
870
- "Enter [q]uit to exit before closing.\n"
871
- "Continue closing?",
872
- "Close {!r}".format(plug.Name),
873
- style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
874
- self.message("The close has been canceled.")
875
- evt.Veto()
876
- return
877
- self.Quit()
878
- break
879
- for frame in self.graph.all_frames:
880
- if frame.pathname is None:
881
- if wx.MessageBox( # Confirm close.
882
- "You are closing unsaved frame.\n\n"
883
- "Continue closing?",
884
- "Close {!r}".format(frame.name),
885
- style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
886
- self.message("The close has been canceled.")
887
- evt.Veto()
888
- return
889
- 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
890
870
  evt.Skip()
891
-
871
+
892
872
  def Destroy(self):
893
- ## for name in list(self.plugins):
894
- ## self.unload_plug(name) # => plug.Destroy
895
873
  self._mgr.UnInit()
896
874
  return mwx.Frame.Destroy(self)
897
-
875
+
898
876
  ## --------------------------------
899
- ## pane window interface
877
+ ## pane window interface.
900
878
  ## --------------------------------
901
-
879
+
902
880
  def get_pane(self, name):
903
881
  """Get named pane or notebook pane.
904
882
 
905
883
  Args:
906
- name : str or plug object.
884
+ name: plug name or object.
907
885
  """
908
886
  plug = self.get_plug(name)
909
887
  if plug:
910
888
  name = plug.category or plug
911
889
  if name:
912
890
  return self._mgr.GetPane(name)
913
-
891
+
914
892
  def show_pane(self, name, show=True, interactive=False):
915
- """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
+ """
916
903
  pane = self.get_pane(name)
917
904
  if not pane.IsOk():
918
905
  return
919
906
 
920
907
  ## Set the graph and output window sizes to half & half.
921
- ## ドッキング時に再計算される
908
+ ## ドッキング時に再計算される.
922
909
  if name == "output" or name is self.output:
923
910
  w, h = self.graph.GetClientSize()
924
- pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
911
+ pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
925
912
 
926
913
  ## Force Layer windows to show.
927
914
  if interactive:
@@ -937,12 +924,8 @@ class Frame(mwx.Frame):
937
924
  pane.Float()
938
925
  show = True
939
926
 
940
- ## Note: We need to distinguish cases whether:
941
- ## - pane.window is AuiNotebook or normal Panel,
942
- ## - pane.window is floating (win.Parent is AuiFloatingFrame) or docked.
943
-
944
- plug = self.get_plug(name) # -> None if pane.window is a Graph
945
- win = pane.window # -> Window (plug / notebook / Graph)
927
+ plug = self.get_plug(name) # -> None if pane.window is a Graph
928
+ win = pane.window
946
929
  try:
947
930
  shown = plug.IsShown()
948
931
  except AttributeError:
@@ -952,33 +935,30 @@ class Frame(mwx.Frame):
952
935
  if isinstance(win, aui.AuiNotebook):
953
936
  j = win.GetPageIndex(plug)
954
937
  if j != win.Selection:
955
- win.Selection = j # the focus moves => EVT_SHOW
938
+ win.Selection = j # the focus moves => EVT_SHOW
956
939
  else:
957
940
  plug.handler('page_shown', plug)
958
941
  else:
959
942
  win.handler('page_shown', win)
943
+ if plug:
944
+ plug.SetFocus() # plugins only
960
945
  elif not show and shown:
961
946
  if isinstance(win, aui.AuiNotebook):
962
- for plug in win.all_pages:
947
+ for plug in win.get_pages():
963
948
  plug.handler('page_closed', plug)
964
949
  else:
965
950
  win.handler('page_closed', win)
966
951
 
967
- if pane.dock_direction:
968
- pane.Dock()
969
- else:
970
- pane.Float()
971
-
972
952
  ## Modify the floating position of the pane when displayed.
973
953
  ## Note: This is a known bug in wxWidgets 3.17 -- 3.20,
974
- ## and will be fixed in wxPython 4.2.1.
954
+ ## and will be fixed in wx ver 4.2.1.
975
955
  if wx.Display.GetFromWindow(pane.window) == -1:
976
956
  pane.floating_pos = wx.GetMousePosition()
977
957
 
978
958
  pane.Show(show)
979
959
  self._mgr.Update()
980
960
  return (show != shown)
981
-
961
+
982
962
  def update_pane(self, name, **props):
983
963
  """Update the layout of the pane (internal use only).
984
964
 
@@ -998,24 +978,36 @@ class Frame(mwx.Frame):
998
978
  pane.dock_direction = dock
999
979
  if not plug.caption:
1000
980
  pane.CaptionVisible(False) # no caption bar
1001
- 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
1002
982
  pane.Dockable(dock)
1003
-
1004
- def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
983
+
984
+ if pane.dock_direction:
985
+ pane.Dock()
986
+ else:
987
+ pane.Float()
988
+
989
+ def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
1005
990
  pane = evt.GetPane()
1006
991
  win = pane.window
1007
992
  if isinstance(win, aui.AuiNotebook):
1008
- for plug in win.all_pages:
993
+ for plug in win.get_pages():
1009
994
  plug.handler('page_closed', plug)
1010
995
  else:
1011
996
  win.handler('page_closed', win)
1012
- evt.Skip(False) # Don't skip to avoid being called twice.
1013
-
997
+ evt.Skip(False) # Don't skip to avoid being called twice.
998
+
1014
999
  ## --------------------------------
1015
- ## Plugin <Layer> interface
1000
+ ## Plugin <Layer> interface.
1016
1001
  ## --------------------------------
1017
1002
  plugins = property(lambda self: self.__plugins)
1018
-
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
+
1019
1011
  def require(self, name):
1020
1012
  """Get named plug window.
1021
1013
  If not found, try to load it once.
@@ -1029,183 +1021,159 @@ class Frame(mwx.Frame):
1029
1021
  if self.load_plug(name) is not False:
1030
1022
  return self.get_plug(name)
1031
1023
  return plug
1032
-
1024
+
1033
1025
  def get_plug(self, name):
1034
- """Get named plug window.
1035
-
1036
- Args:
1037
- name : str or plug object.
1038
- """
1026
+ """Get named plug window."""
1039
1027
  if isinstance(name, str):
1040
1028
  if name.endswith(".py"):
1041
1029
  name, _ = os.path.splitext(os.path.basename(name))
1042
1030
  if name in self.plugins:
1043
1031
  return self.plugins[name].__plug__
1044
1032
  elif isinstance(name, LayerInterface):
1045
- return name
1046
-
1047
- @staticmethod
1048
- def register(cls, module=None):
1049
- """Register dummy plug; Add module.Plugin <Layer>.
1050
- """
1051
- if not module:
1052
- module = inspect.getmodule(cls) # rebase module or __main__
1053
-
1054
- if issubclass(cls, LayerInterface):
1055
- cls.__module__ = module.__name__ # __main__ to module
1056
- warn(f"Duplicate iniheritance of LayerInterface by {cls}.")
1057
- module.Plugin = cls
1058
- return cls
1059
-
1060
- class _Plugin(LayerInterface, cls):
1061
- def __init__(self, parent, session=None, **kwargs):
1062
- cls.__init__(self, parent, **kwargs)
1063
- LayerInterface.__init__(self, parent, session)
1064
-
1065
- _Plugin.__module__ = cls.__module__ = module.__name__
1066
- _Plugin.__name__ = cls.__name__ + str("~")
1067
- _Plugin.__doc__ = cls.__doc__
1068
- module.Plugin = _Plugin
1069
- return _Plugin
1070
-
1071
- def load_module(self, root):
1072
- """Load module of plugin (internal use only).
1073
-
1074
- Note:
1075
- This is called automatically from load_plug,
1076
- and should not be called directly from user.
1077
- """
1078
- dirname_, name = split_paths(root)
1079
-
1080
- ## Update the include-path to load the module correctly.
1081
- if os.path.isdir(dirname_):
1082
- if dirname_ in sys.path:
1083
- sys.path.remove(dirname_)
1084
- sys.path.insert(0, dirname_)
1085
- elif dirname_:
1086
- print("- No such directory {!r}".format(dirname_))
1087
- return False
1088
-
1089
- try:
1090
- if name in sys.modules:
1091
- module = reload(sys.modules[name])
1092
- else:
1093
- module = import_module(name)
1094
- except Exception as e:
1095
- print(f"- Unable to load {root!r}.", e)
1096
- return False
1097
-
1098
- ## the module must have a class `Plugin`.
1099
- if not hasattr(module, 'Plugin'):
1100
- if isinstance(root, type):
1101
- warn(f"Use dummy plug for debugging {name!r}.")
1102
- module.__dummy_plug__ = root
1103
- self.register(root, module)
1104
- else:
1105
- if hasattr(module, '__dummy_plug__'):
1106
- root = module.__dummy_plug__ # old class (imported)
1107
- cls = getattr(module, root.__name__) # new class (reloaded)
1108
- self.register(cls, module)
1109
- return module
1110
-
1111
- def load_plug(self, root, force=False, session=None, show=False,
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,
1112
1041
  dock=0, floating_pos=None, floating_size=None,
1113
1042
  **kwargs):
1114
1043
  """Load plugin.
1115
1044
 
1116
1045
  Args:
1117
- root : Plugin <Layer> module, or name of the module.
1118
- Any wx.Window object can be specified (as dummy-plug).
1119
- However, do not use this mode in release versions.
1120
- force : force loading even if it is already loaded
1121
- session : Conditions for initializing the plug and starting session
1122
- show : the pane is shown after loaded
1123
- dock : dock_direction (1:top, 2:right, 3:bottom, 4:left, 5:center)
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)
1124
1053
  floating_pos: posision of floating window
1125
1054
  floating_size: size of floating window
1126
-
1127
1055
  **kwargs: keywords for Plugin <Layer>
1128
1056
 
1129
1057
  Returns:
1130
1058
  None if succeeded else False
1131
1059
 
1132
1060
  Note:
1133
- The root module must have a class Plugin <Layer>
1061
+ The root module must contain a class Plugin <Layer>.
1134
1062
  """
1135
1063
  props = dict(dock_direction=dock,
1136
1064
  floating_pos=floating_pos,
1137
1065
  floating_size=floating_size)
1138
1066
 
1139
- _dirname, name = split_paths(root)
1067
+ if inspect.ismodule(root):
1068
+ name = root.__name__
1069
+ elif inspect.isclass(root):
1070
+ name = root.__module__
1071
+ else:
1072
+ name = root
1140
1073
 
1141
- plug = self.get_plug(name)
1142
- if plug and not force:
1143
- self.update_pane(name, **props)
1144
- self.show_pane(name, show)
1145
- try:
1146
- if session:
1147
- plug.load_session(session)
1148
- except Exception:
1149
- traceback.print_exc()
1150
- print("- Failed to load session of", plug)
1151
- return None
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
1095
+
1096
+ ## Update the include-path to load the module correctly.
1097
+ if os.path.isdir(dirname_):
1098
+ if dirname_ in sys.path:
1099
+ sys.path.remove(dirname_)
1100
+ sys.path.insert(0, dirname_)
1101
+ elif dirname_:
1102
+ print(f"- No such directory {dirname_!r}.")
1103
+ return False
1152
1104
 
1153
- module = self.load_module(root)
1154
- if not module:
1155
- return False # failed to import
1105
+ ## Load or reload the module, and check whether it contains a class named `Plugin`.
1106
+ try:
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:
1113
+ module = reload(sys.modules[name])
1114
+ else:
1115
+ module = import_module(name)
1116
+ except Exception:
1117
+ traceback.print_exc() # Unable to load the module.
1118
+ return False
1119
+ else:
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)
1156
1131
 
1132
+ ## Note: name (module.__name__) != Plugin.__module__ if module is a package.
1157
1133
  try:
1158
- name = module.Plugin.__module__
1159
- title = module.Plugin.category
1160
-
1161
- pane = self._mgr.GetPane(title)
1162
-
1163
- if pane.IsOk(): # <pane:title> is already registered
1164
- nb = pane.window
1165
- if not isinstance(nb, aui.AuiNotebook):
1166
- raise NameError("Notebook name must not be the same as any other plugins")
1134
+ Plugin = module.Plugin # Check if the module has a class `Plugin`.
1135
+ title = Plugin.category # Plugin <LayerInterface>
1167
1136
 
1168
- 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")
1169
1141
 
1170
- 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():
1171
1144
  if name not in self.plugins:
1172
- raise NameError("Plugin name must not be the same as any other panes")
1173
-
1174
- show = show or pane.IsShown()
1175
- props.update(
1176
- dock_direction = pane.IsDocked() and pane.dock_direction,
1177
- floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
1178
- floating_size = floating_size or pane.floating_size[:], # copy unloading pane
1179
- )
1145
+ raise NameError("Plugin name must not be the same as any other pane")
1146
+
1180
1147
  except (AttributeError, NameError) as e:
1181
1148
  traceback.print_exc()
1182
- wx.CallAfter(wx.MessageBox,
1149
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1183
1150
  f"{e}\n\n" + traceback.format_exc(),
1184
1151
  f"Error in loading {module.__name__!r}",
1185
1152
  style=wx.ICON_ERROR)
1186
1153
  return False
1187
1154
 
1188
- ## Create and register the plugin
1155
+ ## Unload the plugin if loaded.
1189
1156
  if pane.IsOk():
1190
- 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)
1191
1164
 
1165
+ ## Create the plugin object.
1192
1166
  try:
1193
- plug = module.Plugin(self, session, **kwargs)
1167
+ plug = Plugin(self, session, **kwargs)
1194
1168
  except Exception as e:
1195
1169
  traceback.print_exc()
1196
- wx.CallAfter(wx.MessageBox,
1170
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1197
1171
  f"{e}\n\n" + traceback.format_exc(),
1198
1172
  f"Error in loading {name!r}",
1199
1173
  style=wx.ICON_ERROR)
1200
1174
  return False
1201
1175
 
1202
- ## Add to the list after the plug is created successfully.
1203
- self.plugins[name] = module
1204
-
1205
- ## set reference of a plug (one module, one plugin)
1206
- module.__plug__ = plug
1207
-
1208
- ## Create pane or notebook pane
1176
+ ## Create pane or notebook pane.
1209
1177
  caption = plug.caption
1210
1178
  if not isinstance(caption, str):
1211
1179
  caption = name
@@ -1217,7 +1185,7 @@ class Frame(mwx.Frame):
1217
1185
  nb = pane.window
1218
1186
  nb.AddPage(plug, caption)
1219
1187
  else:
1220
- size = plug.GetSize() + (2,30) # padding for notebook
1188
+ size = plug.GetSize() + (2,30) # padding for notebook
1221
1189
  nb = AuiNotebook(self, name=title)
1222
1190
  nb.AddPage(plug, caption)
1223
1191
  self._mgr.AddPane(nb, aui.AuiPaneInfo()
@@ -1233,17 +1201,23 @@ class Frame(mwx.Frame):
1233
1201
  .Name(name).Caption(caption)
1234
1202
  .FloatingSize(size).MinSize(size).Show(0))
1235
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
+
1236
1210
  ## Set winow.Name for inspection.
1237
1211
  plug.Name = name
1238
1212
 
1239
1213
  self.update_pane(name, **props)
1240
1214
  self.show_pane(name, show)
1241
1215
 
1242
- ## Create a menu
1216
+ ## Create a menu.
1243
1217
  plug.__Menu_item = None
1244
1218
 
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)]
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)]
1247
1221
  try:
1248
1222
  __plug_ID__
1249
1223
  except NameError:
@@ -1258,26 +1232,26 @@ class Frame(mwx.Frame):
1258
1232
  hint = (plug.__doc__ or name).strip().splitlines()[0]
1259
1233
  plug.__Menu_item = (
1260
1234
  module.ID_, text, hint, wx.ITEM_CHECK,
1261
- 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)),
1262
1237
  lambda v: v.Check(plug.IsShown()),
1263
1238
  )
1264
1239
  if menu not in self.menubar:
1265
1240
  self.menubar[menu] = []
1266
1241
  self.menubar[menu] += [plug.__Menu_item]
1267
1242
  self.menubar.update(menu)
1243
+
1244
+ self.handler('plug_loaded', plug)
1268
1245
  return None
1269
-
1246
+
1270
1247
  def unload_plug(self, name):
1271
1248
  """Unload plugin and detach the pane from UI manager."""
1272
1249
  plug = self.get_plug(name)
1273
1250
  if not plug:
1251
+ print(f"- {name!r} is not listed in plugins.")
1274
1252
  return
1275
1253
 
1276
- name = plug.__module__
1277
- if name not in self.plugins:
1278
- return
1279
-
1280
- del self.plugins[name]
1254
+ del self.plugins[plug.Name]
1281
1255
 
1282
1256
  if plug.__Menu_item:
1283
1257
  menu, sep, tail = plug.menukey.rpartition('/')
@@ -1288,91 +1262,87 @@ class Frame(mwx.Frame):
1288
1262
  if isinstance(plug.Parent, aui.AuiNotebook):
1289
1263
  nb = plug.Parent
1290
1264
  j = nb.GetPageIndex(plug)
1291
- nb.RemovePage(j) # just remove page
1292
- ## nb.DeletePage(j) # Destroys plug object too.
1265
+ nb.RemovePage(j) # just remove page
1266
+ # nb.DeletePage(j) # Destroys plug object too.
1293
1267
  else:
1294
1268
  nb = None
1295
1269
  self._mgr.DetachPane(plug)
1296
1270
  self._mgr.Update()
1297
1271
 
1298
- 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)
1299
1274
  plug.Destroy()
1300
1275
 
1301
1276
  if nb and not nb.PageCount:
1302
- self._mgr.DetachPane(nb) # detach notebook pane
1277
+ self._mgr.DetachPane(nb) # detach notebook pane
1303
1278
  self._mgr.Update()
1304
1279
  nb.Destroy()
1305
-
1280
+
1306
1281
  def reload_plug(self, name):
1282
+ """Reload plugin."""
1307
1283
  plug = self.get_plug(name)
1308
- if not plug or not plug.reloadable:
1284
+ if not plug:
1285
+ print(f"- {name!r} is not listed in plugins.")
1309
1286
  return
1287
+
1310
1288
  session = {}
1311
1289
  try:
1312
- print("Reloading {}...".format(plug))
1290
+ print(f"Reloading {plug}...")
1313
1291
  plug.save_session(session)
1314
1292
  except Exception:
1315
- traceback.print_exc()
1316
- print("- Failed to save session of", plug)
1293
+ traceback.print_exc() # Failed to save the plug session.
1294
+
1317
1295
  self.load_plug(plug.__module__, force=1, session=session)
1318
1296
 
1319
- ## Update shell.target --> new plug
1320
- for shell in self.shellframe.all_shells:
1297
+ ## Update shell.target --> new plug.
1298
+ for shell in self.shellframe.get_all_shells():
1321
1299
  if shell.target is plug:
1322
1300
  shell.handler('shell_activated', shell)
1323
-
1324
- @ignore(ResourceWarning)
1325
- def edit_plug(self, name):
1326
- plug = self.get_plug(name)
1327
- if not plug:
1328
- return
1329
-
1330
- Popen([self.EDITOR, inspect.getmodule(plug).__file__])
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