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/controls.py CHANGED
@@ -1,7 +1,10 @@
1
1
  #! python3
2
2
  """mwxlib param controller and wx custom controls.
3
3
  """
4
+ from contextlib import contextmanager
4
5
  from itertools import chain
6
+ import io
7
+ import re
5
8
  import wx
6
9
  import wx.lib.platebtn as pb
7
10
  import wx.lib.scrolledpanel as scrolled
@@ -12,7 +15,7 @@ from .utilus import funcall as _F
12
15
  from .framework import pack, Menu, CtrlInterface
13
16
 
14
17
  import numpy as np
15
- from numpy import nan, inf # noqa: necessary to eval
18
+ from numpy import nan, inf # noqa # necessary to eval
16
19
 
17
20
 
18
21
  def _Tip(*tips):
@@ -21,25 +24,26 @@ def _Tip(*tips):
21
24
 
22
25
 
23
26
  class Param:
24
- """Standard Parameter
27
+ """Standard Parameter.
25
28
 
26
29
  Args:
27
- name : label
28
- range : list of values
29
- value : std_value (default is None)
30
- fmt : text formatter or format:str (default is '%g')
31
- `hex` specifies hexadecimal format
32
- handler : called when knob is handled.
33
- updater : called when button is pressed.
34
- checker : called when tick turns on/off.
30
+ name: label
31
+ range: list of values
32
+ value: std_value (default is None)
33
+ fmt: text formatter or format:str (default is '%g')
34
+ `hex` specifies hexadecimal format
35
+ handler: called when knob is handled.
36
+ updater: called when button is pressed.
37
+ checker: called when tick turns on/off.
35
38
 
36
39
  Attributes:
37
- knobs : knob list
38
- callback : single state machine that handles following events
40
+ knobs: knob list
41
+ callback: single state machine that handles following events
39
42
 
40
43
  - control -> when index is changed by knobs or reset (handler)
41
44
  - updated -> when button is pressed (updater)
42
45
  - checked -> when tick turns on/off (checker)
46
+ - notified -> when value changed
43
47
  - overflow -> when value overflows
44
48
  - underflow -> when value underflows
45
49
  """
@@ -57,34 +61,36 @@ class Param:
57
61
  else:
58
62
  self.__eval = lambda v: eval(v)
59
63
  self.__format = fmt or "{:,g}".format
60
- if isinstance(fmt, str): # support %-format:str (deprecated)
64
+ if isinstance(fmt, str): # support %-format:str (deprecated)
61
65
  self.__format = lambda v: fmt % v
62
66
  self.callback = SSM({
63
67
  'control' : [ _F(handler) ] if handler else [],
64
68
  'updated' : [ _F(updater) ] if updater else [],
65
69
  'checked' : [ _F(checker) ] if checker else [],
70
+ 'notified' : [],
66
71
  'overflow' : [],
67
72
  'underflow' : [],
68
73
  })
69
- self._tooltip = _Tip(handler.__doc__, updater.__doc__)
70
-
74
+ self._tooltip = _Tip(handler.__doc__,
75
+ updater.__doc__)
76
+
71
77
  def __str__(self, v=None):
72
78
  if v is None:
73
79
  v = self.value
74
80
  try:
75
81
  return self.__format(v)
76
- except ValueError:
82
+ except (TypeError, ValueError):
77
83
  return str(v)
78
-
84
+
79
85
  def __int__(self):
80
86
  return int(self.value)
81
-
87
+
82
88
  def __float__(self):
83
89
  return float(self.value)
84
-
90
+
85
91
  def __len__(self):
86
92
  return len(self.range)
87
-
93
+
88
94
  def reset(self, v=None, internal_callback=True):
89
95
  """Reset value when indexed (by knobs) with callback."""
90
96
  if v is None:
@@ -93,51 +99,51 @@ class Param:
93
99
  return
94
100
  elif isinstance(v, str):
95
101
  try:
96
- v = self.__eval(v.replace(',', '')) # eliminates commas; includes nan, inf
102
+ v = self.__eval(v.replace(',', '')) # Eliminates commas.
97
103
  except Exception:
98
104
  v = self.value
99
105
  internal_callback = False
100
106
  self.value = v
101
107
  if internal_callback:
102
108
  self.callback('control', self)
103
-
109
+
104
110
  @property
105
111
  def check(self):
106
112
  """A knob check-flag (user defined)."""
107
113
  return self.__check
108
-
114
+
109
115
  @check.setter
110
116
  def check(self, v):
111
117
  self.__check = bool(v)
112
118
  self.callback('checked', self)
113
-
119
+
114
120
  @property
115
121
  def name(self):
116
122
  return self.__name
117
-
123
+
118
124
  @name.setter
119
125
  def name(self, v):
120
126
  self.__name = v
121
127
  for knob in self.knobs:
122
128
  knob.update_label()
123
-
129
+
124
130
  @property
125
131
  def value(self):
126
132
  """Current value := std_value + offset."""
127
133
  return self.__value
128
-
134
+
129
135
  @value.setter
130
136
  def value(self, v):
131
137
  if v is None:
132
138
  v = nan
133
- if np.isnan(v) or np.isinf(v):
139
+ if np.isnan(v) or np.isinf(v): # Skip events for nan and inf.
134
140
  self.__value = v
135
141
  for knob in self.knobs:
136
- knob.update_ctrl(None, notify=False)
142
+ knob.update_control(None)
137
143
  return
138
144
  elif v == self.__value:
139
145
  for knob in self.knobs:
140
- knob.update_ctrl(True, notify=False)
146
+ knob.update_control()
141
147
  return
142
148
 
143
149
  ## If the value is out of range, it will be modified.
@@ -151,13 +157,14 @@ class Param:
151
157
  self.__value = self.max
152
158
  self.callback('overflow', self)
153
159
  for knob in self.knobs:
154
- knob.update_ctrl(valid, notify=True)
155
-
160
+ knob.update_control(valid, notify=True)
161
+ self.callback('notified', self)
162
+
156
163
  @property
157
164
  def std_value(self):
158
165
  """A standard value (default None)."""
159
166
  return self.__std_value
160
-
167
+
161
168
  @std_value.setter
162
169
  def std_value(self, v):
163
170
  if v is None:
@@ -165,7 +172,7 @@ class Param:
165
172
  self.__std_value = v
166
173
  for knob in self.knobs:
167
174
  knob.update_label()
168
-
175
+
169
176
  @property
170
177
  def offset(self):
171
178
  """Offset value
@@ -174,38 +181,40 @@ class Param:
174
181
  if not np.isnan(self.std_value):
175
182
  return self.value - self.std_value
176
183
  return self.value
177
-
184
+
178
185
  @offset.setter
179
186
  def offset(self, v):
180
187
  if not np.isnan(self.std_value):
181
188
  v += self.std_value
182
189
  self.value = v
183
-
190
+
184
191
  min = property(lambda self: self.__range[0] if self else nan)
185
192
  max = property(lambda self: self.__range[-1] if self else nan)
186
-
193
+
187
194
  @property
188
195
  def range(self):
189
196
  """Index range."""
190
197
  return self.__range
191
-
198
+
192
199
  @range.setter
193
200
  def range(self, v):
194
201
  if v is None:
195
- self.__range = [] # dummy data
202
+ self.__range = [] # dummy data
196
203
  else:
197
204
  self.__range = sorted(v)
198
205
  for knob in self.knobs:
199
- knob.update_range() # list range of related knobs
200
-
206
+ knob.update_range() # list range of related knobs
207
+
201
208
  @property
202
209
  def index(self):
