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