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/__init__.py +6 -4
- mwx/bookshelf.py +144 -68
- mwx/controls.py +444 -378
- mwx/framework.py +567 -546
- mwx/graphman.py +745 -726
- mwx/images.py +16 -0
- mwx/matplot2.py +244 -235
- mwx/matplot2g.py +812 -751
- mwx/matplot2lg.py +218 -209
- mwx/mgplt.py +20 -22
- mwx/nutshell.py +1119 -1015
- mwx/plugins/ffmpeg_view.py +96 -90
- mwx/plugins/fft_view.py +13 -15
- mwx/plugins/frame_listview.py +68 -75
- mwx/plugins/line_profile.py +1 -1
- mwx/py/filling.py +13 -14
- mwx/testsuite.py +38 -0
- mwx/utilus.py +284 -219
- mwx/wxmon.py +43 -44
- mwx/wxpdb.py +83 -84
- mwx/wxwil.py +19 -18
- mwx/wxwit.py +38 -45
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/METADATA +12 -5
- mwxlib-1.8.0.dist-info/RECORD +28 -0
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/WHEEL +1 -1
- mwxlib-1.0.0.dist-info/LICENSE +0 -21
- mwxlib-1.0.0.dist-info/RECORD +0 -28
- {mwxlib-1.0.0.dist-info → mwxlib-1.8.0.dist-info}/top_level.txt +0 -0
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
|
|
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
|
|
60
|
-
result
|
|
61
|
-
worker
|
|
62
|
-
owner
|
|
63
|
-
|
|
64
|
-
event
|
|
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
|
|
69
|
-
|
|
70
|
-
- running
|
|
71
|
-
|
|
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({
|
|
84
|
+
self.handler = FSM({ # DNA<Thread>
|
|
100
85
|
None : {
|
|
101
|
-
'thread_begin' : [
|
|
102
|
-
'thread_end' : [
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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):
|
|
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=
|
|
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
|
-
|
|
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
|
|
166
|
-
msg
|
|
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()
|
|
169
|
-
if wx.MessageBox(
|
|
170
|
-
msg
|
|
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
|
|
174
|
-
return
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
175
166
|
finally:
|
|
176
|
-
self.event.set()
|
|
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
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
233
|
-
category
|
|
234
|
-
caption
|
|
235
|
-
|
|
236
|
-
dockable
|
|
237
|
-
|
|
238
|
-
reloadable
|
|
239
|
-
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"
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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(
|
|
320
|
-
if
|
|
321
|
-
|
|
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(
|
|
324
|
-
if
|
|
325
|
-
|
|
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(
|
|
321
|
+
def reset_params(evt, checked_only=False):
|
|
328
322
|
self.Draw(None)
|
|
329
323
|
if self.parameters:
|
|
330
|
-
|
|
324
|
+
self.reset_params(checked_only)
|
|
331
325
|
|
|
332
|
-
self.handler.append({
|
|
326
|
+
self.handler.append({ # DNA<Layer>
|
|
333
327
|
None : {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
'
|
|
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
|
-
(
|
|
365
|
-
lambda v: self.parent.
|
|
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
|
|
374
|
-
lambda v: self.parent.unload_plug(self
|
|
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
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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__}
|
|
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
|
|
497
|
-
loader
|
|
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({
|
|
511
|
+
self.handler.append({ # DNA<Graph>
|
|
506
512
|
None : {
|
|
507
|
-
'focus_set' : [
|
|
508
|
-
'page_shown' : [
|
|
509
|
-
'page_closed' : [
|
|
510
|
-
'frame_shown' : [
|
|
511
|
-
'S-a pressed' : [
|
|
512
|
-
'f5 pressed' : [
|
|
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
|
|
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.
|
|
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
|
|
563
|
-
loader
|
|
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
|
-
|
|
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.
|
|
593
|
+
self.loader.import_index(fn, self.view)
|
|
584
594
|
else:
|
|
585
|
-
paths.append(fn)
|
|
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 = {}
|
|
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()
|
|
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.
|
|
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.
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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", []),
|
|
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_(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
744
|
-
lambda v: v.Check(self.__view.
|
|
745
|
-
or self.__view.
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
("Standard
|
|
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
|
|
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
|
-
|
|
777
|
+
if not frame.parent.IsShown():
|
|
778
|
+
wx.CallAfter(self.show_pane, frame.parent)
|
|
775
779
|
|
|
776
|
-
self.graph.handler.append({
|
|
780
|
+
self.graph.handler.append({ # DNA<Graph:Frame>
|
|
777
781
|
None : {
|
|
778
|
-
'frame_shown' : [
|
|
779
|
-
'frame_loaded' : [
|
|
780
|
-
'frame_modified' : [
|
|
781
|
-
'frame_selected' : [
|
|
782
|
-
'canvas_draw' : [
|
|
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({
|
|
789
|
+
self.output.handler.append({ # DNA<Graph:Frame>
|
|
786
790
|
None : {
|
|
787
|
-
'frame_shown' : [
|
|
788
|
-
'frame_loaded' : [
|
|
789
|
-
'frame_modified' : [
|
|
790
|
-
'frame_selected' : [
|
|
791
|
-
'canvas_draw' : [
|
|
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:
|
|
797
|
-
self.output.menu += self.menubar["Edit"][2:
|
|
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
|
|
814
|
-
self.Bind(wx.EVT_MOVE_END, lambda v
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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):
|
|
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):
|
|
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
|
|
866
|
-
|
|
867
|
-
if
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
941
|
-
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
998
|
+
pane.Gripper(dock not in (0,5)) # show a grip when docked
|
|
1002
999
|
pane.Dockable(dock)
|
|
1003
|
-
|
|
1004
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
1048
|
-
def
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
show
|
|
1123
|
-
dock
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
self.
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1159
|
-
title =
|
|
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
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
1172
|
+
## Unload the plugin if loaded.
|
|
1189
1173
|
if pane.IsOk():
|
|
1190
|
-
|
|
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 =
|
|
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
|
-
##
|
|
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)
|
|
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_'):
|
|
1246
|
-
global __plug_ID__
|
|
1247
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|
|
1292
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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 {}..."
|
|
1305
|
+
print(f"Reloading {plug}...")
|
|
1313
1306
|
plug.save_session(session)
|
|
1314
1307
|
except Exception:
|
|
1315
|
-
traceback.print_exc()
|
|
1316
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
def
|
|
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
|
-
|
|
1386
|
-
with wx.FileDialog(self, "Select index file to
|
|
1387
|
-
defaultDir=os.path.dirname(
|
|
1388
|
-
defaultFile=self.
|
|
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.
|
|
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
|
-
|
|
1393
|
+
"{} files are missing.".format(n, len(res)-n, len(mis))
|
|
1394
|
+
))
|
|
1410
1395
|
return frames
|
|
1411
|
-
|
|
1412
|
-
def
|
|
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
|
-
|
|
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(
|
|
1425
|
-
defaultFile=self.
|
|
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)
|
|
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)
|
|
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
|
-
|
|
1448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
|
1494
|
+
print("- Failed to read attributes;", e)
|
|
1490
1495
|
wx.MessageBox(str(e), style=wx.ICON_ERROR)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
|
1523
|
+
print("- Failed to write attributes;", e)
|
|
1512
1524
|
wx.MessageBox(str(e), style=wx.ICON_ERROR)
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
-
|
|
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
|
|
1528
|
-
fn = os.path.join(savedir, self.
|
|
1554
|
+
if savedir not in saved_results:
|
|
1555
|
+
fn = os.path.join(savedir, self.INDEXFILE)
|
|
1529
1556
|
res, mis = self.read_attributes(fn)
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
frame.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1563
|
-
|
|
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)
|
|
1650
|
+
img.save(path) # PIL saves as L, I, F, and RGB.
|
|
1572
1651
|
except PermissionError:
|
|
1573
1652
|
raise
|
|
1574
|
-
except OSError:
|
|
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
|
|
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
|
-
|
|
1607
|
-
self.message("Loading {!r} ({} of {})...".format(
|
|
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
|
|
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:
|
|
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(
|
|
1687
|
+
self.message("Loading {!r} [{} of {} pages]...".format(name, j+1, n))
|
|
1624
1688
|
buf.seek(j)
|
|
1625
|
-
|
|
1626
|
-
|
|
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,
|
|
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
|
-
|
|
1636
|
-
view.select(frame)
|
|
1704
|
+
view.select(frame)
|
|
1637
1705
|
return frames
|
|
1638
|
-
|
|
1639
|
-
def save_buffer(self, path
|
|
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,
|
|
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):
|
|
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):
|
|
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
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1800
|
-
paths = [
|
|
1801
|
-
o.write(f"self.{
|
|
1802
|
-
o.write(f"self.load_frame({paths!r}, self.{
|
|
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
|
-
|
|
1805
|
-
|
|
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
|
|