203
210
  """A knob index -> value.
204
- Returns -1 if the value is not defined."""
205
- if self:
206
- return int(np.searchsorted(self.range, self.value))
207
- return -1
208
-
211
+ Returns -1 if the value is not defined.
212
+ """
213
+ v = self.value
214
+ if np.isnan(v) or np.isinf(v):
215
+ return -1
216
+ return int(np.searchsorted(self.range, v))
217
+
209
218
  @index.setter
210
219
  def index(self, j):
211
220
  if self:
@@ -214,40 +223,41 @@ class Param:
214
223
 
215
224
 
216
225
  class LParam(Param):
217
- """Linear Parameter
226
+ """Linear Parameter.
218
227
 
219
228
  Args:
220
- name : label
221
- range : range params [min:max:step]
222
- value : std_value (default is None)
223
- fmt : text formatter or format:str (default is '%g')
224
- `hex` specifies hexadecimal format
225
- handler : called when knob is handled.
226
- updater : called when button is pressed.
227
- checker : called when tick turns on/off.
229
+ name: label
230
+ range: range params [min:max:step]
231
+ value: std_value (default is None)
232
+ fmt: text formatter or format:str (default is '%g')
233
+ `hex` specifies hexadecimal format
234
+ handler: called when knob is handled.
235
+ updater: called when button is pressed.
236
+ checker: called when tick turns on/off.
228
237
 
229
238
  Attributes:
230
- knobs : knob list
231
- callback : single state machine that handles following events
239
+ knobs: knob list
240
+ callback: single state machine that handles following events
232
241
 
233
242
  - control -> when index is changed by knobs or reset (handler)
234
243
  - updated -> when button is pressed (updater)
235
244
  - checked -> when tick turns on/off (checker)
245
+ - notified -> when value changed
236
246
  - overflow -> when value overflows
237
247
  - underflow -> when value underflows
238
248
  """
239
249
  min = property(lambda self: self.__min)
240
250
  max = property(lambda self: self.__max)
241
251
  step = property(lambda self: self.__step)
242
-
252
+
243
253
  def __len__(self):
244
- return 1 + int(round((self.max - self.min) / self.step)) # includes [min,max]
245
-
254
+ return 1 + int(round((self.max - self.min) / self.step)) # includes [min,max]
255
+
246
256
  @property
247
257
  def range(self):
248
258
  """Index range."""
249
259
  return np.arange(self.min, self.max + self.step, self.step)
250
-
260
+
251
261
  @range.setter
252
262
  def range(self, v):
253
263
  assert v is None or len(v) <= 3, "The range must be of length <= 3 or None"
@@ -257,8 +267,8 @@ class LParam(Param):
257
267
  self.__max = v[1]
258
268
  self.__step = v[2] if len(v) > 2 else 1
259
269
  for knob in self.knobs:
260
- knob.update_range() # linear range of related knobs
261
-
270
+ knob.update_range() # linear range of related knobs
271
+
262
272
  @property
263
273
  def index(self):
264
274
  """A knob index -> value
@@ -267,19 +277,19 @@ class LParam(Param):
267
277
  v = self.value
268
278
  if np.isnan(v) or np.isinf(v):
269
279
  return -1
270
- return int(round((self.value - self.min) / self.step))
271
-
280
+ return int(round((v - self.min) / self.step))
281
+
272
282
  @index.setter
273
283
  def index(self, j):
274
284
  self.value = self.min + j * self.step
275
285
 
276
286
 
277
287
  ## --------------------------------
278
- ## Knob unit for Parameter Control
288
+ ## Knob unit for Parameter Control.
279
289
  ## --------------------------------
280
290
 
281
291
  class Knob(wx.Panel):
282
- """Parameter controller unit
292
+ """Parameter controller unit.
283
293
 
284
294
  In addition to direct key input to the textctrl,
285
295
  [up][down][wheelup][wheeldown] keys can be used,
@@ -287,41 +297,47 @@ class Knob(wx.Panel):
287
297
  [Mbutton] resets to the std. value if it exists.
288
298
 
289
299
  Args:
290
- param : <Param> or <LParam> object
291
- type : ctrl type (slider[*], [hv]spin, choice, None)
292
- style : style of label
293
- None -> static text (default)
294
- button -> label with flat button
295
- chkbox -> label with checkbox
296
- checkbox -> label with checkbox
297
- cw : width of ctrl
298
- lw : width of label
299
- tw : width of textbox
300
- h : height of widget (defaults to 22)
300
+ param: <Param> or <LParam> object
301
+ type: control type (slider[*], [hv]spin, choice, None)
302
+ style: style of label
303
+ None -> static text (default)
304
+ button -> label with flat button
305
+ checkbox -> label with checkbox
306
+ cw: width of control
307
+ lw: width of label
308
+ tw: width of textbox
309
+ h: height of widget (defaults to 22)
301
310
  """
302
311
  @property
303
312
  def param(self):
304
313
  """Param object referred from knobs."""
305
314
  return self.__par
306
-
315
+
307
316
  @param.setter
308
317
  def param(self, v):
309
318
  self.__par.knobs.remove(self)
310
319
  self.__par = v
311
320
  self.__par.knobs.append(self)
312
321
  self.update_range()
313
- self.update_ctrl()
314
-
322
+ self.update_control()
323
+
324
+ @property
325
+ def button(self):
326
+ if isinstance(self._label, pb.PlateButton):
327
+ return self._label
328
+
329
+ @property
330
+ def control(self):
331
+ return self._ctrl
332
+
315
333
  def __init__(self, parent, param, type=None,
316
334
  style=None, cw=-1, lw=-1, tw=-1, h=22, **kwargs):
317
335
  wx.Panel.__init__(self, parent, **kwargs)
318
336
 
319
- assert isinstance(param, Param),\
320
- "Argument `param` must be an instance of Param"
337
+ assert isinstance(param, Param), "Argument `param` must be an instance of Param"
321
338
 
322
- self.__bit = 1
323
339
  self.__par = param
324
- self.__par.knobs.append(self) # パラメータの関連付けを行う
340
+ self.__par.knobs.append(self) # パラメータの関連付けを行う
325
341
 
326
342
  if not type:
327
343
  type = 'slider'
@@ -336,211 +352,213 @@ class Knob(wx.Panel):
336
352
  label = self.__par.name + ' '
337
353
 
338
354
  if style == 'chkbox' or style == 'checkbox':
339
- if lw >= 0:
340
- lw += 16
341
- self.label = wx.CheckBox(self, label=label, size=(lw,-1))
342
- self.label.Bind(wx.EVT_CHECKBOX, self.OnCheck)
355
+ ## Keep margin for the checkbox: lw += 16
356
+ self._label = wx.CheckBox(self, label=label, size=(lw,-1))
357
+ self._label.Bind(wx.EVT_CHECKBOX, self.OnCheck)
343
358
  elif style == 'button':
344
- if lw >= 0:
345
- lw += 16
346
- self.label = pb.PlateButton(self, label=label, size=(lw,-1),
347
- style=pb.PB_STYLE_DEFAULT|pb.PB_STYLE_SQUARE)
348
- self.label.Bind(wx.EVT_BUTTON, self.OnPress)
359
+ ## Keep margin for the button: lw += 16
360
+ self._label = pb.PlateButton(self, label=label, size=(lw,-1),
361
+ style=pb.PB_STYLE_DEFAULT|pb.PB_STYLE_SQUARE)
362
+ self._label.Bind(wx.EVT_BUTTON, self.OnPress)
349
363
  elif not style:
350
- self.label = wx.StaticText(self, label=label, size=(lw,-1))
364
+ self._label = wx.StaticText(self, label=label, size=(lw,-1))
351
365
  else:
352
- raise Exception("unknown style: {!r}".format(style))
366
+ raise Exception(f"unknown style: {style!r}")
353
367
 
354
- self.label.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
355
- self.label.SetToolTip(self.__par._tooltip)
356
- self.label.Enable(lw) # skip focus
368
+ self._label.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
369
+ self._label.SetToolTip(self.__par._tooltip)
370
+ self._label.Enable(lw) # skip focus
357
371
 
358
- self.text = wx.TextCtrl(self, size=(tw,h), style=wx.TE_PROCESS_ENTER)
359
- self.text.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
360
- self.text.Bind(wx.EVT_KILL_FOCUS, self.OnTextExit)
361
- self.text.Bind(wx.EVT_KEY_DOWN, self.OnTextKeyDown)
362
- self.text.Bind(wx.EVT_KEY_UP, self.OnTextKeyUp)
363
- self.text.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
364
- self.text.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
372
+ self._text = wx.TextCtrl(self, size=(tw,h), style=wx.TE_PROCESS_ENTER)
373
+ self._text.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
374
+ self._text.Bind(wx.EVT_KILL_FOCUS, self.OnTextExit)
375
+ self._text.Bind(wx.EVT_KEY_DOWN, self.OnTextKeyDown)
376
+ self._text.Bind(wx.EVT_KEY_UP, self.OnTextKeyUp)
377
+ self._text.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
378
+ self._text.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
365
379
 
366
- self.text.Enable(tw) # skip focus
380
+ self._text.Enable(tw) # skip focus
367
381
 
368
382
  if type == 'slider':
