mwxlib 1.0.0__py3-none-any.whl → 1.8.0__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
@@ -96,28 +81,24 @@ class Thread:
96
81
  try:
97
82
  self.handler = self.owner.handler
98
83
  except AttributeError:
99
- self.handler = FSM({ # DNA<Thread>
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
- self.handler.append({ # DNA<Layer>
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.
@@ -477,7 +462,7 @@ class LayerInterface(CtrlInterface):
477
462
  if canvas:
478
463
  canvas.draw_idle()
479
464
  except Exception as e:
480
- print(f"- Failed to draw Arts of {self.__module__}.", e)
465
+ print(f"- Failed to draw Arts of {self.__module__};", e)
481
466
  del self.Arts
482
467
 
483
468
 
@@ -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)
@@ -502,77 +508,81 @@ class Graph(GraphPlot):
502
508
  self.parent = parent
503
509
  self.loader = loader or parent
504
510
 
505
- self.handler.append({ # DNA<Graph>
511
+ self.handler.append({ # DNA<Graph>
506
512
  None : {
507
- 'focus_set' : [ None, _F(self.loader.select_view, view=self) ],
508
- 'page_shown' : [ None, ],
509
- 'page_closed' : [ None, ],
510
- 'frame_shown' : [ None, _F(self.update_infobar) ],
511
- 'S-a pressed' : [ None, _F(self.toggle_infobar) ],
512
- 'f5 pressed' : [ None, _F(self.refresh) ],
513
+ 'focus_set' : [None, _F(self.loader.select_view, view=self)],
514
+ 'page_shown' : [None, ],
515
+ 'page_closed' : [None, ],
516
+ 'frame_shown' : [None, _F(self.update_infobar)],
517
+ 'S-a pressed' : [None, _F(self.toggle_infobar)],
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
 
550
+ ## --------------------------------
551
+ ## Overridden buffer methods.
552
+ ## --------------------------------
553
+
554
+ def kill_all_buffers(self):
555
+ """Delete all buffers; (override) confirm the action with a dialog."""
556
+ n = sum(not frame.pathname for frame in self.all_frames) # Check *need-save* frames.
557
+ if n:
558
+ s = 's' if n > 1 else ''
559
+ if wx.MessageBox( # Confirm closing the frame.
560
+ f"You are closing {n} unsaved frame{s}.\n\n"
561
+ "Continue closing?",
562
+ style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
563
+ self.message("The close has been canceled.")
564
+ return None
565
+ del self[:]
566
+
557
567
 
558
568
  class MyFileDropLoader(wx.FileDropTarget):
559
- """File Drop interface
569
+ """File Drop interface.
560
570
 
561
571
  Args:
562
- target : target view to drop in, e.g. frame, graph, pane, etc.
563
- loader : mainframe
572
+ target: target view to drop in, e.g. frame, graph, pane, etc.
573
+ loader: mainframe
564
574
  """
565
575
  def __init__(self, target, loader):
566
576
  wx.FileDropTarget.__init__(self)
567
577
 
568
578
  self.view = target
569
579
  self.loader = loader
570
-
580
+
571
581
  def OnDropFiles(self, x, y, filenames):
572
- pos = self.view.ScreenPosition + (x,y)
582
+ pos = self.view.ScreenPosition + (x, y)
573
583
  paths = []
574
584
  for fn in filenames:
575
- name, ext = os.path.splitext(fn)
585
+ _name, ext = os.path.splitext(fn)
576
586
  if ext == '.py' or os.path.isdir(fn):
577
587
  self.loader.load_plug(fn, show=1,
578
588
  floating_pos=pos,
@@ -580,9 +590,9 @@ class MyFileDropLoader(wx.FileDropTarget):
580
590
  elif ext == '.jssn':
581
591
  self.loader.load_session(fn)
582
592
  elif ext == '.index':
583
- self.loader.load_index(fn, self.view)
593
+ self.loader.import_index(fn, self.view)
584
594
  else:
585
- paths.append(fn) # image file just stacks to be loaded
595
+ paths.append(fn) # image file just stacks to be loaded
586
596
  if paths:
587
597
  self.loader.load_frame(paths, self.view)
588
598
  return True
@@ -600,24 +610,24 @@ class Frame(mwx.Frame):
600
610
  graph = property(lambda self: self.__graph)
601
611
  output = property(lambda self: self.__output)
602
612
  histogram = property(lambda self: self.__histgrm)
603
-
613
+
604
614
  selected_view = property(lambda self: self.__view)
605
-
615
+
606
616
  def select_view(self, view):
607
617
  self.__view = view
608
618
  self.set_title(view.frame)
609
-
619
+
610
620
  @property
611
621
  def graphic_windows(self):
612
622
  """Graphic windows list.
613
623
  [0] graph [1] output [2:] others(user-defined)
614
624
  """
615
625
  return self.__graphic_windows
616
-
626
+
617
627
  @property
618
628
  def graphic_windows_on_screen(self):
619
629
  return [w for w in self.__graphic_windows if w.IsShownOnScreen()]
620
-
630
+
621
631
  def __init__(self, *args, **kwargs):
622
632
  mwx.Frame.__init__(self, *args, **kwargs)
623
633
 
@@ -625,7 +635,7 @@ class Frame(mwx.Frame):
625
635
  self._mgr.SetManagedWindow(self)
626
636
  self._mgr.SetDockSizeConstraint(0.5, 0.5)
627
637
 
628
- self.__plugins = {} # modules in the order of load/save
638
+ self.__plugins = {} # modules in the order of load/save
629
639
 
630
640
  self.__graph = Graph(self, log=self.message, margin=None)
631
641
  self.__output = Graph(self, log=self.message, margin=None)
@@ -646,7 +656,7 @@ class Frame(mwx.Frame):
646
656
  self.histogram.Name = "histogram"
647
657
 
648
658
  self._mgr.AddPane(self.graph,
649
- aui.AuiPaneInfo().CenterPane().CloseButton(1)
659
+ aui.AuiPaneInfo().CenterPane()
650
660
  .Name("graph").Caption("graph").CaptionVisible(1))
651
661
 
652
662
  size = (200, 200)
@@ -670,26 +680,26 @@ class Frame(mwx.Frame):
670
680
  lambda v: v.Enable(self.__view.frame is not None)),
671
681
 
672
682
  (wx.ID_CLOSE_ALL, "&Close all\t(C-S-k)", "Kill all buffers", Icon('book_red'),
673
- lambda v: self.__view.kill_buffer_all(),
683
+ lambda v: self.__view.kill_all_buffers(),
674
684
  lambda v: v.Enable(self.__view.frame is not None)),
675
685
 
676
686
  (wx.ID_SAVE, "&Save as\tCtrl-s", "Save buffer as", Icon('save'),
677
687
  lambda v: self.save_frame(),
678
688
  lambda v: v.Enable(self.__view.frame is not None)),
679
689
 
680
- (wx.ID_SAVEAS, "&Save as TIFFs", "Save buffers as a multi-page tiff", Icon('saveall'),
681
- lambda v: self.save_buffers_as_tiffs(),
690
+ (wx.ID_SAVEAS, "&Save as TIFFs\tCtrl+Shift+s", "Save buffers as a multi-page tiff", Icon('saveall'),
691
+ lambda v: self.save_frames_as_tiff(),
682
692
  lambda v: v.Enable(self.__view.frame is not None)),
683
693
  (),
684
- ("Index", (
685
- (mwx.ID_(11), "&Import index\tCtrl+Shift+o", "Import index file", Icon('open'),
686
- lambda v: self.load_index()),
687
-
688
- (mwx.ID_(12), "&Export index\tCtrl+Shift+s", "Export index file", Icon('saveas'),
689
- lambda v: self.save_index(),
690
- lambda v: v.Enable(self.__view.frame is not None)),
691
- )),
692
- (),
694
+ # ("Index", (
695
+ # (mwx.ID_(11), "&Import index", "Import index file", Icon('open'),
696
+ # lambda v: self.import_index()),
697
+ #
698
+ # (mwx.ID_(12), "&Export index", "Export index file", Icon('saveas'),
699
+ # lambda v: self.export_index(),
700
+ # lambda v: v.Enable(self.__view.frame is not None)),
701
+ # )),
702
+ # (),
693
703
  ("Session", (
694
704
  (mwx.ID_(15), "&Open session", "Open session file",
695
705
  lambda v: self.load_session()),
@@ -701,7 +711,7 @@ class Frame(mwx.Frame):
701
711
  lambda v: self.save_session_as()),
702
712
  )),
703
713
  (),
704
- ("Options", []), # reserved for optional app settings
714
+ ("Options", []), # reserved for optional app settings
705
715
  (),
706
716
  (mwx.ID_(13), "&Graph window\tF9", "Show graph window", wx.ITEM_CHECK,
707
717
  lambda v: self.show_pane(self.graph, v.IsChecked()),
@@ -719,44 +729,37 @@ class Frame(mwx.Frame):
719
729
  (wx.ID_PASTE, "&Paste\t(C-v)", "Paste buffer from clipboard", Icon('paste'),
720
730
  lambda v: self.__view.read_buffer_from_clipboard()),
721
731
  (),
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'),
732
+ (mwx.ID_(23), "Hide all &layers", "Hide all layers", Icon('xr'),
730
733
  lambda v: self.__view.hide_layers()),
731
734
  (),
732
- (mwx.ID_(24), "&Histogram\tCtrl-h", "Show Histogram window", wx.ITEM_CHECK,
735
+ (mwx.ID_(24), "&Histogram\tCtrl-h", "Show histogram window", wx.ITEM_CHECK,
733
736
  lambda v: self.show_pane(self.histogram, v.IsChecked()),
734
737
  lambda v: v.Check(self.histogram.IsShownOnScreen())),
735
738
 
736
- (mwx.ID_(25), "&Invert Color\t(C-i)", "Invert colormap", wx.ITEM_CHECK,
739
+ (mwx.ID_(25), "&Invert color\t(C-i)", "Invert colormap", wx.ITEM_CHECK,
737
740
  lambda v: self.__view.invert_cmap(),
738
- lambda v: v.Check(self.__view.get_cmap()[-2:] == "_r")),
741
+ lambda v: v.Check(self.__view.get_cmapstr()[-2:] == "_r")),
739
742
  ]
740
743
 
741
744
  def _cmenu(i, name):
742
745
  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"),
746
+ lambda v: self.__view.set_cmapstr(name),
747
+ lambda v: v.Check(self.__view.get_cmapstr() == name
748
+ or self.__view.get_cmapstr() == name+"_r"),
746
749
  )
747
750
  colours = [c for c in dir(cm) if c[-2:] != "_r"
748
751
  and isinstance(getattr(cm, c), colors.LinearSegmentedColormap)]
749
752
 
750
753
  self.menubar["Edit"] += [
751
754
  (),
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",
755
+ # (mwx.ID_(26), "Default Color", "gray", wx.ITEM_CHECK,
756
+ # lambda v: self.__view.set_cmapstr('gray'),
757
+ # lambda v: v.Check(self.__view.get_cmapstr()[:4] == "gray")),
758
+ #
759
+ ("Standard colors",
757
760
  [_cmenu(i, c) for i, c in enumerate(colours) if c.islower()]),
758
761
 
759
- ("Other Colors",
762
+ ("Other colors",
760
763
  [_cmenu(i, c) for i, c in enumerate(colours) if not c.islower()]),
761
764
  ]
762
765
 
@@ -771,30 +774,31 @@ class Frame(mwx.Frame):
771
774
  self.menubar.reset()
772
775
 
773
776
  def show_frameview(frame):
774
- wx.CallAfter(self.show_pane, frame.parent) # Show graph / output
777
+ if not frame.parent.IsShown():
778
+ wx.CallAfter(self.show_pane, frame.parent)
775
779
 
776
- self.graph.handler.append({ # DNA<Graph:Frame>
780
+ self.graph.handler.append({ # DNA<Graph:Frame>
777
781
  None : {
778
- 'frame_shown' : [ None, self.set_title ],
779
- 'frame_loaded' : [ None, show_frameview ],
780
- 'frame_modified' : [ None, show_frameview ],
781
- 'frame_selected' : [ None, self.set_title ],
782
- 'canvas_draw' : [ None, lambda v: self.sync(self.graph, self.output) ],
782
+ 'frame_shown' : [None, self.set_title],
783
+ 'frame_loaded' : [None, show_frameview],
784
+ 'frame_modified' : [None, show_frameview],
785
+ 'frame_selected' : [None, self.set_title],
786
+ 'canvas_draw' : [None, lambda v: self.sync(self.graph, self.output)],
783
787
  },
784
788
  })
785
- self.output.handler.append({ # DNA<Graph:Frame>
789
+ self.output.handler.append({ # DNA<Graph:Frame>
786
790
  None : {
787
- 'frame_shown' : [ None, self.set_title ],
788
- 'frame_loaded' : [ None, show_frameview ],
789
- 'frame_modified' : [ None, show_frameview ],
790
- 'frame_selected' : [ None, self.set_title ],
791
- 'canvas_draw' : [ None, lambda v: self.sync(self.output, self.graph) ],
791
+ 'frame_shown' : [None, self.set_title],
792
+ 'frame_loaded' : [None, show_frameview],
793
+ 'frame_modified' : [None, show_frameview],
794
+ 'frame_selected' : [None, self.set_title],
795
+ 'canvas_draw' : [None, lambda v: self.sync(self.output, self.graph)],
792
796
  },
793
797
  })
794
798
 
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]
799
+ ## Add main-menu to context-menu.
800
+ self.graph.menu += self.menubar["Edit"][2:4]
801
+ self.output.menu += self.menubar["Edit"][2:4]
798
802
 
799
803
  self._mgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
800
804
 
@@ -810,10 +814,10 @@ class Frame(mwx.Frame):
810
814
  _display(self.graph, show)
811
815
  _display(self.output, show)
812
816
  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))
817
+ self.Bind(wx.EVT_MOVE_START, lambda v: on_move(v, show=0))
818
+ self.Bind(wx.EVT_MOVE_END, lambda v: on_move(v, show=1))
815
819
 
816
- ## Custom Key Bindings
820
+ ## Custom Key Bindings.
817
821
  self.define_key('* C-g', self.Quit)
818
822
 
819
823
  @self.shellframe.define_key('* C-g')
@@ -821,36 +825,33 @@ class Frame(mwx.Frame):
821
825
  """Dispatch quit to the main Frame."""
822
826
  self.handler('C-g pressed', evt)
823
827
 
824
- ## Accepts DnD
828
+ ## Accepts DnD.
825
829
  self.SetDropTarget(MyFileDropLoader(self.graph, self))
826
-
827
- ## Script editor for plugins (external call)
828
- EDITOR = "notepad"
829
-
830
+
830
831
  SYNC_SWITCH = True
831
-
832
+
832
833
  def sync(self, a, b):
833
834
  """Synchronize b to a."""
834
835
  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
-
836
+ and a.frame and b.frame
837
+ and a.frame.unit == b.frame.unit
838
+ and a.buffer.shape == b.buffer.shape):
839
+ b.xlim = a.xlim
840
+ b.ylim = a.ylim
841
+ b.OnDraw(None)
842
+ b.canvas.draw_idle()
843
+
843
844
  def set_title(self, frame):
844
845
  ssn = os.path.basename(self.session_file or '--')
845
846
  ssn, _ = os.path.splitext(ssn)
846
847
  name = (frame.pathname or frame.name) if frame else ''
847
848
  self.SetTitle("{}@{} - [{}] {}".format(self.Name, platform.node(), ssn, name))
848
-
849
- def OnActivate(self, evt): #<wx._core.ActivateEvent>
849
+
850
+ def OnActivate(self, evt): # <wx._core.ActivateEvent>
850
851
  if self and evt.Active:
851
852
  self.set_title(self.selected_view.frame)
852
-
853
- def OnClose(self, evt): #<wx._core.CloseEvent>
853
+
854
+ def OnClose(self, evt): # <wx._core.CloseEvent>
854
855
  ssn = os.path.basename(self.session_file or '--')
855
856
  with wx.MessageDialog(None,
856
857
  "Do you want to save session before closing program?",
@@ -862,66 +863,69 @@ class Frame(mwx.Frame):
862
863
  elif ret == wx.ID_CANCEL:
863
864
  evt.Veto()
864
865
  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
866
+ n = sum(bool(plug.thread and plug.thread.active) for plug in self.get_all_plugs())
867
+ if n:
868
+ s = 's' if n > 1 else ''
869
+ if wx.MessageBox( # Confirm closing the thread.
870
+ f"Currently running {n} thread{s}.\n\n"
871
+ "Continue closing?",
872
+ style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
873
+ self.message("The close has been canceled.")
874
+ evt.Veto()
875
+ return
876
+ self.Quit()
877
+ n = sum(not frame.pathname for frame in self.graph.all_frames) # Check *need-save* frames.
878
+ if n:
879
+ s = 's' if n > 1 else ''
880
+ if wx.MessageBox( # Confirm closing the frame.
881
+ f"You are closing {n} unsaved frame{s}.\n\n"
882
+ "Continue closing?",
883
+ style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
884
+ self.message("The close has been canceled.")
885
+ evt.Veto()
886
+ return
890
887
  evt.Skip()
891
-
888
+
892
889
  def Destroy(self):
893
- ## for name in list(self.plugins):
894
- ## self.unload_plug(name) # => plug.Destroy
895
890
  self._mgr.UnInit()
896
891
  return mwx.Frame.Destroy(self)
897
-
892
+
898
893
  ## --------------------------------
899
- ## pane window interface
894
+ ## pane window interface.
900
895
  ## --------------------------------
901
-
896
+
902
897
  def get_pane(self, name):
903
898
  """Get named pane or notebook pane.
904
899
 
905
900
  Args:
906
- name : str or plug object.
901
+ name: plug name or object.
907
902
  """
908
903
  plug = self.get_plug(name)
909
904
  if plug:
910
905
  name = plug.category or plug
911
906
  if name:
912
907
  return self._mgr.GetPane(name)
913
-
908
+
914
909
  def show_pane(self, name, show=True, interactive=False):
915
- """Show named pane or notebook pane."""
910
+ """Show named pane or notebook pane.
911
+
912
+ Args:
913
+ name: plug name or object.
914
+ show: Show or hide the pane window.
915
+ interactive: If True, modifier keys can be used to reset or reload
916
+ the plugin when showing the pane:
917
+ - [S-menu] Reset floating position.
918
+ - [M-S-menu] Reload plugin.
919
+ """
916
920
  pane = self.get_pane(name)
917
921
  if not pane.IsOk():
918
922
  return
919
923
 
920
924
  ## Set the graph and output window sizes to half & half.
921
- ## ドッキング時に再計算される
925
+ ## ドッキング時に再計算される.
922
926
  if name == "output" or name is self.output:
923
927
  w, h = self.graph.GetClientSize()
924
- pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
928
+ pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
925
929
 
926
930
  ## Force Layer windows to show.
927
931
  if interactive:
@@ -937,12 +941,8 @@ class Frame(mwx.Frame):
937
941
  pane.Float()
938
942
  show = True
939
943
 
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)
944
+ plug = self.get_plug(name) # -> None if pane.window is a Graph
945
+ win = pane.window
946
946
  try:
947
947
  shown = plug.IsShown()
948
948
  except AttributeError:
@@ -952,33 +952,30 @@ class Frame(mwx.Frame):
952
952
  if isinstance(win, aui.AuiNotebook):
953
953
  j = win.GetPageIndex(plug)
954
954
  if j != win.Selection:
955
- win.Selection = j # the focus moves => EVT_SHOW
955
+ win.Selection = j # the focus moves => EVT_SHOW
956
956
  else:
957
957
  plug.handler('page_shown', plug)
958
958
  else:
959
959
  win.handler('page_shown', win)
960
+ if plug:
961
+ plug.SetFocus() # plugins only
960
962
  elif not show and shown:
961
963
  if isinstance(win, aui.AuiNotebook):
962
- for plug in win.all_pages:
964
+ for plug in win.get_pages():
963
965
  plug.handler('page_closed', plug)
964
966
  else:
965
967
  win.handler('page_closed', win)
966
968
 
967
- if pane.dock_direction:
968
- pane.Dock()
969
- else:
970
- pane.Float()
971
-
972
969
  ## Modify the floating position of the pane when displayed.
973
970
  ## Note: This is a known bug in wxWidgets 3.17 -- 3.20,
974
- ## and will be fixed in wxPython 4.2.1.
971
+ ## and will be fixed in wx ver 4.2.1.
975
972
  if wx.Display.GetFromWindow(pane.window) == -1:
976
973
  pane.floating_pos = wx.GetMousePosition()
977
974
 
978
975
  pane.Show(show)
979
976
  self._mgr.Update()
980
977
  return (show != shown)
981
-
978
+
982
979
  def update_pane(self, name, **props):
983
980
  """Update the layout of the pane (internal use only).
984
981
 
@@ -998,24 +995,36 @@ class Frame(mwx.Frame):
998
995
  pane.dock_direction = dock
999
996
  if not plug.caption:
1000
997
  pane.CaptionVisible(False) # no caption bar
1001
- pane.Gripper(dock not in (0, 5)) # show a grip when docked
998
+ pane.Gripper(dock not in (0,5)) # show a grip when docked
1002
999
  pane.Dockable(dock)
1003
-
1004
- def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
1000
+
1001
+ if pane.dock_direction:
1002
+ pane.Dock()
1003
+ else:
1004
+ pane.Float()
1005
+
1006
+ def OnPaneClose(self, evt): # <wx.aui.AuiManagerEvent>
1005
1007
  pane = evt.GetPane()
1006
1008
  win = pane.window
1007
1009
  if isinstance(win, aui.AuiNotebook):
1008
- for plug in win.all_pages:
1010
+ for plug in win.get_pages():
1009
1011
  plug.handler('page_closed', plug)
1010
1012
  else:
1011
1013
  win.handler('page_closed', win)
1012
- evt.Skip(False) # Don't skip to avoid being called twice.
1013
-
1014
+ evt.Skip(False) # Don't skip to avoid being called twice.
1015
+
1014
1016
  ## --------------------------------
1015
- ## Plugin <Layer> interface
1017
+ ## Plugin <Layer> interface.
1016
1018
  ## --------------------------------
1017
1019
  plugins = property(lambda self: self.__plugins)
1018
-
1020
+
1021
+ def register(self, cls=None, **kwargs):
1022
+ """Decorator of plugin class register."""
1023
+ if cls is None:
1024
+ return lambda f: self.register(f, **kwargs)
1025
+ self.load_plug(cls, force=1, show=1, **kwargs)
1026
+ return cls
1027
+
1019
1028
  def require(self, name):
1020
1029
  """Get named plug window.
1021
1030
  If not found, try to load it once.
@@ -1029,183 +1038,159 @@ class Frame(mwx.Frame):
1029
1038
  if self.load_plug(name) is not False:
1030
1039
  return self.get_plug(name)
1031
1040
  return plug
1032
-
1041
+
1033
1042
  def get_plug(self, name):
1034
- """Get named plug window.
1035
-
1036
- Args:
1037
- name : str or plug object.
1038
- """
1043
+ """Get named plug window."""
1039
1044
  if isinstance(name, str):
1040
1045
  if name.endswith(".py"):
1041
1046
  name, _ = os.path.splitext(os.path.basename(name))
1042
1047
  if name in self.plugins:
1043
1048
  return self.plugins[name].__plug__
1044
1049
  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,
1050
+ # return name
1051
+ return next((x for x in self.get_all_plugs() if x is name), None)
1052
+
1053
+ def get_all_plugs(self):
1054
+ for _name, module in self.plugins.items():
1055
+ yield module.__plug__
1056
+
1057
+ def load_plug(self, root, session=None, force=False, show=False,
1112
1058
  dock=0, floating_pos=None, floating_size=None,
1113
1059
  **kwargs):
1114
1060
  """Load plugin.
1115
1061
 
1116
1062
  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)
1063
+ root: Plugin <Layer> module, or name of the module.
1064
+ Any wx.Window object can be specified (as dummy-plug).
1065
+ However, do not use this mode in release versions.
1066
+ session: Conditions for initializing the plug and starting session
1067
+ force: force loading even if it is already loaded
1068
+ show: the pane is shown after loaded
1069
+ dock: dock_direction (1:top, 2:right, 3:bottom, 4:left, 5:center)
1124
1070
  floating_pos: posision of floating window
1125
1071
  floating_size: size of floating window
1126
-
1127
1072
  **kwargs: keywords for Plugin <Layer>
1128
1073
 
1129
1074
  Returns:
1130
1075
  None if succeeded else False
1131
1076
 
1132
1077
  Note:
1133
- The root module must have a class Plugin <Layer>
1078
+ The root module must contain a class Plugin <Layer>.
1134
1079
  """
1135
1080
  props = dict(dock_direction=dock,
1136
1081
  floating_pos=floating_pos,
1137
1082
  floating_size=floating_size)
1138
1083
 
1139
- _dirname, name = split_paths(root)
1084
+ if inspect.ismodule(root):
1085
+ name = root.__name__
1086
+ elif inspect.isclass(root):
1087
+ name = root.__module__
1088
+ else:
1089
+ name = root
1090
+
1091
+ dirname_, name = os.path.split(name) # if the name is full-path:str
1092
+ if name.endswith(".py"):
1093
+ name = name[:-3]
1140
1094
 
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
1095
+ if not force:
1096
+ ## 文字列参照 (full-path) による重複ロードを避ける.
1097
+ module = next((v for v in self.plugins.values() if root == v.__file__), None)
1098
+ if module:
1099
+ plug = module.__plug__
1100
+ else:
1101
+ plug = self.get_plug(name)
1102
+ ## Check if the named plug is already loaded.
1103
+ if plug:
1104
+ self.update_pane(name, **props)
1105
+ self.show_pane(name, show)
1106
+ try:
1107
+ if session:
1108
+ plug.load_session(session)
1109
+ except Exception:
1110
+ traceback.print_exc() # Failed to load the plug session.
1111
+ return None
1112
+
1113
+ ## Update the include-path to load the module correctly.
1114
+ if os.path.isdir(dirname_):
1115
+ if dirname_ in sys.path:
1116
+ sys.path.remove(dirname_)
1117
+ sys.path.insert(0, dirname_)
1118
+ elif dirname_:
1119
+ print(f"- No such directory {dirname_!r}.")
1120
+ return False
1152
1121
 
1153
- module = self.load_module(root)
1154
- if not module:
1155
- return False # failed to import
1122
+ ## Load or reload the module, and check whether it contains a class named `Plugin`.
1123
+ try:
1124
+ ## Check if the module is reloadable.
1125
+ loadable = not name.startswith(("__main__", "builtins")) # no __file__
1126
+ if not loadable:
1127
+ module = types.ModuleType(name) # dummy module (cannot reload)
1128
+ module.__file__ = "<scratch>"
1129
+ elif name in sys.modules:
1130
+ module = reload(sys.modules[name])
1131
+ else:
1132
+ module = import_module(name)
1133
+ except Exception:
1134
+ traceback.print_exc() # Unable to load the module.
1135
+ return False
1136
+ else:
1137
+ ## Register dummy plug; Add module.Plugin <Layer>.
1138
+ if not hasattr(module, 'Plugin'):
1139
+ if inspect.isclass(root):
1140
+ module.__dummy_plug__ = root.__name__
1141
+ root.reloadable = loadable
1142
+ _register__dummy_plug__(root, module)
1143
+ else:
1144
+ if hasattr(module, '__dummy_plug__'):
1145
+ root = getattr(module, module.__dummy_plug__)
1146
+ root.reloadable = loadable
1147
+ _register__dummy_plug__(root, module)
1156
1148
 
1149
+ ## Note: name (module.__name__) != Plugin.__module__ if module is a package.
1157
1150
  try:
1158
- name = module.Plugin.__module__
1159
- title = module.Plugin.category
1151
+ Plugin = module.Plugin # Check if the module has a class `Plugin`.
1152
+ title = Plugin.category # Plugin <LayerInterface>
1160
1153
 
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")
1167
-
1168
- pane = self.get_pane(name)
1154
+ pane = self._mgr.GetPane(title) # Check if <pane:title> is already registered.
1155
+ if pane.IsOk():
1156
+ if not isinstance(pane.window, aui.AuiNotebook):
1157
+ raise NameError("Notebook name must not be the same as any other plugin")
1169
1158
 
1170
- if pane.IsOk(): # <pane:name> is already registered
1159
+ pane = self.get_pane(name) # Check if <pane:name> is already registered.
1160
+ if pane.IsOk():
1171
1161
  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
- )
1162
+ raise NameError("Plugin name must not be the same as any other pane")
1163
+
1180
1164
  except (AttributeError, NameError) as e:
1181
1165
  traceback.print_exc()
1182
- wx.CallAfter(wx.MessageBox,
1166
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1183
1167
  f"{e}\n\n" + traceback.format_exc(),
1184
1168
  f"Error in loading {module.__name__!r}",
1185
1169
  style=wx.ICON_ERROR)
1186
1170
  return False
1187
1171
 
1188
- ## Create and register the plugin
1172
+ ## Unload the plugin if loaded.
1189
1173
  if pane.IsOk():
1190
- self.unload_plug(name) # unload once right here
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
+ )
1180
+ self.unload_plug(name)
1191
1181
 
1182
+ ## Create the plugin object.
1192
1183
  try:
1193
- plug = module.Plugin(self, session, **kwargs)
1184
+ plug = Plugin(self, session, **kwargs)
1194
1185
  except Exception as e:
1195
1186
  traceback.print_exc()
1196
- wx.CallAfter(wx.MessageBox,
1187
+ wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
1197
1188
  f"{e}\n\n" + traceback.format_exc(),
1198
1189
  f"Error in loading {name!r}",
1199
1190
  style=wx.ICON_ERROR)
1200
1191
  return False
1201
1192
 
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
1193
+ ## Create pane or notebook pane.
1209
1194
  caption = plug.caption
1210
1195
  if not isinstance(caption, str):
1211
1196
  caption = name
@@ -1217,7 +1202,7 @@ class Frame(mwx.Frame):
1217
1202
  nb = pane.window
1218
1203
  nb.AddPage(plug, caption)
1219
1204
  else:
1220
- size = plug.GetSize() + (2,30) # padding for notebook
1205
+ size = plug.GetSize() + (2,30) # padding for notebook
1221
1206
  nb = AuiNotebook(self, name=title)
1222
1207
  nb.AddPage(plug, caption)
1223
1208
  self._mgr.AddPane(nb, aui.AuiPaneInfo()
@@ -1233,20 +1218,24 @@ class Frame(mwx.Frame):
1233
1218
  .Name(name).Caption(caption)
1234
1219
  .FloatingSize(size).MinSize(size).Show(0))
1235
1220
 
1221
+ ## Add to the list after the plug is created successfully.
1222
+ self.plugins[name] = module
1223
+
1224
+ ## Set reference of a plug (one module, one plugin).
1225
+ module.__plug__ = plug
1226
+
1236
1227
  ## Set winow.Name for inspection.
1237
1228
  plug.Name = name
1238
1229
 
1239
1230
  self.update_pane(name, **props)
1240
1231
  self.show_pane(name, show)
1241
1232
 
1242
- ## Create a menu
1233
+ ## Create a menu.
1243
1234
  plug.__Menu_item = None
1244
1235
 
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)]
1247
- try:
1248
- __plug_ID__
1249
- except NameError:
1236
+ if not hasattr(module, 'ID_'): # give a unique index to the module
1237
+ global __plug_ID__ # cache ID *not* in [ID_LOWEST(4999):ID_HIGHEST(5999)]
1238
+ if "__plug_ID__" not in globals():
1250
1239
  __plug_ID__ = 10000
1251
1240
  __plug_ID__ += 1
1252
1241
  module.ID_ = __plug_ID__
@@ -1258,26 +1247,26 @@ class Frame(mwx.Frame):
1258
1247
  hint = (plug.__doc__ or name).strip().splitlines()[0]
1259
1248
  plug.__Menu_item = (
1260
1249
  module.ID_, text, hint, wx.ITEM_CHECK,
1261
- lambda v: self.show_pane(name, v.IsChecked(), interactive=1),
1250
+ lambda v: (self.update_pane(name),
1251
+ self.show_pane(name, v.IsChecked(), interactive=1)),
1262
1252
  lambda v: v.Check(plug.IsShown()),
1263
1253
  )
1264
1254
  if menu not in self.menubar:
1265
1255
  self.menubar[menu] = []
1266
1256
  self.menubar[menu] += [plug.__Menu_item]
1267
1257
  self.menubar.update(menu)
1258
+
1259
+ self.handler('plug_loaded', plug)
1268
1260
  return None
1269
-
1261
+
1270
1262
  def unload_plug(self, name):
1271
1263
  """Unload plugin and detach the pane from UI manager."""
1272
1264
  plug = self.get_plug(name)
1273
1265
  if not plug:
1266
+ print(f"- {name!r} is not listed in plugins.")
1274
1267
  return
1275
1268
 
1276
- name = plug.__module__
1277
- if name not in self.plugins:
1278
- return
1279
-
1280
- del self.plugins[name]
1269
+ del self.plugins[plug.Name]
1281
1270
 
1282
1271
  if plug.__Menu_item:
1283
1272
  menu, sep, tail = plug.menukey.rpartition('/')
@@ -1288,92 +1277,88 @@ class Frame(mwx.Frame):
1288
1277
  if isinstance(plug.Parent, aui.AuiNotebook):
1289
1278
  nb = plug.Parent
1290
1279
  j = nb.GetPageIndex(plug)
1291
- nb.RemovePage(j) # just remove page
1292
- ## nb.DeletePage(j) # Destroys plug object too.
1280
+ nb.RemovePage(j) # just remove page
1281
+ # nb.DeletePage(j) # Destroys plug object too.
1293
1282
  else:
1294
1283
  nb = None
1295
1284
  self._mgr.DetachPane(plug)
1296
1285
  self._mgr.Update()
1297
1286
 
1298
- plug.handler('page_closed', plug) # (even if not shown)
1287
+ self.handler('plug_unloaded', plug)
1288
+ plug.handler('page_closed', plug) # (even if not shown)
1299
1289
  plug.Destroy()
1300
1290
 
1301
1291
  if nb and not nb.PageCount:
1302
- self._mgr.DetachPane(nb) # detach notebook pane
1292
+ self._mgr.DetachPane(nb) # detach notebook pane
1303
1293
  self._mgr.Update()
1304
1294
  nb.Destroy()
1305
-
1295
+
1306
1296
  def reload_plug(self, name):
1297
+ """Reload plugin."""
1307
1298
  plug = self.get_plug(name)
1308
- if not plug or not plug.reloadable:
1299
+ if not plug:
1300
+ print(f"- {name!r} is not listed in plugins.")
1309
1301
  return
1302
+
1310
1303
  session = {}
1311
1304
  try:
1312
- print("Reloading {}...".format(plug))
1305
+ print(f"Reloading {plug}...")
1313
1306
  plug.save_session(session)
1314
1307
  except Exception:
1315
- traceback.print_exc()
1316
- print("- Failed to save session of", plug)
1308
+ traceback.print_exc() # Failed to save the plug session.
1309
+
1317
1310
  self.load_plug(plug.__module__, force=1, session=session)
1318
1311
 
1319
- ## Update shell.target --> new plug
1320
- for shell in self.shellframe.all_shells:
1312
+ ## Update shell.target --> new plug.
1313
+ for shell in self.shellframe.get_all_shells():
1321
1314
  if shell.target is plug:
1322
1315
  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
-
1316
+
1332
1317
  def inspect_plug(self, name):
1333
- """Dive into the process to inspect plugs in the shell.
1334
- """
1318
+ """Dive into the process to inspect plugs in the shell."""
1335
1319
  plug = self.get_plug(name)
1336
1320
  if not plug:
1321
+ print(f"- {name!r} is not listed in plugins.")
1337
1322
  return
1338
1323
 
1339
1324
  shell = self.shellframe.clone_shell(plug)
1325
+ name = plug.Name # init(shell) で名前を参照するため再定義する
1340
1326
 
1341
- @shell.handler.bind("shell_activated")
1327
+ @shell.handler.bind("shell_activated") # @TODO: init action が重複してバインドされてしまう.
1342
1328
  def init(shell):
1329
+ """Called when the plug shell is activated."""
1343
1330
  nonlocal plug
1344
1331
  _plug = self.get_plug(name)
1345
1332
  if _plug is not plug:
1346
- shell.target = _plug or self # reset for loaded/unloaded plug
1333
+ shell.target = _plug or self # Reset the target to the reloaded plug.
1347
1334
  plug = _plug
1348
1335
  init(shell)
1349
1336
  self.shellframe.Show()
1350
- if wx.GetKeyState(wx.WXK_SHIFT):
1337
+ if wx.GetKeyState(wx.WXK_SHIFT): # open the source code.
1351
1338
  self.shellframe.load(plug)
1352
-
1339
+
1353
1340
  def OnLoadPlugins(self, evt):
1354
1341
  with wx.FileDialog(self, "Load a plugin file",
1355
1342
  wildcard="Python file (*.py)|*.py",
1356
- style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST
1357
- |wx.FD_MULTIPLE) as dlg:
1343
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE) as dlg:
1358
1344
  if dlg.ShowModal() == wx.ID_OK:
1359
1345
  for path in dlg.Paths:
1360
1346
  self.load_plug(path)
1361
-
1347
+
1362
1348
  def Quit(self, evt=None):
1363
1349
  """Stop all Layer threads."""
1364
- for name in self.plugins:
1365
- plug = self.get_plug(name)
1350
+ for plug in self.get_all_plugs():
1366
1351
  thread = plug.thread # Note: thread can be None or shared.
1367
1352
  if thread and thread.active:
1368
- thread.active = 0
1353
+ # thread.active = 0
1369
1354
  thread.Stop()
1370
-
1355
+
1371
1356
  ## --------------------------------
1372
- ## load/save index file
1357
+ ## load/save index file.
1373
1358
  ## --------------------------------
1374
- ATTRIBUTESFILE = "results.index"
1375
-
1376
- def load_index(self, filename=None, view=None):
1359
+ INDEXFILE = "results.index"
1360
+
1361
+ def import_index(self, filename=None, view=None):
1377
1362
  """Load frames :ref to the Index file.
1378
1363
 
1379
1364
  If no view given, the currently selected view is chosen.
@@ -1382,10 +1367,10 @@ class Frame(mwx.Frame):
1382
1367
  view = self.selected_view
1383
1368
 
1384
1369
  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,
1370
+ default_path = view.frame.pathname if view.frame else None
1371
+ with wx.FileDialog(self, "Select index file to load",
1372
+ defaultDir=os.path.dirname(default_path or ''),
1373
+ defaultFile=self.INDEXFILE,
1389
1374
  wildcard="Index (*.index)|*.index|"
1390
1375
  "ALL files (*.*)|*.*",
1391
1376
  style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST) as dlg:
@@ -1399,17 +1384,17 @@ class Frame(mwx.Frame):
1399
1384
  frames = self.load_buffer(paths, view)
1400
1385
  if frames:
1401
1386
  for frame in frames:
1402
- frame.update_attributes(res.get(frame.name))
1387
+ frame.update_attr(res.get(frame.name))
1403
1388
 
1404
1389
  n = len(frames)
1405
- self.message(
1390
+ print(self.message(
1406
1391
  "{} frames were imported, "
1407
1392
  "{} files were skipped, "
1408
- "{} files are missing.".format(n, len(res)-n, len(mis)))
1409
- print(self.message.read())
1393
+ "{} files are missing.".format(n, len(res)-n, len(mis))
1394
+ ))
1410
1395
  return frames
1411
-
1412
- def save_index(self, filename=None, frames=None):
1396
+
1397
+ def export_index(self, filename=None, frames=None):
1413
1398
  """Save frames :ref to the Index file.
1414
1399
  """
1415
1400
  view = self.selected_view
@@ -1419,10 +1404,10 @@ class Frame(mwx.Frame):
1419
1404
  return None
1420
1405
 
1421
1406
  if not filename:
1422
- fn = view.frame.pathname if view.frame else ''
1407
+ default_path = view.frame.pathname if view.frame else None
1423
1408
  with wx.FileDialog(self, "Select index file to export",
1424
- defaultDir=os.path.dirname(fn or ''),
1425
- defaultFile=self.ATTRIBUTESFILE,
1409
+ defaultDir=os.path.dirname(default_path or ''),
1410
+ defaultFile=self.INDEXFILE,
1426
1411
  wildcard="Index (*.index)|*.index",
1427
1412
  style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1428
1413
  if dlg.ShowModal() != wx.ID_OK:
@@ -1435,234 +1420,298 @@ class Frame(mwx.Frame):
1435
1420
  try:
1436
1421
  self.message("Export index of {!r}...".format(frame.name))
1437
1422
  fn = frame.pathname
1438
- if not fn:
1439
- fn = os.path.join(savedir, frame.name) # new file
1423
+ if not fn or fn.endswith('>'): # *dummy-path* --> Use buffer name.
1424
+ fn = os.path.join(savedir, fix_fnchars(frame.name))
1440
1425
  if not os.path.exists(fn):
1441
1426
  if not fn.endswith('.tif'):
1442
1427
  fn += '.tif'
1443
1428
  self.write_buffer(fn, frame.buffer)
1444
1429
  frame.pathname = fn
1445
- frame.name = os.path.basename(fn) # new name and pathname
1430
+ frame.name = os.path.basename(fn)
1431
+ print(' ', self.message("\b done."))
1432
+ else:
1433
+ print(' ', self.message("\b skipped."))
1446
1434
  output_frames.append(frame)
1447
- print(' ', self.message("\b done."))
1448
- except (PermissionError, OSError) as e:
1449
- print('-', self.message("\b failed.", e))
1435
+ except OSError as e:
1436
+ print('-', self.message("\b failed;", e))
1450
1437
 
1451
1438
  frames = output_frames
1452
1439
  res, mis = self.write_attributes(filename, frames)
1453
1440
  n = len(frames)
1454
- self.message(
1441
+ print(self.message(
1455
1442
  "{} frames were exported, "
1456
1443
  "{} files were skipped, "
1457
- "{} files are missing.".format(n, len(res)-n, len(mis)))
1458
- print(self.message.read())
1444
+ "{} files are missing.".format(n, len(res)-n, len(mis))
1445
+ ))
1459
1446
  return frames
1460
-
1447
+
1461
1448
  ## --------------------------------
1462
- ## load/save frames and attributes
1449
+ ## load/save frames and attributes.
1463
1450
  ## --------------------------------
1464
-
1465
- @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
1451
+ wildcards = [
1452
+ "TIF file (*.tif)|*.tif",
1453
+ "ALL files (*.*)|*.*",
1454
+ ]
1455
+
1456
+ def read_attributes(self, filename, check_path=True):
1457
+ """Read attributes file.
1458
+
1459
+ Returns:
1460
+ res: <dict> Successfully loaded attribute information.
1461
+ mis: <dict> Attributes whose file paths were missing.
1462
+ """
1463
+ def dt_parser(dct):
1464
+ for k, v in dct.items():
1465
+ if isinstance(v, str):
1466
+ try:
1467
+ dct[k] = datetime.fromisoformat(v)
1468
+ except Exception:
1469
+ pass
1470
+ return dct
1470
1471
  try:
1471
1472
  res = {}
1472
1473
  mis = {}
1473
1474
  savedir = os.path.dirname(filename)
1474
1475
  with open(filename) as i:
1475
- res.update(eval(i.read())) # read res <dict>
1476
+ s = i.read()
1477
+ try:
1478
+ res.update(json.loads(s, object_hook=dt_parser)) # Read res safely.
1479
+ except json.decoder.JSONDecodeError:
1480
+ res.update(eval(s)) # Read as tuple (deprecated).
1476
1481
 
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)
1482
+ if check_path:
1483
+ for name, attr in tuple(res.items()):
1484
+ fn = os.path.join(savedir, name) # Search by relpath (saved dir/name).
1485
+ if os.path.exists(fn):
1486
+ attr['pathname'] = fn # If found, update the path.
1487
+ else:
1488
+ fn = attr.get('pathname') # If not found, check for the recorded path.
1489
+ if not fn or not os.path.exists(fn):
1490
+ mis[name] = res.pop(name) # pop missing items
1486
1491
  except FileNotFoundError:
1487
1492
  pass
1488
1493
  except Exception as e:
1489
- print("- Failed to read attributes.", e)
1494
+ print("- Failed to read attributes;", e)
1490
1495
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1491
- finally:
1492
- return res, mis # finally raises no exception
1493
-
1494
- @classmethod
1495
- def write_attributes(self, filename, frames):
1496
- """Write attributes file."""
1496
+ return res, mis
1497
+
1498
+ def write_attributes(self, filename, frames, merge_data=True):
1499
+ """Write attributes file.
1500
+
1501
+ Returns:
1502
+ res: <dict> Successfully loaded attribute information.
1503
+ mis: <dict> Attributes whose file paths were missing.
1504
+ """
1505
+ def dt_converter(o):
1506
+ if isinstance(o, datetime):
1507
+ return o.isoformat()
1497
1508
  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.
1509
+ new = dict((frame.name, frame.attributes) for frame in frames)
1510
+ mis = {}
1511
+ if merge_data:
1512
+ res, mis = self.read_attributes(filename)
1513
+ ## Merge existing attributes from `res` to `new`,
1514
+ ## while keeping the order and values from `frames` (new) priority.
1515
+ for name, attr in res.items():
1516
+ if name not in new:
1517
+ new[name] = attr
1506
1518
 
1507
1519
  with open(filename, 'w') as o:
1508
- print(pformat(tuple(new.items())), file=o)
1509
-
1520
+ # print(pformat(tuple(new.items())), file=o) # Write as tuple (deprecated).
1521
+ json.dump(new, o, indent=2, default=dt_converter)
1510
1522
  except Exception as e:
1511
- print("- Failed to write attributes.", e)
1523
+ print("- Failed to write attributes;", e)
1512
1524
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1513
- finally:
1514
- return new, mis # finally raises no exception
1515
-
1525
+ return new, mis
1526
+
1516
1527
  def load_frame(self, paths=None, view=None):
1517
- """Load frames from files to the view window.
1528
+ """Load frames and the attributes from files to the view window."""
1529
+ if not view:
1530
+ view = self.selected_view
1531
+
1532
+ if isinstance(paths, str): # for single frame
1533
+ paths = [paths]
1534
+
1535
+ if paths is None:
1536
+ default_path = view.frame.pathname if view.frame else None
1537
+ with wx.FileDialog(self, "Open image files",
1538
+ defaultDir=os.path.dirname(default_path or ''),
1539
+ defaultFile='',
1540
+ wildcard='|'.join(self.wildcards),
1541
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_MULTIPLE) as dlg:
1542
+ if dlg.ShowModal() != wx.ID_OK:
1543
+ return None
1544
+ paths = dlg.Paths
1518
1545
 
1519
- Load buffer and the attributes of the frame.
1520
- If the file names duplicate, the latter takes priority.
1521
- """
1522
1546
  frames = self.load_buffer(paths, view)
1523
1547
  if frames:
1524
- savedirs = {}
1548
+ saved_results = {}
1525
1549
  for frame in frames:
1550
+ if frame.pathname.endswith('>'): # *dummy-path* compiled in load_buffer
1551
+ continue
1552
+ ## Compile attributes from index files located in each frame path.
1526
1553
  savedir = os.path.dirname(frame.pathname)
1527
- if savedir not in savedirs:
1528
- fn = os.path.join(savedir, self.ATTRIBUTESFILE)
1554
+ if savedir not in saved_results:
1555
+ fn = os.path.join(savedir, self.INDEXFILE)
1529
1556
  res, mis = self.read_attributes(fn)
1530
- savedirs[savedir] = res
1531
- results = savedirs[savedir]
1532
- frame.update_attributes(results.get(frame.name))
1557
+ saved_results[savedir] = res
1558
+ res = saved_results[savedir]
1559
+ frame.update_attr(res.get(frame.name))
1533
1560
  return frames
1534
-
1561
+
1535
1562
  def save_frame(self, path=None, frame=None):
1536
- """Save frame to a file.
1563
+ """Save frame and the attributes to a file."""
1564
+ view = self.selected_view
1565
+
1566
+ if not frame:
1567
+ frame = view.frame
1568
+ if not frame:
1569
+ return None
1570
+
1571
+ if not path:
1572
+ default_path = view.frame.pathname if view.frame else None
1573
+ with wx.FileDialog(self, "Save buffer as",
1574
+ defaultDir=os.path.dirname(default_path or ''),
1575
+ defaultFile=fix_fnchars(frame.name),
1576
+ wildcard='|'.join(self.wildcards),
1577
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1578
+ if dlg.ShowModal() != wx.ID_OK:
1579
+ return None
1580
+ path = dlg.Path
1537
1581
 
1538
- Save buffer and the attributes of the frame.
1539
- """
1540
1582
  frame = self.save_buffer(path, frame)
1541
1583
  if frame:
1542
1584
  savedir = os.path.dirname(frame.pathname)
1543
- fn = os.path.join(savedir, self.ATTRIBUTESFILE)
1585
+ fn = os.path.join(savedir, self.INDEXFILE)
1544
1586
  res, mis = self.write_attributes(fn, [frame])
1545
1587
  return frame
1546
-
1588
+
1589
+ def save_frames_as_tiff(self, path=None, frames=None):
1590
+ """Save frames to a multi-page tiff."""
1591
+ if not frames:
1592
+ frames = self.selected_view.all_frames
1593
+ if not frames:
1594
+ return None
1595
+
1596
+ if not path:
1597
+ with wx.FileDialog(self, "Save buffers as a multi-page tiff",
1598
+ defaultFile="Stack-image",
1599
+ wildcard="TIF file (*.tif)|*.tif",
1600
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) as dlg:
1601
+ if dlg.ShowModal() != wx.ID_OK:
1602
+ return None
1603
+ path = dlg.Path
1604
+ _name, ext = os.path.splitext(path)
1605
+ if ext != ".tif":
1606
+ path += ".tif"
1607
+
1608
+ try:
1609
+ name = os.path.basename(path)
1610
+ self.message("Saving {!r}...".format(name))
1611
+ with wx.BusyInfo(f"One moment please, saving {name!r}..."):
1612
+ stack = [Image.fromarray(frame.buffer) for frame in frames]
1613
+ stack[0].save(path,
1614
+ save_all=True,
1615
+ compression="tiff_deflate", # cf. tiff_lzw
1616
+ append_images=stack[1:])
1617
+ n = len(frames)
1618
+ d = len(str(n))
1619
+ for j, frame in enumerate(frames):
1620
+ frame.pathname = path + f"<{j:0{d}}>" # *dummy-path* in multi-page tiff
1621
+ ## multi-page tiff: 同名のインデクスファイルに属性を書き出す.
1622
+ self.write_attributes(path[:-4] + ".index", frames, merge_data=False)
1623
+ self.message("\b done.")
1624
+ return True
1625
+ except Exception as e:
1626
+ self.message("\b failed.")
1627
+ wx.MessageBox(str(e), style=wx.ICON_ERROR)
1628
+ return False
1629
+
1547
1630
  ## --------------------------------
1548
- ## load/save images
1631
+ ## load/save images.
1549
1632
  ## --------------------------------
1550
- wildcards = [
1551
- "TIF file (*.tif)|*.tif",
1552
- "ALL files (*.*)|*.*",
1553
- ]
1554
-
1633
+
1555
1634
  @staticmethod
1556
1635
  def read_buffer(path):
1557
1636
  """Read buffer from a file (to be overridden)."""
1558
1637
  buf = Image.open(path)
1559
1638
  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
1639
+ if buf.mode[:3] == 'RGB': # 今のところカラー画像には対応する気はない▼
1640
+ buf = buf.convert('L') # ここでグレースケールに変換する
1641
+ # return np.asarray(buf), info # ref
1642
+ # return np.array(buf), info # copy
1564
1643
  return buf, info
1565
-
1644
+
1566
1645
  @staticmethod
1567
1646
  def write_buffer(path, buf):
1568
1647
  """Write buffer to a file (to be overridden)."""
1569
1648
  try:
1570
1649
  img = Image.fromarray(buf)
1571
- img.save(path) # PIL saves as L, I, F, and RGB.
1650
+ img.save(path) # PIL saves as L, I, F, and RGB.
1572
1651
  except PermissionError:
1573
1652
  raise
1574
- except OSError: # cannot write mode L, I, F as BMP, etc.
1653
+ except OSError: # cannot write mode L, I, F as BMP, etc.
1575
1654
  if os.path.exists(path):
1576
1655
  os.remove(path)
1577
1656
  raise
1578
-
1657
+
1579
1658
  @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.
1659
+ def load_buffer(self, paths, view):
1660
+ """Load buffers from paths to the view window (internal use only).
1584
1661
  """
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
1662
  frames = []
1603
1663
  frame = None
1664
+ paths = list(dict.fromkeys(paths)) # 順序を保って重複を除く.
1604
1665
  try:
1605
1666
  for i, path in enumerate(paths):
1606
- fn = os.path.basename(path)
1607
- self.message("Loading {!r} ({} of {})...".format(fn, i+1, len(paths)))
1667
+ name = os.path.basename(path)
1668
+ self.message("Loading {!r} ({} of {})...".format(name, i+1, len(paths)))
1608
1669
  try:
1609
1670
  buf, info = self.read_buffer(path)
1610
1671
  except Image.UnidentifiedImageError:
1611
1672
  retvals = self.handler('unknown_format', path)
1612
1673
  if retvals and any(retvals):
1613
1674
  continue
1614
- raise # no context or no handlers or cannot identify image file
1675
+ raise # no context or no handlers or cannot identify image file
1615
1676
  except FileNotFoundError as e:
1616
1677
  print(e)
1617
1678
  continue
1618
1679
 
1619
- if isinstance(buf, TiffImageFile) and buf.n_frames > 1: # multi-page tiff
1680
+ if isinstance(buf, TiffImageFile) and buf.n_frames > 1:
1681
+ ## multi-page tiff: 同名のインデクスファイルから属性を読み出す.
1682
+ res, mis = self.read_attributes(path[:-4] + ".index", check_path=False)
1683
+ items = list({**res, **mis}.items())
1620
1684
  n = buf.n_frames
1621
1685
  d = len(str(n))
1622
1686
  for j in range(n):
1623
- self.message("Loading {!r} [{} of {} pages]...".format(fn, j+1, n))
1687
+ self.message("Loading {!r} [{} of {} pages]...".format(name, j+1, n))
1624
1688
  buf.seek(j)
1625
- name = "{:0{d}}-{}".format(j, fn, d=d)
1626
- frame = view.load(buf, name, show=0)
1689
+ if items:
1690
+ page_name, info = items[j] # original buffer name and attributes
1691
+ else:
1692
+ page_name = name + f"<{j:0{d}}>" # default buffer name
1693
+ info['pathname'] = path + f"<{j:0{d}}>" # *dummy-path* in multi-page tiff
1694
+ frame = view.load(buf, page_name, show=0, **info)
1695
+ frames.append(frame)
1627
1696
  else:
1628
- frame = view.load(buf, fn, show=0, pathname=path, **info)
1697
+ frame = view.load(buf, name, show=0, pathname=path, **info)
1629
1698
  frames.append(frame)
1630
1699
  self.message("\b done.")
1631
1700
  except Exception as e:
1632
1701
  self.message("\b failed.")
1633
1702
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1634
1703
 
1635
- if frame:
1636
- view.select(frame)
1704
+ view.select(frame)
1637
1705
  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
1706
+
1707
+ def save_buffer(self, path, frame):
1708
+ """Save buffer of the frame to a file (internal use only)."""
1658
1709
  try:
1659
1710
  name = os.path.basename(path)
1660
1711
  self.message("Saving {!r}...".format(name))
1661
-
1662
1712
  self.write_buffer(path, frame.buffer)
1663
1713
  frame.name = name
1664
1714
  frame.pathname = path
1665
-
1666
1715
  self.message("\b done.")
1667
1716
  return frame
1668
1717
  except ValueError:
@@ -1674,58 +1723,24 @@ class Frame(mwx.Frame):
1674
1723
  self.message("\b failed.")
1675
1724
  wx.MessageBox(str(e), style=wx.ICON_ERROR)
1676
1725
  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
-
1726
+
1711
1727
  ## --------------------------------
1712
- ## load/save session
1728
+ ## load/save session.
1713
1729
  ## --------------------------------
1714
1730
  session_file = None
1715
-
1731
+
1716
1732
  def load_session(self, filename=None, flush=True):
1717
1733
  """Load session from file."""
1718
1734
  if not filename:
1719
- with wx.FileDialog(self, 'Load session',
1735
+ with wx.FileDialog(self, "Load session",
1720
1736
  wildcard="Session file (*.jssn)|*.jssn",
1721
- style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST
1722
- |wx.FD_CHANGE_DIR) as dlg:
1737
+ style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST|wx.FD_CHANGE_DIR) as dlg:
1723
1738
  if dlg.ShowModal() != wx.ID_OK:
1724
1739
  return
1725
1740
  filename = dlg.Path
1726
1741
 
1727
1742
  if flush:
1728
- for name in list(self.plugins): # plugins:dict mutates during iteration
1743
+ for name in list(self.plugins): # plugins:dict mutates during iteration
1729
1744
  self.unload_plug(name)
1730
1745
  del self.graph[:]
1731
1746
  del self.output[:]
@@ -1745,28 +1760,31 @@ class Frame(mwx.Frame):
1745
1760
  self._mgr.Update()
1746
1761
  self.menubar.reset()
1747
1762
 
1748
- dirname_ = os.path.dirname(i.name)
1749
- if dirname_:
1750
- os.chdir(dirname_)
1751
-
1752
1763
  ## Reposition the window if it is not on the desktop.
1753
1764
  if wx.Display.GetFromWindow(self) == -1:
1754
1765
  self.Position = (0, 0)
1755
1766
 
1767
+ ## LoadPerspective => 表示状態の不整合.手動でイベントを発生させる.
1768
+ for pane in self._mgr.GetAllPanes():
1769
+ if pane.IsShown():
1770
+ win = pane.window
1771
+ if isinstance(win, aui.AuiNotebook):
1772
+ win = win.CurrentPage
1773
+ win.handler('page_shown', win)
1774
+
1756
1775
  self.message("\b done.")
1757
-
1776
+
1758
1777
  def save_session_as(self):
1759
1778
  """Save session as a new file."""
1760
1779
  with wx.FileDialog(self, "Save session as",
1761
1780
  defaultDir=os.path.dirname(self.session_file or ''),
1762
1781
  defaultFile=os.path.basename(self.session_file or ''),
1763
1782
  wildcard="Session file (*.jssn)|*.jssn",
1764
- style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT
1765
- |wx.FD_CHANGE_DIR) as dlg:
1783
+ style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT|wx.FD_CHANGE_DIR) as dlg:
1766
1784
  if dlg.ShowModal() == wx.ID_OK:
1767
1785
  self.session_file = dlg.Path
1768
1786
  self.save_session()
1769
-
1787
+
1770
1788
  def save_session(self):
1771
1789
  """Save session to file."""
1772
1790
  if not self.session_file:
@@ -1775,34 +1793,35 @@ class Frame(mwx.Frame):
1775
1793
  self.message("Saving session to {!r}...".format(self.session_file))
1776
1794
 
1777
1795
  with open(self.session_file, 'w') as o,\
1778
- np.printoptions(threshold=np.inf): # printing all(inf) elements
1796
+ np.printoptions(threshold=np.inf): # printing all(inf) elements
1779
1797
  o.write("#! Session file (This file is generated automatically)\n")
1780
1798
  o.write("self.SetSize({})\n".format(self.Size))
1781
1799
  o.write("self.SetPosition({})\n".format(self.Position))
1782
1800
 
1783
1801
  for name, module in self.plugins.items():
1784
1802
  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]
1803
+ name = module.__file__ # Replace the name with full-path.
1804
+ if not plug or not os.path.exists(name):
1805
+ print(f"Skipping dummy plugin {name!r}...")
1806
+ continue
1807
+ if hasattr(module, '__path__'): # is the module a package?
1808
+ name = os.path.dirname(name)
1789
1809
  session = {}
1790
1810
  try:
1791
1811
  plug.save_session(session)
1792
1812
  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))
1813
+ traceback.print_exc() # Failed to save the plug session.
1814
+ o.write("self.load_plug({!r}, session={})\n".format(name, session))
1796
1815
  o.write("self._mgr.LoadPerspective({!r})\n".format(self._mgr.SavePerspective()))
1797
1816
 
1798
1817
  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")
1818
+ paths = [frame.pathname for frame in view.all_frames if frame.pathname]
1819
+ paths = [fn for fn in paths if not fn.endswith('>')] # *dummy-path* 除外
1820
+ o.write(f"self.{view.Name}.unit = {view.unit:g}\n")
1821
+ o.write(f"self.load_frame({paths!r}, self.{view.Name})\n")
1803
1822
  try:
1804
- index = paths.index(view.frame.pathname)
1805
- o.write(f"self.{name}.select({index})\n")
1823
+ if view.frame.pathname in paths:
1824
+ o.write(f"self.{view.Name}.select({view.frame.name!r})\n")
1806
1825
  except Exception:
1807
1826
  pass
1808
1827