mwxlib 0.99.0__py3-none-any.whl → 1.7.13__py3-none-any.whl

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