369
- self.ctrl = wx.Slider(self, size=(cw,h), style=wx.SL_HORIZONTAL)
370
- self.ctrl.Bind(wx.EVT_SCROLL_CHANGED, self.OnScroll)
371
- self.ctrl.Bind(wx.EVT_KEY_DOWN, self.OnCtrlKeyDown)
372
- self.ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
383
+ self._ctrl = wx.Slider(self, size=(cw,h), style=wx.SL_HORIZONTAL)
384
+ self._ctrl.Bind(wx.EVT_SCROLL_CHANGED, self.OnScroll)
385
+ self._ctrl.Bind(wx.EVT_KEY_DOWN, self.OnCtrlKeyDown)
386
+ self._ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
373
387
 
374
388
  elif type == 'slider*':
375
- self.ctrl = wx.Slider(self, size=(cw,h), style=wx.SL_HORIZONTAL)
376
- self.ctrl.Bind(wx.EVT_SCROLL, self.OnScroll) # called while dragging
377
- self.ctrl.Bind(wx.EVT_SCROLL_CHANGED, lambda v: None) # pass no action
378
- self.ctrl.Bind(wx.EVT_KEY_DOWN, self.OnCtrlKeyDown)
379
- self.ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
389
+ self._ctrl = wx.Slider(self, size=(cw,h), style=wx.SL_HORIZONTAL)
390
+ self._ctrl.Bind(wx.EVT_SCROLL, self.OnScroll) # called while dragging
391
+ self._ctrl.Bind(wx.EVT_SCROLL_CHANGED, lambda v: None) # pass no action
392
+ self._ctrl.Bind(wx.EVT_KEY_DOWN, self.OnCtrlKeyDown)
393
+ self._ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
380
394
 
381
- elif type == 'spin' or type =='hspin':
382
- self.ctrl = wx.SpinButton(self, size=(cw,h), style=wx.SP_HORIZONTAL)
383
- self.ctrl.Bind(wx.EVT_SPIN, self.OnScroll)
384
- self.ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
395
+ elif type == 'spin' or type == 'hspin':
396
+ self._ctrl = wx.SpinButton(self, size=(cw,h), style=wx.SP_HORIZONTAL)
397
+ self._ctrl.Bind(wx.EVT_SPIN, self.OnScroll)
398
+ self._ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
385
399
 
386
400
  elif type == 'vspin':
387
- self.ctrl = wx.SpinButton(self, size=(cw,h), style=wx.SP_VERTICAL)
388
- self.ctrl.Bind(wx.EVT_SPIN, self.OnScroll)
389
- self.ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
401
+ self._ctrl = wx.SpinButton(self, size=(cw,h), style=wx.SP_VERTICAL)
402
+ self._ctrl.Bind(wx.EVT_SPIN, self.OnScroll)
403
+ self._ctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
390
404
 
391
405
  elif type == 'choice':
392
- self.ctrl = wx.Choice(self, size=(cw,h))
393
- self.ctrl.Bind(wx.EVT_CHOICE, self.OnScroll)
394
- self.ctrl.SetValue = self.ctrl.SetSelection # setter of choice
395
- self.ctrl.GetValue = self.ctrl.GetSelection # getter (ditto)
406
+ self._ctrl = wx.Choice(self, size=(cw,h))
407
+ self._ctrl.Bind(wx.EVT_CHOICE, self.OnScroll)
408
+ self._ctrl.SetValue = self._ctrl.SetSelection # setter of choice
409
+ self._ctrl.GetValue = self._ctrl.GetSelection # getter (ditto)
396
410
 
397
411
  else:
398
- raise Exception("unknown type: {!r}".format(type))
412
+ raise Exception(f"unknown type: {type!r}")
399
413
 
400
- self.ctrl.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
401
- self.ctrl.Enable(cw) # skip focus
414
+ self._ctrl.Bind(wx.EVT_MIDDLE_DOWN, lambda v: self.__par.reset())
415
+ self._ctrl.Enable(cw) # skip focus
402
416
 
403
417
  c = (cw and type != 'vspin')
404
418
  self.SetSizer(
405
419
  pack(self, (
406
- (self.label, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, lw and 1),
407
- (self.text, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, tw and 1),
408
- (self.ctrl, c, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, cw and 1),
420
+ (self._label, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, lw and 1),
421
+ (self._text, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, tw and 1),
422
+ (self._ctrl, c, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, cw and 1),
409
423
  ))
410
424
  )
411
425
  self.update_range()
412
- self.update_ctrl()
426
+ self.update_control()
413
427
 
414
428
  self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
415
-
429
+
416
430
  def OnDestroy(self, evt):
417
- self.__par.knobs.remove(self) # パラメータの関連付けを解除する
431
+ self.__par.knobs.remove(self) # パラメータの関連付けを解除する
418
432
  evt.Skip()
419
-
433
+
420
434
  def update_range(self):
421
435
  """Called when range is being changed (internal use only)."""
422
436
  v = self.__par
423
- if isinstance(self.ctrl, wx.Choice): #<wx.Choice>
437
+ if isinstance(self._ctrl, wx.Choice): #<wx.Choice>
424
438
  items = [v.__str__(x) for x in v.range]
425
- if items != self.ctrl.Items:
426
- self.ctrl.SetItems(items)
427
- self.ctrl.SetStringSelection(str(v))
439
+ if items != self._ctrl.Items:
440
+ self._ctrl.SetItems(items)
441
+ self._ctrl.SetStringSelection(str(v))
428
442
  else:
429
- self.ctrl.SetRange(0, len(v)-1) #<wx.Slider> <wx.SpinButton>
430
-
443
+ self._ctrl.SetRange(0, len(v)-1) #<wx.Slider> #<wx.SpinButton>
444
+
431
445
  def update_label(self):
432
446
  """Called when label is being changed (internal use only)."""
433
447
  v = self.__par
434
- if isinstance(self.label, wx.CheckBox):
435
- self.label.SetValue(v.check)
448
+ if isinstance(self._label, wx.CheckBox):
449
+ self._label.SetValue(v.check)
436
450
 
437
451
  t = ' ' if np.isnan(v.std_value) or v.value == v.std_value else '*'
438
- self.label.SetLabel(v.name + t)
439
- self.label.Refresh()
452
+ self._label.SetLabel(v.name + t)
453
+ self._label.Refresh()
440
454
  self.Refresh()
441
-
442
- def update_ctrl(self, valid=True, notify=False):
455
+
456
+ def update_control(self, valid=True, notify=False):
443
457
  """Called when value is being changed (internal use only)."""
444
458
  v = self.__par
445
- self.ctrl.SetValue(v.index)
446
- wx.CallAfter(self.text.SetValue, str(v)) # for wxAssertionError
459
+ self._ctrl.SetValue(v.index)
460
+ wx.CallAfter(self._text.SetValue, str(v)) # for wxAssertionError
447
461
  if valid:
448
462
  if notify:
449
- if self.text.BackgroundColour != '#ffff80':
463
+ if self._text.BackgroundColour != '#ffff80':
450
464
  wx.CallAfter(wx.CallLater, 1000,
451
465
  self.set_textcolour, '#ffffff')
452
- self.set_textcolour('#ffff80') # light-yellow
466
+ self.set_textcolour('#ffff80') # light-yellow
453
467
  else:
454
- self.set_textcolour('#ffffff') # True: white
468
+ self.set_textcolour('#ffffff') # True: white
455
469
  else:
456
- self.set_textcolour('#ffffff') # True: white
470
+ self.set_textcolour('#ffffff') # True: white
457
471
  elif valid is None:
458
- self.set_textcolour('#ffff80') # None: light-yellow
472
+ self.set_textcolour('#ffff80') # None: light-yellow
459
473
  else:
460
- self.set_textcolour('#ff8080') # False: light-red
474
+ self.set_textcolour('#ff8080') # False: light-red
461
475
  self.update_label()
462
-
476
+
463
477
  def set_textcolour(self, c):
464
478
  if self:
465
- self.text.BackgroundColour = c
466
- self.text.Refresh()
467
-
468
- def shift(self, evt, bit):
479
+ self._text.BackgroundColour = c
480
+ self._text.Refresh()
481
+
482
+ def _shift_control(self, evt, bit):
483
+ ## Called when a key/mouse wheel is pressed/scrolled (internal use only).
484
+ ## In addition to direct key input to the textctrl,
485
+ ## [up][down][wheelup][wheeldown] keys can be used,
486
+ ## with modifiers S- 2x, C- 16x, and M- 256x steps.
469
487
  if bit:
