2
# Kiwi: a Framework and Enhanced Widgets for Python
4
# Copyright (C) 2006 Async Open Source
6
# This library is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU Lesser General Public
8
# License as published by the Free Software Foundation; either
9
# version 2.1 of the License, or (at your option) any later version.
11
# This library is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
# Lesser General Public License for more details.
16
# You should have received a copy of the GNU Lesser General Public
17
# License along with this library; if not, write to the Free Software
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21
# Author(s): Johan Dahlin <jdahlin@async.com.br>
22
# Ronaldo Maia <romaia@async.com.br>
27
# When inserting new text, supose, the entry, at some time is like this,
28
# ahd the user presses '0', for instance:
29
# --------------------------------
30
# | ( 1 2 ) 3 4 5 - 6 7 8 9 |
31
# --------------------------------
35
# S - start of the field (start)
36
# E - end of the field (end)
37
# P - pos - where the new text is being inserted. (pos)
39
# So, the new text will be:
41
# the old text, from 0 until P
43
# + the old text, from P until the end of the field, shifted to the
45
# + the old text, from the end of the field, to the end of the text.
47
# After inserting, the text will be this:
48
# --------------------------------
49
# | ( 1 2 ) 3 0 4 5 - 6 7 8 9 |
50
# --------------------------------
55
# When deleting some text, supose, the entry, at some time is like this:
56
# --------------------------------
57
# | ( 1 2 ) 3 4 5 6 - 7 8 9 0 |
58
# --------------------------------
62
# S - start of the field (_start)
63
# E - end of the field (_end)
64
# s - start of the text being deleted (start)
65
# e - end of the text being deleted (end)
67
# end - start -> the number of characters being deleted.
69
# So, the new text will be:
71
# the old text, from 0 until the start of the text being deleted.
72
# + the old text, from the start of where the text is being deleted, until
73
# the end of the field, shifted to the left, end-start positions
74
# + the old text, from the end of the field, to the end of the text.
76
# So, after the text is deleted, the entry will look like this:
78
# --------------------------------
79
# | ( 1 2 ) 3 5 6 - 7 8 9 0 |
80
# --------------------------------
84
# P = the position of the cursor after the deletion, witch is equal to
85
# start (s at the previous illustration)
89
An enchanced version of GtkEntry that supports icons and masks
96
except AttributeError:
97
from sets import Set as set
103
from kiwi.enums import Direction
104
from kiwi.environ import environ
105
from kiwi.ui.icon import IconEntry
106
from kiwi.ui.entrycompletion import KiwiEntryCompletion
107
from kiwi.utils import PropertyObject, gsignal, gproperty, type_register
109
if not environ.epydoc:
110
HAVE_2_6 = gtk.pygtk_version[:2] <= (2, 6)
114
class MaskError(Exception):
120
INPUT_DIGIT) = range(4)
124
'L': INPUT_ASCII_LETTER,
125
'A': INPUT_ALPHANUMERIC,
126
'a': INPUT_ALPHANUMERIC,
130
# Todo list: Other usefull Masks
131
# 9 - Digit, optional
132
# ? - Ascii letter, optional
133
# C - Alpha, optional
136
INPUT_ASCII_LETTER: lambda text: text in string.ascii_letters,
137
INPUT_ALPHA: unicode.isalpha,
138
INPUT_ALPHANUMERIC: unicode.isalnum,
139
INPUT_DIGIT: unicode.isdigit,
144
COL_OBJECT) = range(2)
148
ENTRY_MODE_DATA) = range(3)
150
_ = lambda msg: gettext.dgettext('kiwi', msg)
152
class KiwiEntry(PropertyObject, gtk.Entry):
154
The KiwiEntry is a Entry subclass with the following additions:
156
- IconEntry, allows you to have an icon inside the entry
157
- Mask, force the input to meet certain requirements
158
- IComboMixin: Allows you work with objects instead of strings
159
Adds a number of convenience methods such as L{prefill}().
161
__gtype_name__ = 'KiwiEntry'
163
gproperty("completion", bool, False)
164
gproperty('exact-completion', bool, default=False)
165
gproperty("mask", str, default='')
168
self._completion = None
170
gtk.Entry.__init__(self)
171
PropertyObject.__init__(self)
173
self.connect('insert-text', self._on_insert_text)
174
self.connect('delete-text', self._on_delete_text)
175
self.connect_after('grab-focus', self._after_grab_focus)
177
self.connect('changed', self._on_changed)
179
self.connect('focus', self._on_focus)
180
self.connect('focus-out-event', self._on_focus_out_event)
181
self.connect('move-cursor', self._on_move_cursor)
183
# Ideally, this should be connected to notify::cursor-position, but
184
# there seems to be a bug in gtk that the notification is not emited
186
# TODO: investigate that and report a bug.
187
self.connect('notify::selection-bound',
188
self._on_notify_selection_bound)
190
self._block_changed = False
192
self._current_object = None
193
self._mode = ENTRY_MODE_TEXT
194
self._icon = IconEntry(self)
197
# str -> static characters
198
# int -> dynamic, according to constants above
199
self._mask_validators = []
201
# Fields defined by mask
202
# each item is a tuble, containing the begining and the end of the
204
self._mask_fields = []
205
self._current_field = -1
207
self._selecting = False
209
self._block_insert = False
210
self._block_delete = False
213
# PyGTK 2.6 does not support the virtual method do_size_allocate so
214
# we have to use the signal instead
215
# PyGTK 2.9.0 and later (bug #327715) does not work using the old code,
216
# so we have to make this conditionally
218
gsignal('size-allocate', 'override')
219
def do_size_allocate(self, allocation):
220
self.chain(allocation)
222
if self.flags() & gtk.REALIZED:
223
self._icon.resize_windows()
225
def do_size_allocate(self, allocation):
226
gtk.Entry.do_size_allocate(self, allocation)
228
if self.flags() & gtk.REALIZED:
229
self._icon.resize_windows()
231
def do_expose_event(self, event):
232
gtk.Entry.do_expose_event(self, event)
234
if event.window == self.window:
235
self._icon.draw_pixbuf()
237
def do_realize(self):
238
gtk.Entry.do_realize(self)
239
self._icon.construct()
241
def do_unrealize(self):
242
self._icon.deconstruct()
243
gtk.Entry.do_unrealize(self)
247
def prop_set_exact_completion(self, value):
248
self.set_exact_completion(value)
251
def prop_set_completion(self, value):
252
if not self.get_completion():
253
self.set_completion(gtk.EntryCompletion())
256
def prop_set_mask(self, value):
259
return self.get_mask()
265
def set_text(self, text):
266
completion = self.get_completion()
268
if isinstance(completion, KiwiEntryCompletion):
269
self.handler_block(completion.changed_id)
271
gtk.Entry.set_text(self, text)
273
if isinstance(completion, KiwiEntryCompletion):
274
self.handler_unblock(completion.changed_id)
278
def set_mask(self, mask):
280
Sets the mask of the Entry.
281
Supported format characters are:
283
- 'L' ascii letter (a-z and A-Z)
284
- '&' alphabet, honors the locale
285
- 'a' alphanumeric, honors the locale
286
- 'A' alphanumeric, honors the locale
288
This is similar to MaskedTextBox:
289
U{http://msdn2.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask(VS.80).aspx}
291
Example mask for a ISO-8601 date
292
>>> entry.set_mask('0000-00-00')
294
@param mask: the mask to set
298
self.modify_font(pango.FontDescription("sans"))
303
self._mask_validators = []
304
self._mask_fields = []
305
self._current_field = -1
308
input_length = len(mask)
314
if pos >= input_length:
316
if mask[pos] in INPUT_FORMATS:
317
self._mask_validators += [INPUT_FORMATS[mask[pos]]]
320
self._mask_validators.append(mask[pos])
321
if field_begin != field_end:
322
self._mask_fields.append((field_begin, field_end))
324
field_begin = field_end
327
self._mask_fields.append((field_begin, field_end))
328
self.modify_font(pango.FontDescription("monospace"))
330
self._really_delete_text(0, -1)
331
self._insert_mask(0, input_length)
340
def get_field_text(self, field):
342
raise MaskError("a mask must be set before calling get_field_text")
344
text = self.get_text()
345
start, end = self._mask_fields[field]
346
return text[start: end].strip()
348
def get_fields(self):
350
Get the fields assosiated with the entry.
351
A field is dynamic content separated by static.
352
For example, the format string 000-000 has two fields
354
if a field is empty it'll return an empty string
355
otherwise it'll include the content
358
@rtype: list of strings
361
raise MaskError("a mask must be set before calling get_fields")
365
text = unicode(self.get_text())
366
for start, end in self._mask_fields:
367
fields.append(text[start:end].strip())
371
def get_empty_mask(self, start=None, end=None):
373
Gets the empty mask between start and end
384
end = len(self._mask_validators)
387
for validator in self._mask_validators[start:end]:
388
if isinstance(validator, int):
390
elif isinstance(validator, unicode):
396
def get_field_pos(self, field):
398
Get the position at the specified field.
400
if field >= len(self._mask_fields):
403
start, end = self._mask_fields[field]
407
def _get_field_ideal_pos(self, field):
408
start, end = self._mask_fields[field]
409
text = self.get_field_text(field)
410
pos = start+len(text)
414
if self._current_field >= 0:
415
return self._current_field
419
def set_field(self, field, select=False):
420
if field >= len(self._mask_fields):
423
pos = self._get_field_ideal_pos(field)
424
self.set_position(pos)
427
field_text = self.get_field_text(field)
428
start, end = self._mask_fields[field]
429
self.select_region(start, pos)
431
self._current_field = field
433
def get_field_length(self, field):
434
if 0 <= field < len(self._mask_fields):
435
start, end = self._mask_fields[field]
438
def _shift_text(self, start, end, direction=Direction.LEFT,
441
Shift the text, to the right or left, n positions. Note that this
442
does not change the entry text. It returns the shifted text.
446
@param direction: see L{kiwi.enums.Direction}
447
@param positions: the number of positions to shift.
449
@return: returns the text between start and end, shifted to
450
the direction provided.
452
text = self.get_text()
454
validators = self._mask_validators
456
if direction == Direction.LEFT:
461
# When shifting a text, we wanna keep the static chars where they
462
# are, and move the non-static chars to the right position.
463
while start <= i < end:
464
if isinstance(validators[i], int):
465
# Non-static char shoud be here. Get the next one (depending
466
# on the direction, and the number of positions to skip.)
468
# When shifting left, the next char will be on the right,
469
# so, it will be appended, to the new text.
470
# Otherwise, when shifting right, the char will be
472
next_pos = self._get_next_non_static_char_pos(i, direction,
475
# If its outside the bounds of the region, ignore it.
476
if not start <= next_pos <= end:
479
if next_pos is not None:
480
if direction == Direction.LEFT:
481
new_text = new_text + text[next_pos]
483
new_text = text[next_pos] + new_text
485
if direction == Direction.LEFT:
486
new_text = new_text + ' '
488
new_text = ' ' + new_text
491
# Keep the static char where it is.
492
if direction == Direction.LEFT:
493
new_text = new_text + text[i]
495
new_text = text[i] + new_text
500
def _get_next_non_static_char_pos(self, pos, direction=Direction.LEFT,
503
Get next non-static char position, skiping some chars, if necessary.
504
@param skip: skip first n chars
505
@param direction: direction of the search.
507
text = self.get_text()
508
validators = self._mask_validators
509
i = pos+direction+skip
510
while 0 <= i < len(text):
511
if isinstance(validators[i], int):
517
def _get_field_at_pos(self, pos, dir=None):
519
Return the field index at position pos.
521
for p in self._mask_fields:
522
if p[0] <= pos <= p[1]:
523
return self._mask_fields.index(p)
527
def set_exact_completion(self, value):
529
Enable exact entry completion.
530
Exact means it needs to start with the value typed
531
and the case needs to be correct.
533
@param value: enable exact completion
538
match_func = self._completion_exact_match_func
540
match_func = self._completion_normal_match_func
541
completion = self._get_completion()
542
completion.set_match_func(match_func)
545
text = self.get_text()
547
empty = self.get_empty_mask()
555
def _really_delete_text(self, start, end):
556
# A variant of delete_text() that never is blocked by us
557
self._block_delete = True
558
self.delete_text(start, end)
559
self._block_delete = False
561
def _really_insert_text(self, text, position):
562
# A variant of insert_text() that never is blocked by us
563
self._block_insert = True
564
self.insert_text(text, position)
565
self._block_insert = False
567
def _insert_mask(self, start, end):
568
text = self.get_empty_mask(start, end)
569
self._really_insert_text(text, position=start)
571
def _confirms_to_mask(self, position, text):
572
validators = self._mask_validators
573
if position < 0 or position >= len(validators):
576
validator = validators[position]
577
if isinstance(validator, int):
578
if not INPUT_CHAR_MAP[validator](text):
580
if isinstance(validator, unicode):
581
if validator == text:
587
def _update_current_object(self, text):
588
if self._mode != ENTRY_MODE_DATA:
591
for row in self.get_completion().get_model():
592
if row[COL_TEXT] == text:
593
self._current_object = row[COL_OBJECT]
596
# Customized validation
598
self.set_invalid(_("'%s' is not a valid object" % text))
603
self._current_object = None
605
def _get_text_from_object(self, obj):
606
if self._mode != ENTRY_MODE_DATA:
609
for row in self.get_completion().get_model():
610
if row[COL_OBJECT] == obj:
613
def _get_completion(self):
614
# Check so we have completion enabled, not this does not
615
# depend on the property, the user can manually override it,
616
# as long as there is a completion object set
617
completion = self.get_completion()
621
completion = gtk.EntryCompletion()
622
self.set_completion(completion)
625
def get_completion(self):
626
return self._completion
628
def set_completion(self, completion):
629
if not isinstance(completion, KiwiEntryCompletion):
630
gtk.Entry.set_completion(self, completion)
631
completion.set_model(gtk.ListStore(str, object))
632
completion.set_text_column(0)
633
self._completion = gtk.Entry.get_completion(self)
636
old = self.get_completion()
637
if old == completion:
640
if old and isinstance(old, KiwiEntryCompletion):
641
if old.completion_timeout:
642
gobject.source_remove(old.completion_timeout)
643
old.completion_timeout = 0
645
old._disconnect_completion_signals()
647
self._completion = completion
649
# First, tell the completion what entry it will complete
650
completion.set_entry(self)
651
completion.set_model(gtk.ListStore(str, object))
652
completion.set_text_column(0)
653
self.set_exact_completion(False)
654
completion.connect("match-selected",
655
self._on_completion__match_selected)
656
self._current_object = None
659
def _completion_exact_match_func(self, completion, key, iter):
660
model = completion.get_model()
664
content = model[iter][COL_TEXT]
665
return key.startswith(content)
667
def _completion_normal_match_func(self, completion, key, iter):
668
model = completion.get_model()
671
raw_content = model[iter][COL_TEXT]
672
if raw_content is not None:
673
return key.lower() in raw_content.lower()
677
def _on_completion__match_selected(self, completion, model, iter):
681
# this updates current_object and triggers content-changed
682
self.set_text(model[iter][COL_TEXT])
683
self.set_position(-1)
684
# FIXME: Enable this at some point
687
def _appers_later(self, char, start):
689
Check if a char appers later on the mask. If it does, return
690
the field it appers at. returns False otherwise.
692
validators = self._mask_validators
694
while i < len(validators):
695
if self._mask_validators[i] == char:
696
field = self._get_field_at_pos(i)
706
def _can_insert_at_pos(self, new, pos):
708
Check if a chararcter can be inserted at some position
710
@param new: The char that wants to be inserted.
711
@param pos: The position where it wants to be inserted.
713
@return: Returns None if it can be inserted. If it cannot be,
714
return the next position where it can be successfuly
717
validators = self._mask_validators
719
# Do not let insert if the field is full
720
field = self._get_field_at_pos(pos)
721
if field is not None:
722
text = self.get_field_text(field)
723
length = self.get_field_length(field)
724
if len(text) == length:
728
# If the char confirms to the mask, but is a static char, return the
729
# position after that static char.
730
if (self._confirms_to_mask(pos, new) and
731
not isinstance(validators[pos], int)):
734
# If does not confirms to mask:
735
# - Check if the char the user just tried to enter appers later.
736
# - If it does, Jump to the start of the field after that
737
if not self._confirms_to_mask(pos, new):
738
field = self._appers_later(new, pos)
739
if field is not False:
740
pos = self.get_field_pos(field+1)
742
gobject.idle_add(self.set_position, pos)
747
def _insert_at_pos(self, text, new, pos):
749
Inserts the character at the give position in text. Note that the
750
insertion won't be applied to the entry, but to the text provided.
752
@param text: Text that it will be inserted into.
753
@param new: New text to insert.
754
@param pos: Positon to insert at
756
@return: Returns a tuple, with the position after the insetion
759
field = self._get_field_at_pos(pos)
762
start, end = self._mask_fields[field]
765
new_text = (text[:pos] + new +
766
self._shift_text(pos, end, Direction.RIGHT)[1:] +
770
# new_text = (text[:pos] + new +
771
# text[pos+length:end]+
774
gobject.idle_add(self.set_position, new_pos)
776
# If the field is full, jump to the next field
777
if len(self.get_field_text(field)) == self.get_field_length(field)-1:
778
gobject.idle_add(self.set_field, field+1, True)
779
self.set_field(field+1)
781
return new_pos, new_text
784
def _on_insert_text(self, editable, new, length, position):
785
if not self._mask or self._block_insert:
788
pos = self.get_position()
790
self.stop_emission('insert-text')
792
text = self.get_text()
793
# Insert one char at a time
795
_pos = self._can_insert_at_pos(c, pos)
797
pos, text = self._insert_at_pos(text, c, pos)
801
# Change the text with the new text.
802
self._block_changed = True
803
self._really_delete_text(0, -1)
804
self._block_changed = False
806
self._really_insert_text(text, 0)
808
def _on_delete_text(self, editable, start, end):
809
if not self._mask or self._block_delete:
812
self.stop_emission('delete-text')
814
pos = self.get_position()
815
# Trying to delete an static char. Delete the char before that
816
if (0 < start < len(self._mask_validators)
817
and not isinstance(self._mask_validators[start], int)
819
self._on_delete_text(editable, start-1, start)
822
# we just tried to delete, stop the selection.
823
self._selecting = False
825
field = self._get_field_at_pos(end-1)
826
# Outside a field. Cannot delete.
828
self.set_position(end-1)
830
_start, _end = self._mask_fields[field]
832
# Deleting from outside the bounds of the field.
833
if start < _start or end > _end:
834
_start, _end = start, end
837
text = self.get_text()
840
new_text = (text[:start] +
841
self._shift_text(start, _end, Direction.LEFT,
846
# empty_mask = self.get_empty_mask()
847
# new_text = (text[:_start] +
848
# text[_start:start] +
849
# empty_mask[start:start+(end-start)] +
850
# text[start+(end-start):_end] +
855
self._block_changed = True
856
self._really_delete_text(0, -1)
857
self._block_changed = False
858
self._really_insert_text(new_text, 0)
860
# Position the cursor on the right place.
861
self.set_position(new_pos)
863
self._handle_position_change()
865
def _after_grab_focus(self, widget):
866
# The text is selectet in grab-focus, so this needs to be done after
874
def _on_focus(self, widget, direction):
878
if (direction == gtk.DIR_TAB_FORWARD or
879
direction == gtk.DIR_DOWN):
881
if (direction == gtk.DIR_TAB_BACKWARD or
882
direction == gtk.DIR_UP):
885
field = self._current_field
889
if field == len(self._mask_fields) or field == -1:
890
self.select_region(0, 0)
891
self._current_field = -1
895
field = len(self._mask_fields)-1
897
# grab_focus changes the selection, so we need to grab_focus before
898
# making the selection.
900
self.set_field(field, select=True)
904
def _on_notify_selection_bound(self, widget, pspec):
908
if not self.is_focus():
914
self._handle_position_change()
916
def _handle_position_change(self):
917
pos = self.get_position()
918
field = self._get_field_at_pos(pos)
920
# Humm, the pos is not inside any field. Get the next pos inside
921
# some field, depending on the direction that the cursor is
923
diff = pos - self._pos
924
# but move only one position at a time.
930
while _field is None and pos >= 0:
932
_field = self._get_field_at_pos(pos)
935
self._pos = self.get_field_pos(0)
938
self.set_position(self._pos)
940
self._current_field = field
943
def _on_changed(self, widget):
944
if self._block_changed:
945
self.stop_emission('changed')
947
def _on_focus_out_event(self, widget, event):
951
self._current_field = -1
953
def _on_move_cursor(self, entry, step, count, extend_selection):
954
self._selecting = extend_selection
958
def set_tooltip(self, text):
959
self._icon.set_tooltip(text)
961
def set_pixbuf(self, pixbuf):
962
self._icon.set_pixbuf(pixbuf)
964
def update_background(self, color):
965
self._icon.update_background(color)
967
def get_background(self):
968
return self._icon.get_background()
970
def get_icon_window(self):
971
return self._icon.get_icon_window()
975
def prefill(self, itemdata, sort=False):
977
See L{kiwi.interfaces.IEasyCombo.prefill}
980
if not isinstance(itemdata, (list, tuple)):
981
raise TypeError("'data' parameter must be a list or tuple of item "
982
"descriptions, found %s") % type(itemdata)
984
completion = self._get_completion()
985
model = completion.get_model()
987
if len(itemdata) == 0:
991
if (len(itemdata) > 0 and
992
type(itemdata[0]) in (tuple, list) and
993
len(itemdata[0]) == 2):
994
mode = self._mode = ENTRY_MODE_DATA
999
if mode == ENTRY_MODE_TEXT:
1003
for item in itemdata:
1005
raise KeyError("Tried to insert duplicate value "
1006
"%r into the entry" % item)
1010
model.append((item, None))
1011
elif mode == ENTRY_MODE_DATA:
1013
itemdata.sort(lambda x, y: cmp(x[0], y[0]))
1015
for item in itemdata:
1017
# Add (n) to the end in case of duplicates
1020
while text in values:
1021
text = orig + ' (%d)' % count
1025
model.append((text, data))
1027
raise TypeError("Incorrect format for itemdata; see "
1028
"docstring for more information")
1030
def get_iter_by_data(self, data):
1031
if self._mode != ENTRY_MODE_DATA:
1033
"select_item_by_data can only be used in data mode")
1035
completion = self._get_completion()
1036
model = completion.get_model()
1039
if row[COL_OBJECT] == data:
1043
raise KeyError("No item correspond to data %r in the combo %s"
1044
% (data, self.name))
1046
def get_iter_by_label(self, label):
1047
completion = self._get_completion()
1048
model = completion.get_model()
1050
if row[COL_TEXT] == label:
1053
raise KeyError("No item correspond to label %r in the combo %s"
1054
% (label, self.name))
1056
def get_selected_by_iter(self, treeiter):
1057
completion = self._get_completion()
1058
model = completion.get_model()
1060
text = model[treeiter][COL_TEXT]
1061
if text != self.get_text():
1064
if mode == ENTRY_MODE_TEXT:
1066
elif mode == ENTRY_MODE_DATA:
1067
return model[treeiter][COL_OBJECT]
1069
raise AssertionError
1071
def get_selected_label(self, treeiter):
1072
completion = self._get_completion()
1073
model = completion.get_model()
1074
return model[treeiter][COL_TEXT]
1076
def get_selected_data(self, treeiter):
1077
completion = self._get_completion()
1078
model = completion.get_model()
1079
return model[treeiter][COL_OBJECT]
1081
def get_iter_from_obj(self, obj):
1083
if mode == ENTRY_MODE_TEXT:
1084
return self.get_iter_by_label(obj)
1085
elif mode == ENTRY_MODE_DATA:
1086
return self.get_iter_by_data(obj)
1088
# XXX: When setting the datatype to non string, automatically go to
1090
raise TypeError("unknown Entry mode. Did you call prefill?")
1095
type_register(KiwiEntry)
1099
win.set_title('gtk.Entry subclass')
1100
def cb(window, event):
1101
print 'fields', widget.get_field_text()
1103
win.connect('delete-event', cb)
1105
widget = KiwiEntry()
1106
widget.set_mask('000.000.000.000')
1112
widget.select_region(0, 0)
1115
if __name__ == '__main__':
1117
sys.exit(main(sys.argv))