combobox.py
33.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
#----------------------------------------------------------------------------
# Name: masked.combobox.py
# Authors: Will Sadkin
# Email: wsadkin@nameconnector.com
# Created: 02/11/2003
# Copyright: (c) 2003 by Will Sadkin, 2003
# RCS-ID: $Id$
# License: wxWidgets license
#----------------------------------------------------------------------------
#
# This masked edit class allows for the semantics of masked controls
# to be applied to combo boxes.
#
#----------------------------------------------------------------------------
"""
Provides masked edit capabilities within a ComboBox format, as well as
a base class from which you can derive masked comboboxes tailored to a specific
function. See maskededit module overview for how to configure the control.
"""
import wx, types, string
from wx.lib.masked import *
# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
# be a good place to implement the 2.3 logger class
from wx.tools.dbg import Logger
##dbg = Logger()
##dbg(enable=1)
## ---------- ---------- ---------- ---------- ---------- ---------- ----------
## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
## events, we have to do it ourselves when we auto-complete.
class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
"""
Because calling SetSelection programmatically does not fire EVT_COMBOBOX
events, the derived control has to do it itself when it auto-completes.
"""
def __init__(self, id, selection = 0, object=None):
wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id)
self.__selection = selection
self.SetEventObject(object)
def GetSelection(self):
"""Retrieve the value of the control at the time
this event was generated."""
return self.__selection
class MaskedComboBoxEventHandler(wx.EvtHandler):
"""
This handler ensures that the derived control can react to events
from the base control before any external handlers run, to ensure
proper behavior.
"""
def __init__(self, combobox):
wx.EvtHandler.__init__(self)
self.combobox = combobox
combobox.PushEventHandler(self)
self.Bind(wx.EVT_SET_FOCUS, self.combobox._OnFocus ) ## defeat automatic full selection
self.Bind(wx.EVT_KILL_FOCUS, self.combobox._OnKillFocus ) ## run internal validator
self.Bind(wx.EVT_LEFT_DCLICK, self.combobox._OnDoubleClick) ## select field under cursor on dclick
self.Bind(wx.EVT_RIGHT_UP, self.combobox._OnContextMenu ) ## bring up an appropriate context menu
self.Bind(wx.EVT_CHAR, self.combobox._OnChar ) ## handle each keypress
self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDownInComboBox ) ## for special processing of up/down keys
self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDown ) ## for processing the rest of the control keys
## (next in evt chain)
self.Bind(wx.EVT_COMBOBOX, self.combobox._OnDropdownSelect ) ## to bring otherwise completely independent base
## ctrl selection into maskededit framework
self.Bind(wx.EVT_TEXT, self.combobox._OnTextChange ) ## color control appropriately & keep
## track of previous value for undo
class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
"""
Base class for generic masked edit comboboxes; allows auto-complete of values.
It is not meant to be instantiated directly, but rather serves as a base class
for any subsequent refinements.
"""
def __init__( self, parent, id=-1, value = '',
pos = wx.DefaultPosition,
size = wx.DefaultSize,
choices = [],
style = wx.CB_DROPDOWN,
validator = wx.DefaultValidator,
name = "maskedComboBox",
setupEventHandling = True, ## setup event handling by default):
**kwargs):
kwargs['choices'] = choices ## set up maskededit to work with choice list too
self._prevSelection = (-1, -1)
## Since combobox completion is case-insensitive, always validate same way
if not kwargs.has_key('compareNoCase'):
kwargs['compareNoCase'] = True
MaskedEditMixin.__init__( self, name, **kwargs )
self._choices = self._ctrl_constraints._choices
## dbg('self._choices:', self._choices)
if self._ctrl_constraints._alignRight:
choices = [choice.rjust(self._masklength) for choice in choices]
else:
choices = [choice.ljust(self._masklength) for choice in choices]
wx.ComboBox.__init__(self, parent, id, value='',
pos=pos, size = size,
choices=choices, style=style|wx.WANTS_CHARS,
validator=validator,
name=name)
self.controlInitialized = True
self._PostInit(style=style, setupEventHandling=setupEventHandling,
name=name, value=value, **kwargs)
def _PostInit(self, style=wx.CB_DROPDOWN,
setupEventHandling = True, ## setup event handling by default):
name = "maskedComboBox", value='', **kwargs):
# This is necessary, because wxComboBox currently provides no
# method for determining later if this was specified in the
# constructor for the control...
self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY
if not hasattr(self, 'controlInitialized'):
self.controlInitialized = True ## must have been called via XRC, therefore base class is constructed
if not kwargs.has_key('choices'):
choices=[]
kwargs['choices'] = choices ## set up maskededit to work with choice list too
self._choices = []
## Since combobox completion is case-insensitive, always validate same way
if not kwargs.has_key('compareNoCase'):
kwargs['compareNoCase'] = True
MaskedEditMixin.__init__( self, name, **kwargs )
self._choices = self._ctrl_constraints._choices
## dbg('self._choices:', self._choices)
if self._ctrl_constraints._alignRight:
choices = [choice.rjust(self._masklength) for choice in choices]
else:
choices = [choice.ljust(self._masklength) for choice in choices]
wx.ComboBox.Clear(self)
wx.ComboBox.AppendItems(self, choices)
# Set control font - fixed width by default
self._setFont()
if self._autofit:
self.SetClientSize(self._CalcSize())
width = self.GetSize().width
height = self.GetBestSize().height
self.SetInitialSize((width, height))
if value:
# ensure value is width of the mask of the control:
if self._ctrl_constraints._alignRight:
value = value.rjust(self._masklength)
else:
value = value.ljust(self._masklength)
if self.__readonly:
self.SetStringSelection(value)
else:
self._SetInitialValue(value)
self._SetKeycodeHandler(wx.WXK_UP, self._OnSelectChoice)
self._SetKeycodeHandler(wx.WXK_DOWN, self._OnSelectChoice)
self.replace_next_combobox_event = False
self.correct_selection = -1
if setupEventHandling:
## Setup event handling functions through event handler object,
## to guarantee processing prior to giving event callbacks from
## outside the class:
self.evt_handler = MaskedComboBoxEventHandler(self)
self.Bind(wx.EVT_WINDOW_DESTROY, self.OnWindowDestroy )
def __repr__(self):
return "<MaskedComboBox: %s>" % self.GetValue()
def OnWindowDestroy(self, event):
# clean up associated event handler object:
if self.RemoveEventHandler(self.evt_handler):
wx.CallAfter(self.evt_handler.Destroy)
event.Skip()
def _CalcSize(self, size=None):
"""
Calculate automatic size if allowed; augment base mixin function
to account for the selector button.
"""
size = self._calcSize(size)
return (size[0]+20, size[1])
def SetFont(self, *args, **kwargs):
""" Set the font, then recalculate control size, if appropriate. """
wx.ComboBox.SetFont(self, *args, **kwargs)
if self._autofit:
## dbg('calculated size:', self._CalcSize())
self.SetClientSize(self._CalcSize())
width = self.GetSize().width
height = self.GetBestSize().height
## dbg('setting client size to:', (width, height))
self.SetInitialSize((width, height))
def _GetSelection(self):
"""
Allow mixin to get the text selection of this control.
REQUIRED by any class derived from MaskedEditMixin.
"""
## dbg('MaskedComboBox::_GetSelection()')
return self.GetMark()
def _SetSelection(self, sel_start, sel_to):
"""
Allow mixin to set the text selection of this control.
REQUIRED by any class derived from MaskedEditMixin.
"""
## dbg('MaskedComboBox::_SetSelection: setting mark to (%d, %d)' % (sel_start, sel_to))
if not self.__readonly:
return self.SetMark( sel_start, sel_to )
def _GetInsertionPoint(self):
## dbg('MaskedComboBox::_GetInsertionPoint()', indent=1)
## ret = self.GetInsertionPoint()
# work around new bug in 2.5, in which the insertion point
# returned is always at the right side of the selection,
# rather than the start, as is the case with TextCtrl.
ret = self.GetMark()[0]
## dbg('returned', ret, indent=0)
return ret
def _SetInsertionPoint(self, pos):
## dbg('MaskedComboBox::_SetInsertionPoint(%d)' % pos)
if not self.__readonly:
self.SetInsertionPoint(pos)
def IsEmpty(*args, **kw):
return MaskedEditMixin.IsEmpty(*args, **kw)
def _GetValue(self):
"""
Allow mixin to get the raw value of the control with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
return self.GetValue()
def _SetValue(self, value):
"""
Allow mixin to set the raw value of the control with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
# For wxComboBox, ensure that values are properly padded so that
# if varying length choices are supplied, they always show up
# in the window properly, and will be the appropriate length
# to match the mask:
if self._ctrl_constraints._alignRight:
value = value.rjust(self._masklength)
else:
value = value.ljust(self._masklength)
# Record current selection and insertion point, for undo
self._prevSelection = self._GetSelection()
self._prevInsertionPoint = self._GetInsertionPoint()
## dbg('MaskedComboBox::_SetValue(%s), selection beforehand: %d' % (value, self.GetSelection()))
wx.ComboBox.SetValue(self, value)
## dbg('MaskedComboBox::_SetValue(%s), selection now: %d' % (value, self.GetSelection()))
# text change events don't always fire, so we check validity here
# to make certain formatting is applied:
self._CheckValid()
def SetValue(self, value):
"""
This function redefines the externally accessible .SetValue to be
a smart "paste" of the text in question, so as not to corrupt the
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
## dbg('MaskedComboBox::SetValue(%s)' % value, indent=1)
if not self._mask:
wx.ComboBox.SetValue(value) # revert to base control behavior
## dbg('no mask; deferring to base class', indent=0)
return
# else...
# empty previous contents, replacing entire value:
## dbg('MaskedComboBox::SetValue: selection beforehand: %d' % (self.GetSelection()))
self._SetInsertionPoint(0)
self._SetSelection(0, self._masklength)
if( len(value) < self._masklength # value shorter than control
and (self._isFloat or self._isInt) # and it's a numeric control
and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
# try to intelligently "pad out" the value to the right size:
value = self._template[0:self._masklength - len(value)] + value
## dbg('padded value = "%s"' % value)
# For wxComboBox, ensure that values are properly padded so that
# if varying length choices are supplied, they always show up
# in the window properly, and will be the appropriate length
# to match the mask:
elif self._ctrl_constraints._alignRight:
value = value.rjust(self._masklength)
else:
value = value.ljust(self._masklength)
# make SetValue behave the same as if you had typed the value in:
try:
value, replace_to = self._Paste(value, raise_on_invalid=True, just_return_value=True)
if self._isFloat:
self._isNeg = False # (clear current assumptions)
value = self._adjustFloat(value)
elif self._isInt:
self._isNeg = False # (clear current assumptions)
value = self._adjustInt(value)
elif self._isDate and not self.IsValid(value) and self._4digityear:
value = self._adjustDate(value, fixcentury=True)
except ValueError:
# If date, year might be 2 digits vs. 4; try adjusting it:
if self._isDate and self._4digityear:
dateparts = value.split(' ')
dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
value = string.join(dateparts, ' ')
value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
else:
raise
## dbg('adjusted value: "%s"' % value)
# Attempt to compensate for fact that calling .SetInsertionPoint() makes the
# selection index -1, even if the resulting set value is in the list.
# So, if we are setting a value that's in the list, use index selection instead.
if value in self._choices:
index = self._choices.index(value)
self._prevValue = self._curValue
self._curValue = self._choices[index]
self._ctrl_constraints._autoCompleteIndex = index
self.SetSelection(index)
else:
self._SetValue(value)
#### dbg('queuing insertion after .SetValue', replace_to)
wx.CallAfter(self._SetInsertionPoint, replace_to)
wx.CallAfter(self._SetSelection, replace_to, replace_to)
## dbg(indent=0)
def _Refresh(self):
"""
Allow mixin to refresh the base control with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
wx.ComboBox.Refresh(self)
def Refresh(self):
"""
This function redefines the externally accessible .Refresh() to
validate the contents of the masked control as it refreshes.
NOTE: this must be done in the class derived from the base wx control.
"""
self._CheckValid()
self._Refresh()
def _IsEditable(self):
"""
Allow mixin to determine if the base control is editable with this function.
REQUIRED by any class derived from MaskedEditMixin.
"""
return not self.__readonly
def Cut(self):
"""
This function redefines the externally accessible .Cut to be
a smart "erase" of the text in question, so as not to corrupt the
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
if self._mask:
self._Cut() # call the mixin's Cut method
else:
wx.ComboBox.Cut(self) # else revert to base control behavior
def Paste(self):
"""
This function redefines the externally accessible .Paste to be
a smart "paste" of the text in question, so as not to corrupt the
masked control. NOTE: this must be done in the class derived
from the base wx control.
"""
if self._mask:
self._Paste() # call the mixin's Paste method
else:
wx.ComboBox.Paste(self) # else revert to base control behavior
def Undo(self):
"""
This function defines the undo operation for the control. (The default
undo is 1-deep.)
"""
if not self.__readonly:
if self._mask:
self._Undo()
else:
wx.ComboBox.Undo(self) # else revert to base control behavior
def Append( self, choice, clientData=None ):
"""
This base control function override is necessary so the control can keep track
of any additions to the list of choices, because wx.ComboBox doesn't have an
accessor for the choice list. The code here is the same as in the
SetParameters() mixin function, but is done for the individual value
as appended, so the list can be built incrementally without speed penalty.
"""
if self._mask:
if type(choice) not in (types.StringType, types.UnicodeType):
raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
elif not self.IsValid(choice):
raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
if not self._ctrl_constraints._choices:
self._ctrl_constraints._compareChoices = []
self._ctrl_constraints._choices = []
self._hasList = True
compareChoice = choice.strip()
if self._ctrl_constraints._compareNoCase:
compareChoice = compareChoice.lower()
if self._ctrl_constraints._alignRight:
choice = choice.rjust(self._masklength)
else:
choice = choice.ljust(self._masklength)
if self._ctrl_constraints._fillChar != ' ':
choice = choice.replace(' ', self._fillChar)
## dbg('updated choice:', choice)
self._ctrl_constraints._compareChoices.append(compareChoice)
self._ctrl_constraints._choices.append(choice)
self._choices = self._ctrl_constraints._choices # (for shorthand)
if( not self.IsValid(choice) and
(not self._ctrl_constraints.IsEmpty(choice) or
(self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ):
raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name))
wx.ComboBox.Append(self, choice, clientData)
def AppendItems( self, choices ):
"""
AppendItems() is handled in terms of Append, to avoid code replication.
"""
for choice in choices:
self.Append(choice)
def Clear( self ):
"""
This base control function override is necessary so the derived control can
keep track of any additions to the list of choices, because wx.ComboBox
doesn't have an accessor for the choice list.
"""
if self._mask:
self._choices = []
self._ctrl_constraints._autoCompleteIndex = -1
if self._ctrl_constraints._choices:
self.SetCtrlParameters(choices=[])
wx.ComboBox.Clear(self)
def _OnCtrlParametersChanged(self):
"""
This overrides the mixin's default OnCtrlParametersChanged to detect
changes in choice list, so masked.Combobox can update the base control:
"""
if self.controlInitialized and self._choices != self._ctrl_constraints._choices:
wx.ComboBox.Clear(self)
self._choices = self._ctrl_constraints._choices
for choice in self._choices:
wx.ComboBox.Append( self, choice )
# Not all wx platform implementations have .GetMark, so we make the following test,
# and fall back to our old hack if they don't...
#
if not hasattr(wx.ComboBox, 'GetMark'):
def GetMark(self):
"""
This function is a hack to make up for the fact that wx.ComboBox has no
method for returning the selected portion of its edit control. It
works, but has the nasty side effect of generating lots of intermediate
events.
"""
## dbg(suspend=1) # turn off debugging around this function
## dbg('MaskedComboBox::GetMark', indent=1)
if self.__readonly:
## dbg(indent=0)
return 0, 0 # no selection possible for editing
## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
sel_start = sel_to = self.GetInsertionPoint()
## dbg("current sel_start:", sel_start)
value = self.GetValue()
## dbg('value: "%s"' % value)
self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
wx.ComboBox.Cut(self)
newvalue = self.GetValue()
## dbg("value after Cut operation:", newvalue)
if newvalue != value: # something was selected; calculate extent
## dbg("something selected")
sel_to = sel_start + len(value) - len(newvalue)
wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
wx.ComboBox.SetInsertionPoint(self, sel_start)
wx.ComboBox.SetMark(self, sel_start, sel_to)
self._ignoreChange = False # tell _OnTextChange() to pay attn again
## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
return sel_start, sel_to
else:
def GetMark(self):
## dbg('MaskedComboBox::GetMark()', indent = 1)
ret = wx.ComboBox.GetMark(self)
## dbg('returned', ret, indent=0)
return ret
def SetSelection(self, index):
"""
Necessary override for bookkeeping on choice selection, to keep current value
current.
"""
## dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1)
if self._mask:
self._prevValue = self._curValue
self._ctrl_constraints._autoCompleteIndex = index
if index != -1:
self._curValue = self._choices[index]
else:
self._curValue = None
wx.ComboBox.SetSelection(self, index)
## dbg('selection now: %d' % self.GetCurrentSelection(), indent=0)
def _OnKeyDownInComboBox(self, event):
"""
This function is necessary because navigation and control key events
do not seem to normally be seen by the wxComboBox's EVT_CHAR routine.
(Tabs don't seem to be visible no matter what, except for CB_READONLY
controls, for some bizarre reason... {:-( )
"""
key = event.GetKeyCode()
## dbg('MaskedComboBox::OnKeyDownInComboBox(%d)' % key)
if event.GetKeyCode() in self._nav + self._control:
if not self._IsEditable():
# WANTS_CHARS with CB_READONLY apparently prevents navigation on WXK_TAB;
# ensure we can still navigate properly, as maskededit mixin::OnChar assumes
# that event.Skip() will just work, but it doesn't:
if self._keyhandlers.has_key(key):
self._keyhandlers[key](event)
# else pass
else:
## dbg('calling OnChar()')
self._OnChar(event)
else:
event.Skip() # let mixin default KeyDown behavior occur
## dbg(indent=0)
def _OnDropdownSelect(self, event):
"""
This function appears to be necessary because dropdown selection seems to
manipulate the contents of the control in an inconsistent way, properly
changing the selection index, but *not* the value. (!) Calling SetSelection()
on a selection event for the same selection would seem like a nop, but it seems to
fix the problem.
"""
## dbg('MaskedComboBox::OnDropdownSelect(%d)' % event.GetSelection(), indent=1)
if self.replace_next_combobox_event:
## dbg('replacing EVT_COMBOBOX')
self.replace_next_combobox_event = False
self._OnAutoSelect(self._ctrl_constraints, self.correct_selection)
else:
## dbg('skipping EVT_COMBOBOX')
event.Skip()
## dbg(indent=0)
def _OnSelectChoice(self, event):
"""
This function appears to be necessary, because the processing done
on the text of the control somehow interferes with the combobox's
selection mechanism for the arrow keys.
"""
## dbg('MaskedComboBox::OnSelectChoice', indent=1)
if not self._mask:
event.Skip()
return
value = self.GetValue().strip()
if self._ctrl_constraints._compareNoCase:
value = value.lower()
if event.GetKeyCode() == wx.WXK_UP:
direction = -1
else:
direction = 1
match_index, partial_match = self._autoComplete(
direction,
self._ctrl_constraints._compareChoices,
value,
self._ctrl_constraints._compareNoCase,
current_index = self._ctrl_constraints._autoCompleteIndex)
if match_index is not None:
## dbg('setting selection to', match_index)
# issue appropriate event to outside:
self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
self._CheckValid()
keep_processing = False
else:
pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
field = self._FindField(pos)
if self.IsEmpty() or not field._hasList:
## dbg('selecting 1st value in list')
self._OnAutoSelect(self._ctrl_constraints, match_index=0)
self._CheckValid()
keep_processing = False
else:
# attempt field-level auto-complete
## dbg(indent=0)
keep_processing = self._OnAutoCompleteField(event)
## dbg('keep processing?', keep_processing, indent=0)
return keep_processing
def _OnAutoSelect(self, field, match_index=None):
"""
Override mixin (empty) autocomplete handler, so that autocompletion causes
combobox to update appropriately.
Additionally allow items that aren't in the drop down.
"""
## dbg('MaskedComboBox::OnAutoSelect(%d, %s)' % (field._index, repr(match_index)), indent=1)
## field._autoCompleteIndex = match
if isinstance(match_index, int):
if field == self._ctrl_constraints:
self.SetSelection(match_index)
## dbg('issuing combo selection event')
self.GetEventHandler().ProcessEvent(
MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
self._CheckValid()
## dbg('field._autoCompleteIndex:', match)
## dbg('self.GetCurrentSelection():', self.GetCurrentSelection())
end = self._goEnd(getPosOnly=True)
## dbg('scheduling set of end position to:', end)
# work around bug in wx 2.5
wx.CallAfter(self.SetInsertionPoint, 0)
wx.CallAfter(self.SetInsertionPoint, end)
elif isinstance(match_index, str) or isinstance(match_index, unicode):
## dbg('CallAfter SetValue')
# Preserve the textbox contents
# See commentary in _OnReturn docstring.
wx.CallAfter(self.SetValue, match_index)
#### dbg('queuing insertion after .SetValue', replace_to)
## dbg(indent=0)
def _OnReturn(self, event):
"""
For wx.ComboBox, it seems that if you hit return when the dropdown is
dropped, the event that dismisses the dropdown will also blank the
control, because of the implementation of wxComboBox. So this function
examines the selection and if it is -1, and the value according to
(the base control!) is a value in the list, then it schedules a
programmatic wxComboBox.SetSelection() call to pick the appropriate
item in the list. (and then does the usual OnReturn bit.)
If the value isn't a value in the list then allow the current textbox contents to stay.
"""
## dbg('MaskedComboBox::OnReturn', indent=1)
## dbg('current value: "%s"' % self.GetValue(), 'current selection:', self.GetCurrentSelection())
if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
## wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
self.replace_next_combobox_event = True
self.correct_selection = self._ctrl_constraints._autoCompleteIndex
else:
# Not doing this causes the item to be empty after hitting return on a non-selection while the drop
# down is showing. Not all masked comboboxes require choices from an autocomplete list.
self.replace_next_combobox_event = True
self.correct_selection = self._GetValue()
event.m_keyCode = wx.WXK_TAB
event.Skip()
## dbg(indent=0)
def _LostFocus(self):
## dbg('MaskedComboBox::LostFocus; Selection=%d, value="%s"' % (self.GetSelection(), self.GetValue()))
if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
## dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
"""
The "user-visible" masked combobox control, this class is
identical to the BaseMaskedComboBox class it's derived from.
(This extra level of inheritance allows us to add the generic
set of masked edit parameters only to this class while allowing
other classes to derive from the "base" masked combobox control,
and provide a smaller set of valid accessor functions.)
See BaseMaskedComboBox for available methods.
"""
pass
class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
"""
This class exists to support the use of XRC subclassing.
"""
# This should really be wx.EVT_WINDOW_CREATE but it is not
# currently delivered for native controls on all platforms, so
# we'll use EVT_SIZE instead. It should happen shortly after the
# control is created as the control is set to its "best" size.
_firstEventType = wx.EVT_SIZE
def __init__(self):
pre = wx.PreComboBox()
self.PostCreate(pre)
self.Bind(self._firstEventType, self.OnCreate)
def OnCreate(self, evt):
self.Unbind(self._firstEventType)
self._PostInit()
__i = 0
## CHANGELOG:
## ====================
## Version 1.4
## 1. Added handler for EVT_COMBOBOX to address apparently inconsistent behavior
## of control when the dropdown control is used to do a selection.
## NOTE: due to misbehavior of wx.ComboBox re: losing all concept of the
## current selection index if SetInsertionPoint() is called, which is required
## to support masked .SetValue(), this control is flaky about retaining selection
## information. I can't truly fix this without major changes to the base control,
## but I've tried to compensate as best I can.
## TODO: investigate replacing base control with ComboCtrl instead...
## 2. Fixed navigation in readonly masked combobox, which was not working because
## the base control doesn't do navigation if style=CB_READONLY|WANTS_CHARS.
##
##
## Version 1.3
## 1. Made definition of "hack" GetMark conditional on base class not
## implementing it properly, to allow for migration in wx code base
## while taking advantage of improvements therein for some platforms.
##
## Version 1.2
## 1. Converted docstrings to reST format, added doc for ePyDoc.
## 2. Renamed helper functions, vars etc. not intended to be visible in public
## interface to code.
##
## Version 1.1
## 1. Added .SetFont() method that properly resizes control
## 2. Modified control to support construction via XRC mechanism.
## 3. Added AppendItems() to conform with latest combobox.