470
488
  if evt.ShiftDown(): bit *= 2
471
489
  if evt.ControlDown(): bit *= 16
472
490
  if evt.AltDown(): bit *= 256
473
491
  v = self.__par
474
- j = self.ctrl.GetValue() + bit
492
+ j = self._ctrl.GetValue() + bit
475
493
  if j != v.index:
476
494
  v.index = j
477
495
  v.reset(v.value)
478
-
479
- def OnScroll(self, evt): #<wx._core.ScrollEvent><wx._controls.SpinEvent><wx._core.CommandEvent>
496
+
497
+ def OnScroll(self, evt): #<wx._core.ScrollEvent> #<wx._controls.SpinEvent> #<wx._core.CommandEvent>
480
498
  v = self.__par
481
- j = self.ctrl.GetValue()
499
+ j = self._ctrl.GetValue()
482
500
  if j != v.index:
483
501
  v.index = j
484
502
  v.reset(v.value)
485
503
  evt.Skip()
486
-
487
- def OnMouseWheel(self, evt): #<wx._core.MouseEvent>
488
- self.shift(evt, 1 if evt.WheelRotation>0 else -1)
504
+
505
+ def OnMouseWheel(self, evt): #<wx._core.MouseEvent>
506
+ self._shift_control(evt, (1 if evt.WheelRotation > 0 else -1))
489
507
  evt.Skip(False)
490
-
491
- def OnCtrlKeyDown(self, evt): #<wx._core.KeyEvent>
508
+
509
+ def OnCtrlKeyDown(self, evt): #<wx._core.KeyEvent>
492
510
  key = evt.GetKeyCode()
493
- if key == wx.WXK_LEFT: return self.shift(evt, -1)
494
- if key == wx.WXK_RIGHT: return self.shift(evt, 1)
511
+ if key == wx.WXK_LEFT: return self._shift_control(evt, -1)
512
+ if key == wx.WXK_RIGHT: return self._shift_control(evt, 1)
495
513
 
496
514
  def _focus(c):
497
- if isinstance(c, Knob) and c.ctrl.IsEnabled():
498
- c.ctrl.SetFocus()
515
+ if isinstance(c, Knob) and c._ctrl.IsEnabled():
516
+ c._ctrl.SetFocus()
499
517
  return True
500
518
 
501
519
  ls = next(x for x in self.Parent.layout_groups if self in x)
502
520
  i = ls.index(self)
503
521
  if key == wx.WXK_DOWN: return any(_focus(c) for c in ls[i+1:])
504
522
  if key == wx.WXK_UP: return any(_focus(c) for c in ls[i-1::-1])
505
-
506
- def OnTextKeyUp(self, evt): #<wx._core.KeyEvent>
523
+
524
+ def OnTextKeyUp(self, evt): #<wx._core.KeyEvent>
507
525
  evt.Skip()
508
-
509
- def OnTextKeyDown(self, evt): #<wx._core.KeyEvent>
526
+
527
+ def OnTextKeyDown(self, evt): #<wx._core.KeyEvent>
510
528
  key = evt.GetKeyCode()
511
- if key == wx.WXK_DOWN: return self.shift(evt, -1)
512
- if key == wx.WXK_UP: return self.shift(evt, 1)
529
+ if key == wx.WXK_DOWN: return self._shift_control(evt, -1)
530
+ if key == wx.WXK_UP: return self._shift_control(evt, 1)
513
531
  if key == wx.WXK_ESCAPE:
514
- self.__par.reset(self.__par.value, internal_callback=None) # restore value
532
+ self.__par.reset(self.__par.value, internal_callback=None) # restore value
515
533
  evt.Skip()
516
-
517
- def OnTextEnter(self, evt): #<wx._core.CommandEvent>
534
+
535
+ def OnTextEnter(self, evt): #<wx._core.CommandEvent>
518
536
  evt.Skip()
519
- x = self.text.Value.strip()
537
+ x = self._text.Value.strip()
520
538
  self.__par.reset(x)
521
-
522
- def OnTextExit(self, evt): #<wx._core.FocusEvent>
523
- x = self.text.Value.strip()
539
+
540
+ def OnTextExit(self, evt): #<wx._core.FocusEvent>
541
+ x = self._text.Value.strip()
524
542
  if x != str(self.__par):
525
543
  self.__par.reset(x)
526
544
  evt.Skip()
527
-
528
- def OnCheck(self, evt): #<wx._core.CommandEvent>
545
+
546
+ def OnCheck(self, evt): #<wx._core.CommandEvent>
529
547
  self.__par.check = evt.IsChecked()
530
548
  evt.Skip()
531
-
532
- def OnPress(self, evt): #<wx._core.CommandEvent>
549
+
550
+ def OnPress(self, evt): #<wx._core.CommandEvent>
533
551
  self.__par.callback('updated', self.__par)
534
552
  evt.Skip()
535
-
553
+
536
554
  def Enable(self, p=True):
537
- self.label.Enable(p)
538
- self.ctrl.Enable(p)
539
- self.text.Enable(p)
555
+ self._label.Enable(p)
556
+ self._ctrl.Enable(p)
557
+ self._text.Enable(p)
540
558
 
541
559
 
542
560
  class KnobCtrlPanel(scrolled.ScrolledPanel):
543
- """Scrollable Control Panel
561
+ """Scrollable Control Panel.
544
562
  """
545
563
  def __init__(self, *args, **kwargs):
546
564
  scrolled.ScrolledPanel.__init__(self, *args, **kwargs)
@@ -572,81 +590,83 @@ class KnobCtrlPanel(scrolled.ScrolledPanel):
572
590
  self.Bind(wx.EVT_SCROLLWIN_THUMBRELEASE, self.OnRecalcLayout)
573
591
  self.Bind(wx.EVT_MOUSEWHEEL, self.OnRecalcLayout)
574
592
  self.Bind(wx.EVT_LEFT_DOWN, self.OnRecalcLayout)
575
-
576
- def OnRecalcLayout(self, evt): #<wx._core.ScrollWinEvent>
593
+
594
+ def OnRecalcLayout(self, evt): #<wx._core.ScrollWinEvent>
577
595
  self.Layout()
578
596
  evt.Skip()
579
-
580
- def OnToggleFold(self, evt): #<wx._core.MouseEvent>
597
+
598
+ def OnToggleFold(self, evt): #<wx._core.MouseEvent>
581
599
  x, y = evt.Position
582
- for child in self.Sizer.Children: # child <wx._core.SizerItem>
600
+ for child in self.Sizer.Children: # child <wx._core.SizerItem>
583
601
  if child.IsShown():
584
602
  obj = child.Sizer
585
603
  if isinstance(obj, wx.StaticBoxSizer):
586
604
  cx, cy = obj.Position
587
605
  if cx < x < cx + obj.Size[0] and cy < y < cy+22:
588
- for cc in obj.Children: # child of child <wx._core.SizerItem>
606
+ for cc in obj.Children: # child of child <wx._core.SizerItem>
589
607
  cc.Show(not cc.IsShown())
590
608
  self.Layout()
609
+ self.SendSizeEvent()
591
610
  break
592
611
  evt.Skip()
593
-
612
+
594
613
  ## --------------------------------
595
- ## Layout commands and attributes
614
+ ## Layout commands and attributes.
596
615
  ## --------------------------------
597
616
  @property
598
617
  def layout_groups(self):
599
618
  return self.__groups
600
-
619
+
601
620
  def is_enabled(self, groupid, pred=all):
602
621
  return pred(win.Enabled for win in self.__groups[groupid])
603
-
622
+
604
623
  def enable(self, groupid, p=True):
605
- for win in self.__groups[groupid]: # child could be deep nesting
624
+ for win in self.__groups[groupid]: # child could be deep nesting
606
625
  win.Enable(p)
607
-
626
+
608
627
  def is_shown(self, groupid):
609
- ## child = self.Sizer.Children[groupid]
610
- ## return child.IsShown()
628
+ # child = self.Sizer.Children[groupid]
629
+ # return child.IsShown()
611
630
  return self.Sizer.IsShown(groupid % len(self.__groups))
612
-
631
+
613
632
  def show(self, groupid, p=True):
614
633
  """Show/hide all including the box."""
615
- ## child = self.Sizer.Children[groupid]
616
- ## child.Show(p)
634
+ # child = self.Sizer.Children[groupid]
635
+ # child.Show(p)
617
636
  self.Sizer.Show(groupid % len(self.__groups), p)
618
637
  self.Layout()
619
-
638
+
620
639
  def is_folded(self, groupid):
621
640
  child = self.Sizer.Children[groupid]
622
641
  return not any(cc.IsShown() for cc in child.Sizer.Children)
623
-
642
+
624
643
  def fold(self, groupid, p=True):
625
644
  """Fold/unfold the boxed group."""
626
645
  child = self.Sizer.Children[groupid]
627
646
  if isinstance(child.Sizer, wx.StaticBoxSizer) and child.IsShown():
628
- for cc in child.Sizer.Children: # child of child <wx._core.SizerItem>
647
+ for cc in child.Sizer.Children: # child of child <wx._core.SizerItem>
629
648
  cc.Show(not p)
630
649
  self.Layout()
631
-
650
+
632
651
  def layout(self, items, title=None,
633
652
  row=0, expand=0, border=2, hspacing=1, vspacing=1,
634
653
  show=True, visible=True, align=wx.ALIGN_LEFT, **kwargs):
635
654
  """Do layout (cf. Layout).
