mwxlib 1.0.0__py3-none-any.whl → 1.7.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mwx/__init__.py +6 -4
- mwx/bookshelf.py +133 -57
- mwx/controls.py +441 -375
- mwx/framework.py +531 -513
- mwx/graphman.py +683 -677
- mwx/images.py +16 -0
- mwx/matplot2.py +225 -216
- mwx/matplot2g.py +735 -652
- mwx/matplot2lg.py +192 -183
- mwx/mgplt.py +20 -22
- mwx/nutshell.py +1063 -939
- mwx/plugins/ffmpeg_view.py +74 -75
- mwx/plugins/fft_view.py +13 -15
- mwx/plugins/frame_listview.py +55 -52
- mwx/plugins/line_profile.py +1 -1
- mwx/py/filling.py +9 -8
- mwx/testsuite.py +38 -0
- mwx/utilus.py +273 -210
- mwx/wxmon.py +39 -38
- mwx/wxpdb.py +81 -83
- mwx/wxwil.py +18 -18
- mwx/wxwit.py +32 -35
- {mwxlib-1.0.0.dist-info → mwxlib-1.7.13.dist-info}/METADATA +12 -5
- mwxlib-1.7.13.dist-info/RECORD +28 -0
- {mwxlib-1.0.0.dist-info → mwxlib-1.7.13.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.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
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
|
|
@@ -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,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
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.
|
|
@@ -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)
|
|
@@ -512,67 +518,54 @@ class Graph(GraphPlot):
|
|
|
512
518
|
'f5 pressed' : [ None, _F(self.refresh) ],
|
|
513
519
|
},
|
|
514
520
|
})
|
|
515
|
-
##
|
|
521
|
+
## ドロップターゲットを許可する.
|
|
516
522
|
self.SetDropTarget(MyFileDropLoader(self, self.loader))
|
|
517
|
-
|
|
523
|
+
|
|
518
524
|
def refresh(self):
|
|
519
525
|
if self.frame:
|
|
520
526
|
self.frame.update_buffer()
|
|
521
527
|
self.draw()
|
|
522
|
-
|
|
528
|
+
|
|
523
529
|
def toggle_infobar(self):
|
|
524
530
|
"""Toggle infobar (frame.annotation)."""
|
|
525
531
|
if self.infobar.IsShown():
|
|
526
532
|
self.infobar.Dismiss()
|
|
527
533
|
elif self.frame:
|
|
528
534
|
self.infobar.ShowMessage(self.frame.annotation)
|
|
529
|
-
|
|
535
|
+
|
|
530
536
|
def update_infobar(self, frame):
|
|
531
537
|
"""Show infobar (frame.annotation)."""
|
|
532
538
|
if self.infobar.IsShown():
|
|
533
539
|
self.infobar.ShowMessage(frame.annotation)
|
|
534
|
-
|
|
535
|
-
def get_markups_visible(self):
|
|
536
|
-
return self.marked.get_visible()
|
|
537
|
-
|
|
538
|
-
def set_markups_visible(self, v):
|
|
539
|
-
self.selected.set_visible(v)
|
|
540
|
-
self.marked.set_visible(v)
|
|
541
|
-
self.rected.set_visible(v)
|
|
542
|
-
self.update_art_of_mark()
|
|
543
|
-
|
|
544
|
-
def remove_markups(self):
|
|
545
|
-
del self.Selector
|
|
546
|
-
del self.Markers
|
|
547
|
-
del self.Region
|
|
548
|
-
|
|
540
|
+
|
|
549
541
|
def hide_layers(self):
|
|
550
|
-
for
|
|
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
|
|
|
557
550
|
|
|
558
551
|
class MyFileDropLoader(wx.FileDropTarget):
|
|
559
|
-
"""File Drop interface
|
|
552
|
+
"""File Drop interface.
|
|
560
553
|
|
|
561
554
|
Args:
|
|
562
|
-
target
|
|
563
|
-
loader
|
|
555
|
+
target: target view to drop in, e.g. frame, graph, pane, etc.
|
|
556
|
+
loader: mainframe
|
|
564
557
|
"""
|
|
565
558
|
def __init__(self, target, loader):
|
|
566
559
|
wx.FileDropTarget.__init__(self)
|
|
567
560
|
|
|
568
561
|
self.view = target
|
|
569
562
|
self.loader = loader
|
|
570
|
-
|
|
563
|
+
|
|
571
564
|
def OnDropFiles(self, x, y, filenames):
|
|
572
|
-
pos = self.view.ScreenPosition + (x,y)
|
|
565
|
+
pos = self.view.ScreenPosition + (x, y)
|
|
573
566
|
paths = []
|
|
574
567
|
for fn in filenames:
|
|
575
|
-
|
|
568
|
+
_name, ext = os.path.splitext(fn)
|
|
576
569
|
if ext == '.py' or os.path.isdir(fn):
|
|
577
570
|
self.loader.load_plug(fn, show=1,
|
|
578
571
|
floating_pos=pos,
|
|
@@ -582,7 +575,7 @@ class MyFileDropLoader(wx.FileDropTarget):
|
|
|
582
575
|
elif ext == '.index':
|
|
583
576
|
self.loader.load_index(fn, self.view)
|
|
584
577
|
else:
|
|
585
|
-
paths.append(fn)
|
|
578
|
+
paths.append(fn) # image file just stacks to be loaded
|
|
586
579
|
if paths:
|
|
587
580
|
self.loader.load_frame(paths, self.view)
|
|
588
581
|
return True
|
|
@@ -600,24 +593,24 @@ class Frame(mwx.Frame):
|
|
|
600
593
|
graph = property(lambda self: self.__graph)
|
|
601
594
|
output = property(lambda self: self.__output)
|
|
602
595
|
histogram = property(lambda self: self.__histgrm)
|
|
603
|
-
|
|
596
|
+
|
|
604
597
|
selected_view = property(lambda self: self.__view)
|
|
605
|
-
|
|
598
|
+
|
|
606
599
|
def select_view(self, view):
|
|
607
600
|
self.__view = view
|
|
608
601
|
self.set_title(view.frame)
|
|
609
|
-
|
|
602
|
+
|
|
610
603
|
@property
|
|
611
604
|
def graphic_windows(self):
|
|
612
605
|
"""Graphic windows list.
|
|
613
606
|
[0] graph [1] output [2:] others(user-defined)
|
|
614
607
|
"""
|
|
615
608
|
return self.__graphic_windows
|
|
616
|
-
|
|
609
|
+
|
|
617
610
|
@property
|
|
618
611
|
def graphic_windows_on_screen(self):
|
|
619
612
|
return [w for w in self.__graphic_windows if w.IsShownOnScreen()]
|
|
620
|
-
|
|
613
|
+
|
|
621
614
|
def __init__(self, *args, **kwargs):
|
|
622
615
|
mwx.Frame.__init__(self, *args, **kwargs)
|
|
623
616
|
|
|
@@ -625,7 +618,7 @@ class Frame(mwx.Frame):
|
|
|
625
618
|
self._mgr.SetManagedWindow(self)
|
|
626
619
|
self._mgr.SetDockSizeConstraint(0.5, 0.5)
|
|
627
620
|
|
|
628
|
-
self.__plugins = {}
|
|
621
|
+
self.__plugins = {} # modules in the order of load/save
|
|
629
622
|
|
|
630
623
|
self.__graph = Graph(self, log=self.message, margin=None)
|
|
631
624
|
self.__output = Graph(self, log=self.message, margin=None)
|
|
@@ -646,7 +639,7 @@ class Frame(mwx.Frame):
|
|
|
646
639
|
self.histogram.Name = "histogram"
|
|
647
640
|
|
|
648
641
|
self._mgr.AddPane(self.graph,
|
|
649
|
-
aui.AuiPaneInfo().CenterPane()
|
|
642
|
+
aui.AuiPaneInfo().CenterPane()
|
|
650
643
|
.Name("graph").Caption("graph").CaptionVisible(1))
|
|
651
644
|
|
|
652
645
|
size = (200, 200)
|
|
@@ -678,7 +671,7 @@ class Frame(mwx.Frame):
|
|
|
678
671
|
lambda v: v.Enable(self.__view.frame is not None)),
|
|
679
672
|
|
|
680
673
|
(wx.ID_SAVEAS, "&Save as TIFFs", "Save buffers as a multi-page tiff", Icon('saveall'),
|
|
681
|
-
lambda v: self.
|
|
674
|
+
lambda v: self.save_frames_as_tiff(),
|
|
682
675
|
lambda v: v.Enable(self.__view.frame is not None)),
|
|
683
676
|
(),
|
|
684
677
|
("Index", (
|
|
@@ -701,7 +694,7 @@ class Frame(mwx.Frame):
|
|
|
701
694
|
lambda v: self.save_session_as()),
|
|
702
695
|
)),
|
|
703
696
|
(),
|
|
704
|
-
("Options", []),
|
|
697
|
+
("Options", []), # reserved for optional app settings
|
|
705
698
|
(),
|
|
706
699
|
(mwx.ID_(13), "&Graph window\tF9", "Show graph window", wx.ITEM_CHECK,
|
|
707
700
|
lambda v: self.show_pane(self.graph, v.IsChecked()),
|
|
@@ -719,44 +712,37 @@ class Frame(mwx.Frame):
|
|
|
719
712
|
(wx.ID_PASTE, "&Paste\t(C-v)", "Paste buffer from clipboard", Icon('paste'),
|
|
720
713
|
lambda v: self.__view.read_buffer_from_clipboard()),
|
|
721
714
|
(),
|
|
722
|
-
(mwx.ID_(
|
|
723
|
-
lambda v: self.__view.set_markups_visible(v.IsChecked()),
|
|
724
|
-
lambda v: v.Check(self.__view.get_markups_visible())),
|
|
725
|
-
|
|
726
|
-
(mwx.ID_(22), "&Remove Markers", "Remove markups", Icon('-'),
|
|
727
|
-
lambda v: self.__view.remove_markups()),
|
|
728
|
-
(),
|
|
729
|
-
(mwx.ID_(23), "Hide all &Layers", "Hide all layers", Icon('xr'),
|
|
715
|
+
(mwx.ID_(23), "Hide all &layers", "Hide all layers", Icon('xr'),
|
|
730
716
|
lambda v: self.__view.hide_layers()),
|
|
731
717
|
(),
|
|
732
|
-
(mwx.ID_(24), "&Histogram\tCtrl-h", "Show
|
|
718
|
+
(mwx.ID_(24), "&Histogram\tCtrl-h", "Show histogram window", wx.ITEM_CHECK,
|
|
733
719
|
lambda v: self.show_pane(self.histogram, v.IsChecked()),
|
|
734
720
|
lambda v: v.Check(self.histogram.IsShownOnScreen())),
|
|
735
721
|
|
|
736
|
-
(mwx.ID_(25), "&Invert
|
|
722
|
+
(mwx.ID_(25), "&Invert color\t(C-i)", "Invert colormap", wx.ITEM_CHECK,
|
|
737
723
|
lambda v: self.__view.invert_cmap(),
|
|
738
|
-
lambda v: v.Check(self.__view.
|
|
724
|
+
lambda v: v.Check(self.__view.get_cmapstr()[-2:] == "_r")),
|
|
739
725
|
]
|
|
740
726
|
|
|
741
727
|
def _cmenu(i, name):
|
|
742
728
|
return (mwx.ID_(30 + i), "&" + name, name, wx.ITEM_CHECK,
|
|
743
|
-
lambda v: self.__view.
|
|
744
|
-
lambda v: v.Check(self.__view.
|
|
745
|
-
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"),
|
|
746
732
|
)
|
|
747
733
|
colours = [c for c in dir(cm) if c[-2:] != "_r"
|
|
748
734
|
and isinstance(getattr(cm, c), colors.LinearSegmentedColormap)]
|
|
749
735
|
|
|
750
736
|
self.menubar["Edit"] += [
|
|
751
737
|
(),
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
("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",
|
|
757
743
|
[_cmenu(i, c) for i, c in enumerate(colours) if c.islower()]),
|
|
758
744
|
|
|
759
|
-
("Other
|
|
745
|
+
("Other colors",
|
|
760
746
|
[_cmenu(i, c) for i, c in enumerate(colours) if not c.islower()]),
|
|
761
747
|
]
|
|
762
748
|
|
|
@@ -771,7 +757,8 @@ class Frame(mwx.Frame):
|
|
|
771
757
|
self.menubar.reset()
|
|
772
758
|
|
|
773
759
|
def show_frameview(frame):
|
|
774
|
-
|
|
760
|
+
if not frame.parent.IsShown():
|
|
761
|
+
wx.CallAfter(self.show_pane, frame.parent)
|
|
775
762
|
|
|
776
763
|
self.graph.handler.append({ # DNA<Graph:Frame>
|
|
777
764
|
None : {
|
|
@@ -792,9 +779,9 @@ class Frame(mwx.Frame):
|
|
|
792
779
|
},
|
|
793
780
|
})
|
|
794
781
|
|
|
795
|
-
## Add main-menu to context-menu
|
|
796
|
-
self.graph.menu += self.menubar["Edit"][2:
|
|
797
|
-
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]
|
|
798
785
|
|
|
799
786
|
self._mgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
|
|
800
787
|
|
|
@@ -810,10 +797,10 @@ class Frame(mwx.Frame):
|
|
|
810
797
|
_display(self.graph, show)
|
|
811
798
|
_display(self.output, show)
|
|
812
799
|
evt.Skip()
|
|
813
|
-
self.Bind(wx.EVT_MOVE_START, lambda v
|
|
814
|
-
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))
|
|
815
802
|
|
|
816
|
-
## Custom Key Bindings
|
|
803
|
+
## Custom Key Bindings.
|
|
817
804
|
self.define_key('* C-g', self.Quit)
|
|
818
805
|
|
|
819
806
|
@self.shellframe.define_key('* C-g')
|
|
@@ -821,36 +808,33 @@ class Frame(mwx.Frame):
|
|
|
821
808
|
"""Dispatch quit to the main Frame."""
|
|
822
809
|
self.handler('C-g pressed', evt)
|
|
823
810
|
|
|
824
|
-
## Accepts DnD
|
|
811
|
+
## Accepts DnD.
|
|
825
812
|
self.SetDropTarget(MyFileDropLoader(self.graph, self))
|
|
826
|
-
|
|
827
|
-
## Script editor for plugins (external call)
|
|
828
|
-
EDITOR = "notepad"
|
|
829
|
-
|
|
813
|
+
|
|
830
814
|
SYNC_SWITCH = True
|
|
831
|
-
|
|
815
|
+
|
|
832
816
|
def sync(self, a, b):
|
|
833
817
|
"""Synchronize b to a."""
|
|
834
818
|
if (self.SYNC_SWITCH
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
819
|
+
and a.frame and b.frame
|
|
820
|
+
and a.frame.unit == b.frame.unit
|
|
821
|
+
and a.buffer.shape == b.buffer.shape):
|
|
822
|
+
b.xlim = a.xlim
|
|
823
|
+
b.ylim = a.ylim
|
|
824
|
+
b.OnDraw(None)
|
|
825
|
+
b.canvas.draw_idle()
|
|
826
|
+
|
|
843
827
|
def set_title(self, frame):
|
|
844
828
|
ssn = os.path.basename(self.session_file or '--')
|
|
845
829
|
ssn, _ = os.path.splitext(ssn)
|
|
846
830
|
name = (frame.pathname or frame.name) if frame else ''
|
|
847
831
|
self.SetTitle("{}@{} - [{}] {}".format(self.Name, platform.node(), ssn, name))
|
|
848
|
-
|
|
849
|
-
def OnActivate(self, evt):
|
|
832
|
+
|
|
833
|
+
def OnActivate(self, evt): #<wx._core.ActivateEvent>
|
|
850
834
|
if self and evt.Active:
|
|
851
835
|
self.set_title(self.selected_view.frame)
|
|
852
|
-
|
|
853
|
-
def OnClose(self, evt):
|
|
836
|
+
|
|
837
|
+
def OnClose(self, evt): #<wx._core.CloseEvent>
|
|
854
838
|
ssn = os.path.basename(self.session_file or '--')
|
|
855
839
|
with wx.MessageDialog(None,
|
|
856
840
|
"Do you want to save session before closing program?",
|
|
@@ -862,66 +846,69 @@ class Frame(mwx.Frame):
|
|
|
862
846
|
elif ret == wx.ID_CANCEL:
|
|
863
847
|
evt.Veto()
|
|
864
848
|
return
|
|
865
|
-
for
|
|
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
|
|
849
|
+
n = sum(bool(plug.thread and plug.thread.active) for plug in self.get_all_plugs())
|
|
850
|
+
if n:
|
|
851
|
+
s = 's' if n > 1 else ''
|
|
852
|
+
if wx.MessageBox( # Confirm closing the thread.
|
|
853
|
+
f"Currently running {n} thread{s}.\n\n"
|
|
854
|
+
"Continue closing?",
|
|
855
|
+
style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
|
|
856
|
+
self.message("The close has been canceled.")
|
|
857
|
+
evt.Veto()
|
|
858
|
+
return
|
|
859
|
+
self.Quit()
|
|
860
|
+
n = sum(frame.pathname is None for frame in self.graph.all_frames)
|
|
861
|
+
if n:
|
|
862
|
+
s = 's' if n > 1 else ''
|
|
863
|
+
if wx.MessageBox( # Confirm closing the frame.
|
|
864
|
+
f"You are closing {n} unsaved frame{s}.\n\n"
|
|
865
|
+
"Continue closing?",
|
|
866
|
+
style=wx.YES_NO|wx.ICON_INFORMATION) != wx.YES:
|
|
867
|
+
self.message("The close has been canceled.")
|
|
868
|
+
evt.Veto()
|
|
869
|
+
return
|
|
890
870
|
evt.Skip()
|
|
891
|
-
|
|
871
|
+
|
|
892
872
|
def Destroy(self):
|
|
893
|
-
## for name in list(self.plugins):
|
|
894
|
-
## self.unload_plug(name) # => plug.Destroy
|
|
895
873
|
self._mgr.UnInit()
|
|
896
874
|
return mwx.Frame.Destroy(self)
|
|
897
|
-
|
|
875
|
+
|
|
898
876
|
## --------------------------------
|
|
899
|
-
## pane window interface
|
|
877
|
+
## pane window interface.
|
|
900
878
|
## --------------------------------
|
|
901
|
-
|
|
879
|
+
|
|
902
880
|
def get_pane(self, name):
|
|
903
881
|
"""Get named pane or notebook pane.
|
|
904
882
|
|
|
905
883
|
Args:
|
|
906
|
-
name
|
|
884
|
+
name: plug name or object.
|
|
907
885
|
"""
|
|
908
886
|
plug = self.get_plug(name)
|
|
909
887
|
if plug:
|
|
910
888
|
name = plug.category or plug
|
|
911
889
|
if name:
|
|
912
890
|
return self._mgr.GetPane(name)
|
|
913
|
-
|
|
891
|
+
|
|
914
892
|
def show_pane(self, name, show=True, interactive=False):
|
|
915
|
-
"""Show named pane or notebook pane.
|
|
893
|
+
"""Show named pane or notebook pane.
|
|
894
|
+
|
|
895
|
+
Args:
|
|
896
|
+
name: plug name or object.
|
|
897
|
+
show: Show or hide the pane window.
|
|
898
|
+
interactive: If True, modifier keys can be used to reset or reload
|
|
899
|
+
the plugin when showing the pane:
|
|
900
|
+
- [S-menu] Reset floating position.
|
|
901
|
+
- [M-S-menu] Reload plugin.
|
|
902
|
+
"""
|
|
916
903
|
pane = self.get_pane(name)
|
|
917
904
|
if not pane.IsOk():
|
|
918
905
|
return
|
|
919
906
|
|
|
920
907
|
## Set the graph and output window sizes to half & half.
|
|
921
|
-
##
|
|
908
|
+
## ドッキング時に再計算される.
|
|
922
909
|
if name == "output" or name is self.output:
|
|
923
910
|
w, h = self.graph.GetClientSize()
|
|
924
|
-
pane.best_size = (w//2 - 3, h)
|
|
911
|
+
pane.best_size = (w//2 - 3, h) # 分割線幅補正 -12pix (Windows only ?)
|
|
925
912
|
|
|
926
913
|
## Force Layer windows to show.
|
|
927
914
|
if interactive:
|
|
@@ -937,12 +924,8 @@ class Frame(mwx.Frame):
|
|
|
937
924
|
pane.Float()
|
|
938
925
|
show = True
|
|
939
926
|
|
|
940
|
-
|
|
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)
|
|
927
|
+
plug = self.get_plug(name) # -> None if pane.window is a Graph
|
|
928
|
+
win = pane.window
|
|
946
929
|
try:
|
|
947
930
|
shown = plug.IsShown()
|
|
948
931
|
except AttributeError:
|
|
@@ -952,33 +935,30 @@ class Frame(mwx.Frame):
|
|
|
952
935
|
if isinstance(win, aui.AuiNotebook):
|
|
953
936
|
j = win.GetPageIndex(plug)
|
|
954
937
|
if j != win.Selection:
|
|
955
|
-
win.Selection = j
|
|
938
|
+
win.Selection = j # the focus moves => EVT_SHOW
|
|
956
939
|
else:
|
|
957
940
|
plug.handler('page_shown', plug)
|
|
958
941
|
else:
|
|
959
942
|
win.handler('page_shown', win)
|
|
943
|
+
if plug:
|
|
944
|
+
plug.SetFocus() # plugins only
|
|
960
945
|
elif not show and shown:
|
|
961
946
|
if isinstance(win, aui.AuiNotebook):
|
|
962
|
-
for plug in win.
|
|
947
|
+
for plug in win.get_pages():
|
|
963
948
|
plug.handler('page_closed', plug)
|
|
964
949
|
else:
|
|
965
950
|
win.handler('page_closed', win)
|
|
966
951
|
|
|
967
|
-
if pane.dock_direction:
|
|
968
|
-
pane.Dock()
|
|
969
|
-
else:
|
|
970
|
-
pane.Float()
|
|
971
|
-
|
|
972
952
|
## Modify the floating position of the pane when displayed.
|
|
973
953
|
## Note: This is a known bug in wxWidgets 3.17 -- 3.20,
|
|
974
|
-
## and will be fixed in
|
|
954
|
+
## and will be fixed in wx ver 4.2.1.
|
|
975
955
|
if wx.Display.GetFromWindow(pane.window) == -1:
|
|
976
956
|
pane.floating_pos = wx.GetMousePosition()
|
|
977
957
|
|
|
978
958
|
pane.Show(show)
|
|
979
959
|
self._mgr.Update()
|
|
980
960
|
return (show != shown)
|
|
981
|
-
|
|
961
|
+
|
|
982
962
|
def update_pane(self, name, **props):
|
|
983
963
|
"""Update the layout of the pane (internal use only).
|
|
984
964
|
|
|
@@ -998,24 +978,36 @@ class Frame(mwx.Frame):
|
|
|
998
978
|
pane.dock_direction = dock
|
|
999
979
|
if not plug.caption:
|
|
1000
980
|
pane.CaptionVisible(False) # no caption bar
|
|
1001
|
-
pane.Gripper(dock not in (0,
|
|
981
|
+
pane.Gripper(dock not in (0,5)) # show a grip when docked
|
|
1002
982
|
pane.Dockable(dock)
|
|
1003
|
-
|
|
1004
|
-
|
|
983
|
+
|
|
984
|
+
if pane.dock_direction:
|
|
985
|
+
pane.Dock()
|
|
986
|
+
else:
|
|
987
|
+
pane.Float()
|
|
988
|
+
|
|
989
|
+
def OnPaneClose(self, evt): #<wx.aui.AuiManagerEvent>
|
|
1005
990
|
pane = evt.GetPane()
|
|
1006
991
|
win = pane.window
|
|
1007
992
|
if isinstance(win, aui.AuiNotebook):
|
|
1008
|
-
for plug in win.
|
|
993
|
+
for plug in win.get_pages():
|
|
1009
994
|
plug.handler('page_closed', plug)
|
|
1010
995
|
else:
|
|
1011
996
|
win.handler('page_closed', win)
|
|
1012
|
-
evt.Skip(False)
|
|
1013
|
-
|
|
997
|
+
evt.Skip(False) # Don't skip to avoid being called twice.
|
|
998
|
+
|
|
1014
999
|
## --------------------------------
|
|
1015
|
-
## Plugin <Layer> interface
|
|
1000
|
+
## Plugin <Layer> interface.
|
|
1016
1001
|
## --------------------------------
|
|
1017
1002
|
plugins = property(lambda self: self.__plugins)
|
|
1018
|
-
|
|
1003
|
+
|
|
1004
|
+
def register(self, cls=None, **kwargs):
|
|
1005
|
+
"""Decorator of plugin class register."""
|
|
1006
|
+
if cls is None:
|
|
1007
|
+
return lambda f: self.register(f, **kwargs)
|
|
1008
|
+
self.load_plug(cls, force=1, show=1, **kwargs)
|
|
1009
|
+
return cls
|
|
1010
|
+
|
|
1019
1011
|
def require(self, name):
|
|
1020
1012
|
"""Get named plug window.
|
|
1021
1013
|
If not found, try to load it once.
|
|
@@ -1029,183 +1021,159 @@ class Frame(mwx.Frame):
|
|
|
1029
1021
|
if self.load_plug(name) is not False:
|
|
1030
1022
|
return self.get_plug(name)
|
|
1031
1023
|
return plug
|
|
1032
|
-
|
|
1024
|
+
|
|
1033
1025
|
def get_plug(self, name):
|
|
1034
|
-
"""Get named plug window.
|
|
1035
|
-
|
|
1036
|
-
Args:
|
|
1037
|
-
name : str or plug object.
|
|
1038
|
-
"""
|
|
1026
|
+
"""Get named plug window."""
|
|
1039
1027
|
if isinstance(name, str):
|
|
1040
1028
|
if name.endswith(".py"):
|
|
1041
1029
|
name, _ = os.path.splitext(os.path.basename(name))
|
|
1042
1030
|
if name in self.plugins:
|
|
1043
1031
|
return self.plugins[name].__plug__
|
|
1044
1032
|
elif isinstance(name, LayerInterface):
|
|
1045
|
-
return name
|
|
1046
|
-
|
|
1047
|
-
|
|
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,
|
|
1033
|
+
# return name
|
|
1034
|
+
return next((x for x in self.get_all_plugs() if x is name), None)
|
|
1035
|
+
|
|
1036
|
+
def get_all_plugs(self):
|
|
1037
|
+
for name, module in self.plugins.items():
|
|
1038
|
+
yield module.__plug__
|
|
1039
|
+
|
|
1040
|
+
def load_plug(self, root, session=None, force=False, show=False,
|
|
1112
1041
|
dock=0, floating_pos=None, floating_size=None,
|
|
1113
1042
|
**kwargs):
|
|
1114
1043
|
"""Load plugin.
|
|
1115
1044
|
|
|
1116
1045
|
Args:
|
|
1117
|
-
root
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
show
|
|
1123
|
-
dock
|
|
1046
|
+
root: Plugin <Layer> module, or name of the module.
|
|
1047
|
+
Any wx.Window object can be specified (as dummy-plug).
|
|
1048
|
+
However, do not use this mode in release versions.
|
|
1049
|
+
session: Conditions for initializing the plug and starting session
|
|
1050
|
+
force: force loading even if it is already loaded
|
|
1051
|
+
show: the pane is shown after loaded
|
|
1052
|
+
dock: dock_direction (1:top, 2:right, 3:bottom, 4:left, 5:center)
|
|
1124
1053
|
floating_pos: posision of floating window
|
|
1125
1054
|
floating_size: size of floating window
|
|
1126
|
-
|
|
1127
1055
|
**kwargs: keywords for Plugin <Layer>
|
|
1128
1056
|
|
|
1129
1057
|
Returns:
|
|
1130
1058
|
None if succeeded else False
|
|
1131
1059
|
|
|
1132
1060
|
Note:
|
|
1133
|
-
The root module must
|
|
1061
|
+
The root module must contain a class Plugin <Layer>.
|
|
1134
1062
|
"""
|
|
1135
1063
|
props = dict(dock_direction=dock,
|
|
1136
1064
|
floating_pos=floating_pos,
|
|
1137
1065
|
floating_size=floating_size)
|
|
1138
1066
|
|
|
1139
|
-
|
|
1067
|
+
if inspect.ismodule(root):
|
|
1068
|
+
name = root.__name__
|
|
1069
|
+
elif inspect.isclass(root):
|
|
1070
|
+
name = root.__module__
|
|
1071
|
+
else:
|
|
1072
|
+
name = root
|
|
1140
1073
|
|
|
1141
|
-
|
|
1142
|
-
if
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1074
|
+
dirname_, name = os.path.split(name) # if the name is full-path:str
|
|
1075
|
+
if name.endswith(".py"):
|
|
1076
|
+
name = name[:-3]
|
|
1077
|
+
|
|
1078
|
+
if not force:
|
|
1079
|
+
## 文字列参照 (full-path) による重複ロードを避ける.
|
|
1080
|
+
module = next((v for v in self.plugins.values() if root == v.__file__), None)
|
|
1081
|
+
if module:
|
|
1082
|
+
plug = module.__plug__
|
|
1083
|
+
else:
|
|
1084
|
+
plug = self.get_plug(name)
|
|
1085
|
+
## Check if the named plug is already loaded.
|
|
1086
|
+
if plug:
|
|
1087
|
+
self.update_pane(name, **props)
|
|
1088
|
+
self.show_pane(name, show)
|
|
1089
|
+
try:
|
|
1090
|
+
if session:
|
|
1091
|
+
plug.load_session(session)
|
|
1092
|
+
except Exception:
|
|
1093
|
+
traceback.print_exc() # Failed to load the plug session.
|
|
1094
|
+
return None
|
|
1095
|
+
|
|
1096
|
+
## Update the include-path to load the module correctly.
|
|
1097
|
+
if os.path.isdir(dirname_):
|
|
1098
|
+
if dirname_ in sys.path:
|
|
1099
|
+
sys.path.remove(dirname_)
|
|
1100
|
+
sys.path.insert(0, dirname_)
|
|
1101
|
+
elif dirname_:
|
|
1102
|
+
print(f"- No such directory {dirname_!r}.")
|
|
1103
|
+
return False
|
|
1152
1104
|
|
|
1153
|
-
module
|
|
1154
|
-
|
|
1155
|
-
|
|
1105
|
+
## Load or reload the module, and check whether it contains a class named `Plugin`.
|
|
1106
|
+
try:
|
|
1107
|
+
## Check if the module is reloadable.
|
|
1108
|
+
loadable = not name.startswith(("__main__", "builtins")) # no __file__
|
|
1109
|
+
if not loadable:
|
|
1110
|
+
module = types.ModuleType(name) # dummy module (cannot reload)
|
|
1111
|
+
module.__file__ = "<scratch>"
|
|
1112
|
+
elif name in sys.modules:
|
|
1113
|
+
module = reload(sys.modules[name])
|
|
1114
|
+
else:
|
|
1115
|
+
module = import_module(name)
|
|
1116
|
+
except Exception:
|
|
1117
|
+
traceback.print_exc() # Unable to load the module.
|
|
1118
|
+
return False
|
|
1119
|
+
else:
|
|
1120
|
+
## Register dummy plug; Add module.Plugin <Layer>.
|
|
1121
|
+
if not hasattr(module, 'Plugin'):
|
|
1122
|
+
if inspect.isclass(root):
|
|
1123
|
+
module.__dummy_plug__ = root.__name__
|
|
1124
|
+
root.reloadable = loadable
|
|
1125
|
+
_register__dummy_plug__(root, module)
|
|
1126
|
+
else:
|
|
1127
|
+
if hasattr(module, '__dummy_plug__'):
|
|
1128
|
+
root = getattr(module, module.__dummy_plug__)
|
|
1129
|
+
root.reloadable = loadable
|
|
1130
|
+
_register__dummy_plug__(root, module)
|
|
1156
1131
|
|
|
1132
|
+
## Note: name (module.__name__) != Plugin.__module__ if module is a package.
|
|
1157
1133
|
try:
|
|
1158
|
-
|
|
1159
|
-
title =
|
|
1160
|
-
|
|
1161
|
-
pane = self._mgr.GetPane(title)
|
|
1162
|
-
|
|
1163
|
-
if pane.IsOk(): # <pane:title> is already registered
|
|
1164
|
-
nb = pane.window
|
|
1165
|
-
if not isinstance(nb, aui.AuiNotebook):
|
|
1166
|
-
raise NameError("Notebook name must not be the same as any other plugins")
|
|
1134
|
+
Plugin = module.Plugin # Check if the module has a class `Plugin`.
|
|
1135
|
+
title = Plugin.category # Plugin <LayerInterface>
|
|
1167
1136
|
|
|
1168
|
-
pane = self.
|
|
1137
|
+
pane = self._mgr.GetPane(title) # Check if <pane:title> is already registered.
|
|
1138
|
+
if pane.IsOk():
|
|
1139
|
+
if not isinstance(pane.window, aui.AuiNotebook):
|
|
1140
|
+
raise NameError("Notebook name must not be the same as any other plugin")
|
|
1169
1141
|
|
|
1170
|
-
|
|
1142
|
+
pane = self.get_pane(name) # Check if <pane:name> is already registered.
|
|
1143
|
+
if pane.IsOk():
|
|
1171
1144
|
if name not in self.plugins:
|
|
1172
|
-
raise NameError("Plugin name must not be the same as any other
|
|
1173
|
-
|
|
1174
|
-
show = show or pane.IsShown()
|
|
1175
|
-
props.update(
|
|
1176
|
-
dock_direction = pane.IsDocked() and pane.dock_direction,
|
|
1177
|
-
floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
|
|
1178
|
-
floating_size = floating_size or pane.floating_size[:], # copy unloading pane
|
|
1179
|
-
)
|
|
1145
|
+
raise NameError("Plugin name must not be the same as any other pane")
|
|
1146
|
+
|
|
1180
1147
|
except (AttributeError, NameError) as e:
|
|
1181
1148
|
traceback.print_exc()
|
|
1182
|
-
wx.CallAfter(wx.MessageBox,
|
|
1149
|
+
wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
|
|
1183
1150
|
f"{e}\n\n" + traceback.format_exc(),
|
|
1184
1151
|
f"Error in loading {module.__name__!r}",
|
|
1185
1152
|
style=wx.ICON_ERROR)
|
|
1186
1153
|
return False
|
|
1187
1154
|
|
|
1188
|
-
##
|
|
1155
|
+
## Unload the plugin if loaded.
|
|
1189
1156
|
if pane.IsOk():
|
|
1190
|
-
|
|
1157
|
+
show = show or pane.IsShown()
|
|
1158
|
+
props.update(
|
|
1159
|
+
dock_direction = pane.IsDocked() and pane.dock_direction,
|
|
1160
|
+
floating_pos = floating_pos or pane.floating_pos[:], # copy unloading pane
|
|
1161
|
+
floating_size = floating_size or pane.floating_size[:], # copy unloading pane
|
|
1162
|
+
)
|
|
1163
|
+
self.unload_plug(name)
|
|
1191
1164
|
|
|
1165
|
+
## Create the plugin object.
|
|
1192
1166
|
try:
|
|
1193
|
-
plug =
|
|
1167
|
+
plug = Plugin(self, session, **kwargs)
|
|
1194
1168
|
except Exception as e:
|
|
1195
1169
|
traceback.print_exc()
|
|
1196
|
-
wx.CallAfter(wx.MessageBox,
|
|
1170
|
+
wx.CallAfter(wx.MessageBox, # Show the message after load_session has finished.
|
|
1197
1171
|
f"{e}\n\n" + traceback.format_exc(),
|
|
1198
1172
|
f"Error in loading {name!r}",
|
|
1199
1173
|
style=wx.ICON_ERROR)
|
|
1200
1174
|
return False
|
|
1201
1175
|
|
|
1202
|
-
##
|
|
1203
|
-
self.plugins[name] = module
|
|
1204
|
-
|
|
1205
|
-
## set reference of a plug (one module, one plugin)
|
|
1206
|
-
module.__plug__ = plug
|
|
1207
|
-
|
|
1208
|
-
## Create pane or notebook pane
|
|
1176
|
+
## Create pane or notebook pane.
|
|
1209
1177
|
caption = plug.caption
|
|
1210
1178
|
if not isinstance(caption, str):
|
|
1211
1179
|
caption = name
|
|
@@ -1217,7 +1185,7 @@ class Frame(mwx.Frame):
|
|
|
1217
1185
|
nb = pane.window
|
|
1218
1186
|
nb.AddPage(plug, caption)
|
|
1219
1187
|
else:
|
|
1220
|
-
size = plug.GetSize() + (2,30)
|
|
1188
|
+
size = plug.GetSize() + (2,30) # padding for notebook
|
|
1221
1189
|
nb = AuiNotebook(self, name=title)
|
|
1222
1190
|
nb.AddPage(plug, caption)
|
|
1223
1191
|
self._mgr.AddPane(nb, aui.AuiPaneInfo()
|
|
@@ -1233,17 +1201,23 @@ class Frame(mwx.Frame):
|
|
|
1233
1201
|
.Name(name).Caption(caption)
|
|
1234
1202
|
.FloatingSize(size).MinSize(size).Show(0))
|
|
1235
1203
|
|
|
1204
|
+
## Add to the list after the plug is created successfully.
|
|
1205
|
+
self.plugins[name] = module
|
|
1206
|
+
|
|
1207
|
+
## Set reference of a plug (one module, one plugin).
|
|
1208
|
+
module.__plug__ = plug
|
|
1209
|
+
|
|
1236
1210
|
## Set winow.Name for inspection.
|
|
1237
1211
|
plug.Name = name
|
|
1238
1212
|
|
|
1239
1213
|
self.update_pane(name, **props)
|
|
1240
1214
|
self.show_pane(name, show)
|
|
1241
1215
|
|
|
1242
|
-
## Create a menu
|
|
1216
|
+
## Create a menu.
|
|
1243
1217
|
plug.__Menu_item = None
|
|
1244
1218
|
|
|
1245
|
-
if not hasattr(module, 'ID_'):
|
|
1246
|
-
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)]
|
|
1247
1221
|
try:
|
|
1248
1222
|
__plug_ID__
|
|
1249
1223
|
except NameError:
|
|
@@ -1258,26 +1232,26 @@ class Frame(mwx.Frame):
|
|
|
1258
1232
|
hint = (plug.__doc__ or name).strip().splitlines()[0]
|
|
1259
1233
|
plug.__Menu_item = (
|
|
1260
1234
|
module.ID_, text, hint, wx.ITEM_CHECK,
|
|
1261
|
-
lambda v: self.
|
|
1235
|
+
lambda v: (self.update_pane(name),
|
|
1236
|
+
self.show_pane(name, v.IsChecked(), interactive=1)),
|
|
1262
1237
|
lambda v: v.Check(plug.IsShown()),
|
|
1263
1238
|
)
|
|
1264
1239
|
if menu not in self.menubar:
|
|
1265
1240
|
self.menubar[menu] = []
|
|
1266
1241
|
self.menubar[menu] += [plug.__Menu_item]
|
|
1267
1242
|
self.menubar.update(menu)
|
|
1243
|
+
|
|
1244
|
+
self.handler('plug_loaded', plug)
|
|
1268
1245
|
return None
|
|
1269
|
-
|
|
1246
|
+
|
|
1270
1247
|
def unload_plug(self, name):
|
|
1271
1248
|
"""Unload plugin and detach the pane from UI manager."""
|
|
1272
1249
|
plug = self.get_plug(name)
|
|
1273
1250
|
if not plug:
|
|
1251
|
+
print(f"- {name!r} is not listed in plugins.")
|
|
1274
1252
|
return
|
|
1275
1253
|
|
|
1276
|
-
|
|
1277
|
-
if name not in self.plugins:
|
|
1278
|
-
return
|
|
1279
|
-
|
|
1280
|
-
del self.plugins[name]
|
|
1254
|
+
del self.plugins[plug.Name]
|
|
1281
1255
|
|
|
1282
1256
|
if plug.__Menu_item:
|
|
1283
1257
|
menu, sep, tail = plug.menukey.rpartition('/')
|
|
@@ -1288,91 +1262,87 @@ class Frame(mwx.Frame):
|
|
|
1288
1262
|
if isinstance(plug.Parent, aui.AuiNotebook):
|
|
1289
1263
|
nb = plug.Parent
|
|
1290
1264
|
j = nb.GetPageIndex(plug)
|
|
1291
|
-
nb.RemovePage(j)
|
|
1292
|
-
|
|
1265
|
+
nb.RemovePage(j) # just remove page
|
|
1266
|
+
# nb.DeletePage(j) # Destroys plug object too.
|
|
1293
1267
|
else:
|
|
1294
1268
|
nb = None
|
|
1295
1269
|
self._mgr.DetachPane(plug)
|
|
1296
1270
|
self._mgr.Update()
|
|
1297
1271
|
|
|
1298
|
-
|
|
1272
|
+
self.handler('plug_unloaded', plug)
|
|
1273
|
+
plug.handler('page_closed', plug) # (even if not shown)
|
|
1299
1274
|
plug.Destroy()
|
|
1300
1275
|
|
|
1301
1276
|
if nb and not nb.PageCount:
|
|
1302
|
-
self._mgr.DetachPane(nb)
|
|
1277
|
+
self._mgr.DetachPane(nb) # detach notebook pane
|
|
1303
1278
|
self._mgr.Update()
|
|
1304
1279
|
nb.Destroy()
|
|
1305
|
-
|
|
1280
|
+
|
|
1306
1281
|
def reload_plug(self, name):
|
|
1282
|
+
"""Reload plugin."""
|
|
1307
1283
|
plug = self.get_plug(name)
|
|
1308
|
-
if not plug
|
|
1284
|
+
if not plug:
|
|
1285
|
+
print(f"- {name!r} is not listed in plugins.")
|
|
1309
1286
|
return
|
|
1287
|
+
|
|
1310
1288
|
session = {}
|
|
1311
1289
|
try:
|
|
1312
|
-
print("Reloading {}..."
|
|
1290
|
+
print(f"Reloading {plug}...")
|
|
1313
1291
|
plug.save_session(session)
|
|
1314
1292
|
except Exception:
|
|
1315
|
-
traceback.print_exc()
|
|
1316
|
-
|
|
1293
|
+
traceback.print_exc() # Failed to save the plug session.
|
|
1294
|
+
|
|
1317
1295
|
self.load_plug(plug.__module__, force=1, session=session)
|
|
1318
1296
|
|
|
1319
|
-
## Update shell.target --> new plug
|
|
1320
|
-
for shell in self.shellframe.
|
|
1297
|
+
## Update shell.target --> new plug.
|
|
1298
|
+
for shell in self.shellframe.get_all_shells():
|
|
1321
1299
|
if shell.target is plug:
|
|
1322
1300
|
shell.handler('shell_activated', shell)
|
|
1323
|
-
|
|
1324
|
-
@ignore(ResourceWarning)
|
|
1325
|
-
def edit_plug(self, name):
|
|
1326
|
-
plug = self.get_plug(name)
|
|
1327
|
-
if not plug:
|
|
1328
|
-
return
|
|
1329
|
-
|
|
1330
|
-
Popen([self.EDITOR, inspect.getmodule(plug).__file__])
|
|
1331
|
-
|
|
1301
|
+
|
|
1332
1302
|
def inspect_plug(self, name):
|
|
1333
|
-
"""Dive into the process to inspect plugs in the shell.
|
|
1334
|
-
"""
|
|
1303
|
+
"""Dive into the process to inspect plugs in the shell."""
|
|
1335
1304
|
plug = self.get_plug(name)
|
|
1336
1305
|
if not plug:
|
|
1306
|
+
print(f"- {name!r} is not listed in plugins.")
|
|
1337
1307
|
return
|
|
1338
1308
|
|
|
1339
1309
|
shell = self.shellframe.clone_shell(plug)
|
|
1310
|
+
name = plug.Name # init(shell) で名前を参照するため再定義する
|
|
1340
1311
|
|
|
1341
1312
|
@shell.handler.bind("shell_activated")
|
|
1342
1313
|
def init(shell):
|
|
1314
|
+
"""Called when the plug shell is activated."""
|
|
1343
1315
|
nonlocal plug
|
|
1344
1316
|
_plug = self.get_plug(name)
|
|
1345
1317
|
if _plug is not plug:
|
|
1346
|
-
shell.target = _plug or self
|
|
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
|
|