636
655
 
637
656
  Args:
638
- items : list of Params, wx.Objects, tuple of sizing, or None
639
- title : box header string (default is None - no box)
640
- row : number of row to arange widgets
641
- expand : (0) fixed size
642
- (1) to expand horizontally
643
- (2) to expand horizontally and vertically
644
- border : size of outline border
657
+ items: list of Params, wx.Objects, tuple of sizing, or None
658
+ title: box header string (default is None - no box)
659
+ row: number of row to arange widgets
660
+ expand: expansion flag
661
+ - (0) fixed size
662
+ - (1) to expand horizontally
663
+ - (2) to expand horizontally and vertically
664
+ border: size of outline border
645
665
  hspacing: horizontal spacing among packed objs inside the group
646
666
  vspacing: vertical spacing among packed objs inside the group
647
- show : fold or unfold the boxed group
648
- visible : Hide the boxed group if False
649
- align : alignment flag (wx.ALIGN_*) default is ALIGN_LEFT
667
+ show: Fold or unfold the boxed group.
668
+ visible: Hide the boxed group if False.
669
+ align: alignment flag (wx.ALIGN_*) default is ALIGN_LEFT
650
670
  **kwargs: extra keyword arguments given for Knob
651
671
  """
652
672
  objs = [Knob(self, c, **kwargs) if isinstance(c, Param)
@@ -666,70 +686,74 @@ class KnobCtrlPanel(scrolled.ScrolledPanel):
666
686
 
667
687
  self.Sizer.Add(sizer, expand>1, p | wx.ALL, border)
668
688
 
669
- ## Register object and parameter groups
670
- def _flatiter(a):
671
- for c in a:
689
+ ## Register objects and parameter groups.
690
+ def _flatiter(objects):
691
+ for c in objects:
672
692
  if isinstance(c, tuple):
673
693
  yield from _flatiter(c)
674
694
  elif isinstance(c, wx.Object):
675
695
  yield c
676
696
  self.__groups.append(list(_flatiter(objs)))
677
697
 
678
- def _variter(a):
679
- for c in a:
698
+ ## Parameters : Knob.param or widgets that have a `value`.
699
+ def _variter(objects):
700
+ for c in objects:
680
701
  if isinstance(c, Knob):
681
702
  yield c.param
682
703
  elif hasattr(c, 'value'):
683
704
  yield c
684
705
  self.__params.append(list(_variter(objs)))
685
706
 
686
- ## Set appearance of the layout group
707
+ ## Set appearance of the layout group.
687
708
  self.show(-1, visible)
688
709
  self.fold(-1, not show)
689
710
  self.Sizer.Fit(self)
690
-
711
+
712
+ return self.__groups[-1]
713
+
691
714
  ## --------------------------------
692
- ## 外部入出力/クリップボード通信
715
+ ## 外部入出力/クリップボード通信.
693
716
  ## --------------------------------
694
717
  @property
695
718
  def parameters(self):
696
719
  return [p.value for p in self.get_params()]
697
-
720
+
698
721
  @parameters.setter
699
722
  def parameters(self, v):
700
723
  self.set_params(v)
701
-
724
+
702
725
  def get_params(self, checked_only=False):
703
726
  params = chain(*self.__params)
704
727
  if not checked_only:
705
728
  return params
706
729
  return filter(lambda c: getattr(c, 'check', None), params)
707
-
708
- def set_params(self, argv=None, checked_only=False):
730
+
731
+ def set_params(self, argv, checked_only=False):
709
732
  params = self.get_params(checked_only)
710
- if argv is None:
711
- for p in params:
712
- try:
713
- p.reset()
714
- except (AttributeError, TypeError):
715
- pass
716
- else:
717
- for p, v in zip(params, argv):
718
- try:
719
- p.reset(v) # eval v:str -> value
720
- except AttributeError:
721
- p.value = v
722
- except Exception as e: # failed to eval
723
- print(f"- Failed to reset {p!r}.", e)
724
-
725
- reset_params = set_params
726
-
733
+ for p, v in zip(params, argv):
734
+ try:
735
+ p.reset(v) # eval v:str -> value
736
+ except AttributeError:
737
+ p.value = v
738
+ except Exception as e:
739
+ print(f"- Failed to eval {v}:", e)
740
+
741
+ def reset_params(self, checked_only=False):
742
+ params = self.get_params(checked_only)
743
+ for p in params:
744
+ try:
745
+ p.reset()
746
+ except (AttributeError, TypeError):
747
+ ## TypeError might occur if p.reset(v) is called with
748
+ ## missing 1 required positional argument.
749
+ pass
750
+
727
751
  def copy_to_clipboard(self, checked_only=False):
728
752
  params = self.get_params(checked_only)
729
753
  text = '\t'.join(str(p) if isinstance(p, Param) else
730
754
  str(p.value) for p in params)
731
755
  Clipboard.write(text)
732
-
756
+
733
757
  def paste_from_clipboard(self, checked_only=False):
734
758
  text = Clipboard.read()
735
759
  if text:
@@ -745,11 +769,24 @@ class ControlPanel(CtrlInterface, KnobCtrlPanel):
745
769
 
746
770
 
747
771
  class Clipboard:
748
- """Clipboard interface of text and image
772
+ """Clipboard interface of text and image.
749
773
 
750
774
  This does not work unless wx.App instance exists.
751
775
  The clipboard data cannot be transferred unless wx.Frame exists.
752
776
  """
777
+ @contextmanager
778
+ @staticmethod
779
+ def istrstream():
780
+ with io.StringIO(Clipboard.read()) as f:
781
+ yield f
782
+
783
+ @contextmanager
784
+ @staticmethod
785
+ def ostrstream():
786
+ with io.StringIO() as f:
787
+ yield f
788
+ Clipboard.write(f.getvalue())
789
+
753
790
  @staticmethod
754
791
  def read(verbose=False):
755
792
  do = wx.TextDataObject()
@@ -762,7 +799,8 @@ class Clipboard:
762
799
  return text
763
800
  else:
764
801
  print("- Unable to open clipboard.")
765
-
802
+ return None
803
+
766
804
  @staticmethod
767
805
  def write(text, verbose=False):
768
806
  do = wx.TextDataObject(str(text))
@@ -774,7 +812,7 @@ class Clipboard:
774
812
  print(f"To clipboard:\n{text}")
775
813
  else:
776
814
  print("- Unable to open clipboard.")
777
-
815
+
778
816
  @staticmethod
779
817
  def imread(verbose=False):
780
818
  do = wx.BitmapDataObject()
@@ -784,30 +822,31 @@ class Clipboard:
784
822
  bmp = do.GetBitmap()
785
823
  else:
786
824
  print("- Unable to open clipboard.")
787
- return
825
+ return None
788
826
  try:
789
- ## Convert bmp --> buf
827
+ ## Convert bmp --> buf.
790
828
  img = bmp.ConvertToImage()
791
- buf = np.array(img.GetDataBuffer()) # do copy, don't ref
829
+ buf = np.array(img.GetDataBuffer()) # Do copy, don't ref.
792
830
  if verbose:
793
831
  print("From clipboard: {:.1f} Mb data read.".format(buf.nbytes/1e6))
794
832
  w, h = img.GetSize()
795
833
  return buf.reshape(h, w, 3)
796
834
  except Exception:
797
- print("- The contents of the clipboard are not images.")
798
-
835
+ print("- Contents of the clipboard are not images.")
836
+ return None
837
+
799
838
  @staticmethod
800
839
  def imwrite(buf, verbose=False):
801
840
  try:
802
- ## Convert buf --> bmp
841
+ ## Convert buf --> bmp.
803
842
  h, w = buf.shape[:2]
804
843
  if buf.ndim < 3:
805
- ## buf = np.array([buf] * 3).transpose((1,2,0)) # convert to gray bitmap
806
- buf = buf.repeat(3, axis=1) # convert to gray bitmap
844
+ # buf = np.array([buf] * 3).transpose((1,2,0)) # convert to gray bitmap
845
+ buf = buf.repeat(3, axis=1) # Convert to gray bitmap.
807
846
  img = wx.Image(w, h, buf.tobytes())
808
847
  bmp = img.ConvertToBitmap()
809
848
  except Exception:
810
- print("- The contents of the clipboard are not images.")
849
+ print("- Argument 'buf' is not a 2d array.")
811
850
  return
812
851
  do = wx.BitmapDataObject(bmp)
813
852
  if wx.TheClipboard.Open():
@@ -821,17 +860,17 @@ class Clipboard:
821
860
 
822
861
 
823
862
  ## --------------------------------
824
- ## Wx custom controls and bitmaps
863
+ ## Wx custom controls and bitmaps.
825
864
  ## --------------------------------
826
865
 
827
866
  class Icon(wx.Bitmap):
828
- """Returns an iconic bitmap with the specified size (w, h).
867
+ """Return an iconic bitmap with the specified size (w, h).
829
868
 
830
869
  The key is either Icon.provided_arts or Icon.custom_images key.
831
870
  If the key is empty it returns a transparent bitmap, otherwise NullBitmap.
832
871
 
833
872
  Note:
834
- A null (0-shaped) bitmap fails with AssertionError from 4.1.1
873
+ A null (0-shaped) bitmap fails with AssertionError from wx ver 4.1.1.
835
874
  """
836
875
  provided_arts = {
837
876
  'cut' : wx.ART_CUT,
@@ -855,8 +894,8 @@ class Icon(wx.Bitmap):
855
894
  '!!!' : wx.ART_ERROR,
856
895
  '+' : wx.ART_PLUS,
857
896
  '-' : wx.ART_MINUS,
858
- 'x' : wx.ART_DELETE,
859
- 't' : wx.ART_TICK_MARK,
897
+ # 'x' : wx.ART_DELETE,
898
+ # 't' : wx.ART_TICK_MARK,
860
899
  '~' : wx.ART_GO_HOME,
861
900
  'undo' : wx.ART_UNDO,
862
901
  'redo' : wx.ART_REDO,
@@ -868,10 +907,10 @@ class Icon(wx.Bitmap):
868
907
  '->|' : wx.ART_GOTO_LAST,
869
908
  }
870
909
  custom_images = {
871
- k:v for k, v in vars(images).items()
872
- if isinstance(v, wx.lib.embeddedimage.PyEmbeddedImage)
910
+ k: v for k, v in vars(images).items()
911
+ if isinstance(v, wx.lib.embeddedimage.PyEmbeddedImage)
873
912
  }
874
-
913
+
875
914
  def __init__(self, *args, **kwargs):
876
915
  try:
877
916
  bmp = Icon._getBitmap1(*args, **kwargs)
@@ -881,15 +920,25 @@ class Icon(wx.Bitmap):
881
920
 
882
921
  @staticmethod
883
922
  def _getBitmap1(key, size=None):
923
+ if not isinstance(size, (type(None), tuple, wx.Size)):
924
+ raise TypeError("invalid size type")
925
+
884
926
  if isinstance(key, wx.Bitmap):
885
927
  if size and key.Size != size:
886
928
  key = (key.ConvertToImage()
887
929
  .Scale(*size, wx.IMAGE_QUALITY_NEAREST)
888
930
  .ConvertToBitmap())
889
- return key #<wx.Bitmap>
890
- if not size:
931
+ return key #<wx.Bitmap>
932
+ if size is None:
891
933
  size = (16, 16)
892
934
  if key:
935
+ ## Returns a bitmap of provided artwork.
936
+ ## Note: The result could be a zero-shaped bitmap.
937
+ try:
938
+ if re.match("bullet(.*)", key):
939
+ return eval(f"Icon.{key}") # -> Icon.bullet(*v, **kw)
940
+ except TypeError:
941
+ pass
893
942
  try:
894
943
  art = Icon.custom_images.get(key)
895
944
  bmp = art.GetBitmap()
@@ -897,17 +946,16 @@ class Icon(wx.Bitmap):
897
946
  art = Icon.provided_arts.get(key)
898
947
  bmp = wx.ArtProvider.GetBitmap(art or key, wx.ART_OTHER, size)
899
948
  return bmp
900
-
901
- ## Note: null (0-shaped) bitmap fails with AssertionError from 4.1.1
902
- elif key == '':
949
+ if key == '':
950
+ ## Returns dummy-sized blank bitmap.
951
+ ## Note: A zero-shaped bitmap fails with AssertionError since wx ver 4.1.1.
903
952
  bmp = wx.Bitmap(size)
904
953
  with wx.MemoryDC(bmp) as dc:
905
954
  dc.SetBackground(wx.Brush('black'))
906
955
  dc.Clear()
907
- bmp.SetMaskColour('black') # return dummy-sized blank bitmap
956
+ bmp.SetMaskColour('black')
908
957
  return bmp
909
-
910
- return wx.NullBitmap # The standard wx controls accept this,
958
+ return wx.NullBitmap # The standard wx controls accept this.
911
959
 
912
960
  @staticmethod
913
961
  def _getBitmap2(back, fore, size=None, subsize=3/4):
@@ -917,17 +965,19 @@ class Icon(wx.Bitmap):
917
965
  subsize = wx.Size(size) * subsize
918
966
  back = Icon._getBitmap1(back, size)
919
967
  fore = Icon._getBitmap1(fore, subsize)
968
+ if back.Size == (0, 0) or fore.Size == (0, 0):
969
+ return back
920
970
  x = size[0] - subsize[0]
921
971
  y = size[1] - subsize[1]
922
972
  with wx.MemoryDC(back) as dc:
923
- ## dc = wx.GCDC(dc)
924
- ## dc.DrawBitmap(fore, x, y, useMask=True)
973
+ # dc = wx.GCDC(dc)
974
+ # dc.DrawBitmap(fore, x, y, useMask=True)
925
975
  gc = wx.GraphicsContext.Create(dc)
926
976
  gc.DrawBitmap(fore, x, y, *subsize)
927
977
  return back
928
978
 
929
979
  @staticmethod
930
- def bullet(colour, ec=None, size=None, radius=4):
980
+ def bullet(colour, radius=4, size=None, ec=None):
931
981
  if not size:
932
982
  size = (16, 16)
933
983
  bmp = wx.Bitmap(size)
@@ -942,7 +992,7 @@ class Icon(wx.Bitmap):
942
992
 
943
993
  @staticmethod
944
994
  def iconify(icon, w, h):
945
- ## if wx.VERSION >= (4,1,0):
995
+ ## if wx.VERSION >= (4,1,0): ...
946
996
  try:
947
997
  import wx.svg
948
998
  import requests
@@ -957,12 +1007,12 @@ class Icon(wx.Bitmap):
957
1007
 
958
1008
 
959
1009
  class ClassicButton(wx.Button):
960
- """Flat button
1010
+ """Classic button.
961
1011
 
962
1012
  Args:
963
- label : button label
964
- handler : event handler when the button is pressed
965
- icon : key:str or bitmap for button icon
1013
+ label: button label
1014
+ handler: event handler when the button is pressed
1015
+ icon: key:str or bitmap for button icon
966
1016
  **kwargs: keywords for wx.lib.platebtn.PlateButton
967
1017
  """
968
1018
  def __init__(self, parent, label='', handler=None, icon=None, **kwargs):
@@ -970,19 +1020,18 @@ class ClassicButton(wx.Button):
970
1020
 
971
1021
  if handler:
972
1022
  self.Bind(wx.EVT_BUTTON, _F(handler))
973
-
974
- self.SetToolTip(_Tip(handler.__doc__))
1023
+ self.SetToolTip(_Tip(handler.__doc__))
975
1024
  if icon:
976
1025
  self.SetBitmap(Icon(icon))
977
1026
 
978
1027
 
979
1028
  class Button(pb.PlateButton):
980
- """Flat button
1029
+ """Flat button.
981
1030
 
982
1031
  Args:
983
- label : button label
984
- handler : event handler when the button is pressed
985
- icon : key:str or bitmap for button icon
1032
+ label: button label
1033
+ handler: event handler when the button is pressed
1034
+ icon: key:str or bitmap for button icon
986
1035
  **kwargs: keywords for wx.lib.platebtn.PlateButton
987
1036
  """
988
1037
  def __init__(self, parent, label='', handler=None, icon=None, **kwargs):
@@ -991,13 +1040,13 @@ class Button(pb.PlateButton):
991
1040
 
992
1041
  if handler:
993
1042
  self.Bind(wx.EVT_BUTTON, _F(handler))
994
-
995
- self.SetToolTip(_Tip(handler.__doc__))
1043
+ self.SetToolTip(_Tip(handler.__doc__))
996
1044
  if icon:
997
1045
  self.SetBitmap(Icon(icon))
998
-
1046
+
999
1047
  def SetBitmap(self, bmp):
1000
1048
  """Set the bitmap displayed in the button.
1049
+
1001
1050
  (override) If it fails, it clears the bitmap.
1002
1051
  """
1003
1052
  try:
@@ -1008,12 +1057,12 @@ class Button(pb.PlateButton):
1008
1057
 
1009
1058
 
1010
1059
  class ToggleButton(wx.ToggleButton):
1011
- """Togglable button
1060
+ """Togglable button.
1012
1061
 
1013
1062
  Args:
1014
- label : button label
1015
- handler : event handler when the button is pressed
1016
- icon : key:str or bitmap for button icon
1063
+ label: button label
1064
+ handler: event handler when the button is pressed
1065
+ icon: key:str or bitmap for button icon
1017
1066
  **kwargs: keywords for wx.ToggleButton
1018
1067
 
1019
1068
  Note:
@@ -1024,8 +1073,7 @@ class ToggleButton(wx.ToggleButton):
1024
1073
 
1025
1074
  if handler:
1026
1075
  self.Bind(wx.EVT_TOGGLEBUTTON, _F(handler))
1027
-
1028
- self.SetToolTip(_Tip(handler.__doc__))
1076
+ self.SetToolTip(_Tip(handler.__doc__))
1029
1077
  if icon:
1030
1078
  try:
1031
1079
  self.SetBitmap(Icon(icon[0]))
@@ -1034,38 +1082,38 @@ class ToggleButton(wx.ToggleButton):
1034
1082
  self.SetBitmap(Icon(icon))
1035
1083
 
1036
1084
 
1037
- class TextCtrl(wx.Control):
1038
- """Text panel
1085
+ class TextBox(wx.Control):
1086
+ """Text control.
1039
1087
 
1040
1088
  Args:
1041
- label : button label
1042
- handler : event handler when text is entered
1043
- updater : event handler when the button is pressed
1044
- icon : key:str or bitmap for button icon
1089
+ label: button label
1090
+ handler: event handler when text is entered
1091
+ updater: event handler when the button is pressed
1092
+ icon: key:str or bitmap for button icon
1045
1093
  readonly: flag:bool (equiv. style=wx.TE_READONLY)
1046
1094
  **kwargs: keywords for wx.TextCtrl
1047
1095
  e.g., value:str
1048
1096
  """
1049
- Value = property(
1097
+ Value = property( # textctrl value:str
1050
1098
  lambda self: self._ctrl.GetValue(),
1051
- lambda self,v: self._ctrl.SetValue(v),
1052
- doc="textctrl value:str")
1053
-
1054
- value = Value #: internal use only
1055
-
1099
+ lambda self, v: self._ctrl.SetValue(v))
1100
+
1101
+ value = Value # internal use only
1102
+
1103
+ button = property(lambda self: self._btn)
1104
+ control = property(lambda self: self._ctrl)
1105
+
1056
1106
  def __init__(self, parent, label='', handler=None, updater=None,
1057
1107
  icon=None, readonly=False, size=(-1,-1), **kwargs):
1058
1108
  wx.Control.__init__(self, parent, size=size, style=wx.BORDER_NONE)
1059
1109
 
1060
1110
  kwargs['style'] = (kwargs.get('style', 0)
1061
- | wx.TE_PROCESS_ENTER
1062
- | (wx.TE_READONLY if readonly else 0))
1111
+ | wx.TE_PROCESS_ENTER
1112
+ | (wx.TE_READONLY if readonly else 0))
1063
1113
 
1064
1114
  self._ctrl = wx.TextCtrl(self, **kwargs)
1065
1115
  self._btn = Button(self, label, None, icon,
1066
1116
  size=(-1,-1) if label or icon else (0,0))
1067
- self._ctrl.SetToolTip(_Tip(handler.__doc__))
1068
- self._btn.SetToolTip(_Tip(updater.__doc__))
1069
1117
 
1070
1118
  self.SetSizer(
1071
1119
  pack(self, (
@@ -1076,10 +1124,14 @@ class TextCtrl(wx.Control):
1076
1124
  if handler:
1077
1125
  self._handler = _F(handler)
1078
1126
  self._ctrl.Bind(wx.EVT_TEXT_ENTER, lambda v: self._handler(self))
1127
+ self._ctrl.SetToolTip(_Tip(handler.__doc__))
1079
1128
  if updater:
1080
1129
  self._updater = _F(updater)
1081
1130
  self._btn.Bind(wx.EVT_BUTTON, lambda v: self._updater(self))
1082
-
1131
+ self._btn.SetToolTip(_Tip(updater.__doc__))
1132
+
1133
+ self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavKey)
1134
+
1083
1135
  def reset(self, v):
1084
1136
  try:
1085
1137
  self.Value = v
@@ -1087,15 +1139,21 @@ class TextCtrl(wx.Control):
1087
1139
  except AttributeError:
1088
1140
  pass
1089
1141
 
1142
+ def OnNavKey(self, evt):
1143
+ if evt.EventObject is self._ctrl:
1144
+ self.Navigate(evt.Direction)
1145
+ elif self.IsShown():
1146
+ self._ctrl.SetFocus()
1147
+
1090
1148
 
1091
1149
  class Choice(wx.Control):
1092
- """Editable Choice (ComboBox) panel
1150
+ """Editable Choice (ComboBox) control.
1093
1151
 
1094
1152
  Args:
1095
- label : button label
1096
- handler : event handler when text is entered or item is selected
1097
- updater : event handler when the button is pressed
1098
- icon : key:str or bitmap for button icon
1153
+ label: button label
1154
+ handler: event handler when text is entered or item is selected
1155
+ updater: event handler when the button is pressed
1156
+ icon: key:str or bitmap for button icon
1099
1157
  readonly: flag:bool (equiv. style=wx.CB_READONLY)
1100
1158
  **kwargs: keywords for wx.ComboBox
1101
1159
  e.g., choices:list
@@ -1104,36 +1162,34 @@ class Choice(wx.Control):
1104
1162
  If the input item is not found in the choices,
1105
1163
  it will be added to the list (unless readonly)
1106
1164
  """
1107
- Value = property(
1165
+ Value = property( # combobox value:str
1108
1166
  lambda self: self._ctrl.GetValue(),
1109
- lambda self,v: self._ctrl.SetValue(v),
1110
- doc="combobox value:str")
1111
-
1112
- value = Value #: internal use only
1113
-
1114
- Selection = property(
1167
+ lambda self, v: self._ctrl.SetValue(v))
1168
+
1169
+ value = Value # internal use only
1170
+
1171
+ Selection = property( # combobox selection:int or NOT_FOUND(-1)
1115
1172
  lambda self: self._ctrl.GetSelection(),
1116
- lambda self,v: self._ctrl.SetSelection(v), # int or NOT_FOUND(-1)
1117
- doc="combobox selection:int")
1118
-
1119
- Items = property(
1173
+ lambda self, v: self._ctrl.SetSelection(v))
1174
+
1175
+ Items = property( # combobox items:list
1120
1176
  lambda self: self._ctrl.GetItems(),
1121
- lambda self,v: self._ctrl.SetItems(v),
1122
- doc="combobox items:list")
1123
-
1177
+ lambda self, v: self._ctrl.SetItems(v))
1178
+
1179
+ button = property(lambda self: self._btn)
1180
+ control = property(lambda self: self._ctrl)
1181
+
1124
1182
  def __init__(self, parent, label='', handler=None, updater=None,
1125
1183
  icon=None, readonly=False, size=(-1,-1), **kwargs):
1126
1184
  wx.Control.__init__(self, parent, size=size, style=wx.BORDER_NONE)
1127
1185
 
1128
1186
  kwargs['style'] = (kwargs.get('style', 0)
1129
- | wx.TE_PROCESS_ENTER
1130
- | (wx.CB_READONLY if readonly else 0))
1187
+ | wx.TE_PROCESS_ENTER
1188
+ | (wx.CB_READONLY if readonly else 0))
1131
1189
 
1132
1190
  self._ctrl = wx.ComboBox(self, **kwargs)
1133
1191
  self._btn = Button(self, label, None, icon,
1134
1192
  size=(-1,-1) if label or icon else (0,0))
1135
- self._ctrl.SetToolTip(_Tip(handler.__doc__))
1136
- self._btn.SetToolTip(_Tip(updater.__doc__))
1137
1193
 
1138
1194
  self.SetSizer(
1139
1195
  pack(self, (
@@ -1145,18 +1201,22 @@ class Choice(wx.Control):
1145
1201
  self._handler = _F(handler)
1146
1202
  self._ctrl.Bind(wx.EVT_TEXT_ENTER, lambda v: self._handler(self))
1147
1203
  self._ctrl.Bind(wx.EVT_COMBOBOX, lambda v: self._handler(self))
1204
+ self._ctrl.SetToolTip(_Tip(handler.__doc__))
1148
1205
  self._ctrl.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
1149
1206
  if updater:
1150
1207
  self._updater = _F(updater)
1151
1208
  self._btn.Bind(wx.EVT_BUTTON, lambda v: self._updater(self))
1152
-
1209
+ self._btn.SetToolTip(_Tip(updater.__doc__))
1210
+
1211
+ self.Bind(wx.EVT_NAVIGATION_KEY, self.OnNavKey)
1212
+
1153
1213
  def reset(self, v):
1154
1214
  try:
1155
1215
  self.Value = v
1156
1216
  self._handler(self)
1157
1217
  except AttributeError:
1158
1218
  pass
1159
-
1219
+
1160
1220
  def OnTextEnter(self, evt):
1161
1221
  s = evt.String.strip()
1162
1222
  if not s:
@@ -1166,29 +1226,35 @@ class Choice(wx.Control):
1166
1226
  self._ctrl.SetStringSelection(s)
1167
1227
  evt.Skip()
1168
1228
 
1229
+ def OnNavKey(self, evt):
1230
+ if evt.EventObject is self._ctrl:
1231
+ self.Navigate(evt.Direction)
1232
+ elif self.IsShown():
1233
+ self._ctrl.SetFocus()
1234
+
1169
1235
 
1170
1236
  class Indicator(wx.Control):
1171
- """Traffic light indicator
1237
+ """Traffic light indicator.
1172
1238
 
1173
1239
  Args:
1174
- colors : list of colors (default is tricolour) cf. wx.ColourDatabase
1175
- value : initial value
1240
+ colors: list of colors (default is tricolour) cf. wx.ColourDatabase
1241
+ value: initial value
1176
1242
  **kwargs: keywords for wx.Control
1177
1243
  """
1178
1244
  @property
1179
1245
  def Value(self):
1180
1246
  return self.__value
1181
-
1247
+
1182
1248
  @Value.setter
1183
1249
  def Value(self, v):
1184
1250
  self.__value = int(v)
1185
1251
  self.Refresh()
1186
-
1252
+
1187
1253
  def redesign(self, **kwargs):
1188
1254
  """Update multiple design properties at once.
1189
1255
 
1190
1256
  This method is useful for changing colors, spacing, radius, etc.
1191
- The best size will be automatically invalidated and re-calculated.
1257
+ The best size will be automatically invalidated and recalculated.
1192
1258
 
1193
1259
  Args:
1194
1260
  **kwargs: class attributes, e.g. colors, spacing, radius.
@@ -1198,14 +1264,14 @@ class Indicator(wx.Control):
1198
1264
  """
1199
1265
  self.__dict__.update(kwargs)
1200
1266
  self.InvalidateBestSize()
1201
-
1202
- colors = ('green', 'yellow', 'red') # default tricolor style
1267
+
1268
+ colors = ('green', 'yellow', 'red') # default tricolor style
1203
1269
  backgroundColour = 'dark gray'
1204
1270
  foregroundColour = 'light gray'
1205
1271
  spacing = 7
1206
1272
  radius = 4
1207
1273
  glow = 0
1208
-
1274
+
1209
1275
  def __init__(self, parent, colors=None, value=0,
1210
1276
  style=wx.BORDER_NONE, **kwargs):
1211
1277
  wx.Control.__init__(self, parent, style=style, **kwargs)
@@ -1219,19 +1285,19 @@ class Indicator(wx.Control):
1219
1285
  self.InvalidateBestSize()
1220
1286
  self.Fit()
1221
1287
 
1222
- self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # to avoid flickering
1288
+ self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # to avoid flickering
1223
1289
 
1224
1290
  self.Bind(wx.EVT_SIZE, self.OnSize)
1225
1291
  self.Bind(wx.EVT_PAINT, self.OnPaint)
1226
-
1292
+
1227
1293
  def DoGetBestSize(self):
1228
1294
  N = len(self.colors)
1229
1295
  s = self.spacing
1230
1296
  return wx.Size((2*s-1)*N+3, 2*s+2)
1231
-
1297
+
1232
1298
  def OnSize(self, evt):
1233
1299
  self.Refresh()
1234
-
1300
+
1235
1301
  def OnPaint(self, evt):
1236
1302
  dc = wx.BufferedPaintDC(self)
1237
1303
  dc.Clear()
@@ -1267,7 +1333,7 @@ class Indicator(wx.Control):
1267
1333
  gc.DrawPath(path)
1268
1334
  dc.SetBrush(wx.Brush(name if b else self.foregroundColour))
1269
1335
  dc.DrawCircle(x, y, r)
1270
-
1336
+
1271
1337
  def blink(self, msec, mask=0):
1272
1338
  """Blinks once for given milliseconds.
1273
1339
 
@@ -1286,31 +1352,31 @@ class Indicator(wx.Control):
1286
1352
 
1287
1353
 
1288
1354
  class Gauge(wx.Control):
1289
- """Rainbow gauge
1355
+ """Rainbow gauge.
1290
1356
 
1291
1357
  Args:
1292
- range : maximum value
1293
- value : initial value
1358
+ range: maximum value
1359
+ value: initial value
1294
1360
  **kwargs: keywords for wx.Control
1295
1361
  """
1296
1362
  @property
1297
1363
  def Value(self):
1298
1364
  return self.__value
1299
-
1365
+
1300
1366
  @Value.setter
1301
1367
  def Value(self, v):
1302
1368
  self.__value = int(v)
1303
1369
  self.Refresh()
1304
-
1370
+
1305
1371
  @property
1306
1372
  def Range(self):
1307
1373
  return self.__range
1308
-
1374
+
1309
1375
  @Range.setter
1310
1376
  def Range(self, v):
1311
1377
  self.__range = int(v)
1312
1378
  self.Refresh()
1313
-
1379
+
1314
1380
  def __init__(self, parent, range=24, value=0,
1315
1381
  style=wx.BORDER_NONE, **kwargs):
1316
1382
  wx.Control.__init__(self, parent, style=style, **kwargs)
@@ -1318,14 +1384,14 @@ class Gauge(wx.Control):
1318
1384
  self.__range = range
1319
1385
  self.__value = value
1320
1386
 
1321
- self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # to avoid flickering
1387
+ self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # to avoid flickering
1322
1388
 
1323
1389
  self.Bind(wx.EVT_SIZE, self.OnSize)
1324
1390
  self.Bind(wx.EVT_PAINT, self.OnPaint)
1325
-
1391
+
1326
1392
  def OnSize(self, evt):
1327
1393
  self.Refresh()
1328
-
1394
+
1329
1395
  def OnPaint(self, evt):
1330
1396
  dc = wx.BufferedPaintDC(self)
1331
1397
  dc.Clear()