~ubuntu-branches/ubuntu/natty/pida/natty

« back to all changes in this revision

Viewing changes to contrib/kiwi/kiwi/ui/entry.py

  • Committer: Bazaar Package Importer
  • Author(s): Jan Luebbe
  • Date: 2007-09-05 17:54:09 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20070905175409-ty9f6qpuctyjv1sd
Tags: 0.5.1-2
* Depend on librsvg2-common, which is not pulled in by the other depends
  (closes: #394860)
* gvim is no alternative for python-gnome2 and python-gnome2-extras
  (closes: #436431)
* Pida now uses ~/.pida2, so it can no longer be confused by old
  configurations (closes: #421378)
* Culebra is no longer supported by upstream (closes: #349009)
* Update manpage (closes: #440375)
* Update watchfile (pida is now called PIDA)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Kiwi: a Framework and Enhanced Widgets for Python
 
3
#
 
4
# Copyright (C) 2006 Async Open Source
 
5
#
 
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.
 
10
#
 
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.
 
15
#
 
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
 
19
# USA
 
20
#
 
21
# Author(s): Johan Dahlin <jdahlin@async.com.br>
 
22
#            Ronaldo Maia <romaia@async.com.br>
 
23
#
 
24
#
 
25
# Design notes:
 
26
#
 
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
#   --------------------------------
 
32
#              ^ ^     ^
 
33
#              S P     E
 
34
#
 
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)
 
38
#
 
39
#   So, the new text will be:
 
40
#
 
41
#     the old text, from 0 until P
 
42
#   + the new text
 
43
#   + the old text, from P until the end of the field, shifted to the
 
44
#     right
 
45
#   + the old text, from the end of the field, to the end of the text.
 
46
#
 
47
#   After inserting, the text will be this:
 
48
#   --------------------------------
 
49
#   | ( 1 2 )   3 0 4 5 - 6 7 8 9  |
 
50
#   --------------------------------
 
51
#              ^   ^   ^
 
52
#              S   P   E
 
53
#
 
54
#
 
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
#   --------------------------------
 
59
#              ^ ^ ^   ^
 
60
#              S s e   E
 
61
#
 
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)
 
66
#
 
67
#   end - start -> the number of characters being deleted.
 
68
#
 
69
#   So, the new text will be:
 
70
#
 
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.
 
75
#
 
76
#   So, after the text is deleted, the entry will look like this:
 
77
#
 
78
#   --------------------------------
 
79
#   | ( 1 2 )   3 5 6   - 7 8 9 0  |
 
80
#   --------------------------------
 
81
#                ^
 
82
#                P
 
83
#
 
84
#   P = the position of the cursor after the deletion, witch is equal to
 
85
#   start (s at the previous illustration)
 
86
 
 
87
 
 
88
"""
 
89
An enchanced version of GtkEntry that supports icons and masks
 
90
"""
 
91
 
 
92
import gettext
 
93
import string
 
94
try:
 
95
    set
 
96
except AttributeError:
 
97
    from sets import Set as set
 
98
 
 
99
import gobject
 
100
import pango
 
101
import gtk
 
102
 
 
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
 
108
 
 
109
if not environ.epydoc:
 
110
    HAVE_2_6 = gtk.pygtk_version[:2] <= (2, 6)
 
111
else:
 
112
    HAVE_2_6 = True
 
113
 
 
114
class MaskError(Exception):
 
115
    pass
 
116
 
 
117
(INPUT_ASCII_LETTER,
 
118
 INPUT_ALPHA,
 
119
 INPUT_ALPHANUMERIC,
 
120
 INPUT_DIGIT) = range(4)
 
121
 
 
122
INPUT_FORMATS = {
 
123
    '0': INPUT_DIGIT,
 
124
    'L': INPUT_ASCII_LETTER,
 
125
    'A': INPUT_ALPHANUMERIC,
 
126
    'a': INPUT_ALPHANUMERIC,
 
127
    '&': INPUT_ALPHA,
 
128
    }
 
129
 
 
130
# Todo list: Other usefull Masks
 
131
#  9 - Digit, optional
 
132
#  ? - Ascii letter, optional
 
133
#  C - Alpha, optional
 
134
 
 
135
INPUT_CHAR_MAP = {
 
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,
 
140
    }
 
141
 
 
142
 
 
143
(COL_TEXT,
 
144
 COL_OBJECT) = range(2)
 
145
 
 
146
(ENTRY_MODE_UNKNOWN,
 
147
 ENTRY_MODE_TEXT,
 
148
 ENTRY_MODE_DATA) = range(3)
 
149
 
 
150
_ = lambda msg: gettext.dgettext('kiwi', msg)
 
151
 
 
152
class KiwiEntry(PropertyObject, gtk.Entry):
 
153
    """
 
154
    The KiwiEntry is a Entry subclass with the following additions:
 
155
 
 
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}().
 
160
    """
 
161
    __gtype_name__ = 'KiwiEntry'
 
162
 
 
163
    gproperty("completion", bool, False)
 
164
    gproperty('exact-completion', bool, default=False)
 
165
    gproperty("mask", str, default='')
 
166
 
 
167
    def __init__(self):
 
168
        self._completion = None
 
169
 
 
170
        gtk.Entry.__init__(self)
 
171
        PropertyObject.__init__(self)
 
172
 
 
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)
 
176
 
 
177
        self.connect('changed', self._on_changed)
 
178
 
 
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)
 
182
 
 
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
 
185
        # when it should.
 
186
        # TODO: investigate that and report a bug.
 
187
        self.connect('notify::selection-bound',
 
188
                     self._on_notify_selection_bound)
 
189
 
 
190
        self._block_changed = False
 
191
 
 
192
        self._current_object = None
 
193
        self._mode = ENTRY_MODE_TEXT
 
194
        self._icon = IconEntry(self)
 
195
 
 
196
        # List of validators
 
197
        #  str -> static characters
 
198
        #  int -> dynamic, according to constants above
 
199
        self._mask_validators = []
 
200
        self._mask = None
 
201
        # Fields defined by mask
 
202
        # each item is a tuble, containing the begining and the end of the
 
203
        # field in the text
 
204
        self._mask_fields = []
 
205
        self._current_field = -1
 
206
        self._pos = 0
 
207
        self._selecting = False
 
208
 
 
209
        self._block_insert = False
 
210
        self._block_delete = False
 
211
 
 
212
    # Virtual methods
 
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
 
217
    if HAVE_2_6:
 
218
        gsignal('size-allocate', 'override')
 
219
        def do_size_allocate(self, allocation):
 
220
            self.chain(allocation)
 
221
 
 
222
            if self.flags() & gtk.REALIZED:
 
223
                self._icon.resize_windows()
 
224
    else:
 
225
        def do_size_allocate(self, allocation):
 
226
            gtk.Entry.do_size_allocate(self, allocation)
 
227
 
 
228
            if self.flags() & gtk.REALIZED:
 
229
                self._icon.resize_windows()
 
230
 
 
231
    def do_expose_event(self, event):
 
232
        gtk.Entry.do_expose_event(self, event)
 
233
 
 
234
        if event.window == self.window:
 
235
            self._icon.draw_pixbuf()
 
236
 
 
237
    def do_realize(self):
 
238
        gtk.Entry.do_realize(self)
 
239
        self._icon.construct()
 
240
 
 
241
    def do_unrealize(self):
 
242
        self._icon.deconstruct()
 
243
        gtk.Entry.do_unrealize(self)
 
244
 
 
245
    # Properties
 
246
 
 
247
    def prop_set_exact_completion(self, value):
 
248
        self.set_exact_completion(value)
 
249
        return value
 
250
 
 
251
    def prop_set_completion(self, value):
 
252
        if not self.get_completion():
 
253
            self.set_completion(gtk.EntryCompletion())
 
254
        return value
 
255
 
 
256
    def prop_set_mask(self, value):
 
257
        try:
 
258
            self.set_mask(value)
 
259
            return self.get_mask()
 
260
        except MaskError, e:
 
261
            pass
 
262
        return ''
 
263
 
 
264
    # Public API
 
265
    def set_text(self, text):
 
266
        completion = self.get_completion()
 
267
 
 
268
        if isinstance(completion, KiwiEntryCompletion):
 
269
            self.handler_block(completion.changed_id)
 
270
 
 
271
        gtk.Entry.set_text(self, text)
 
272
 
 
273
        if isinstance(completion, KiwiEntryCompletion):
 
274
            self.handler_unblock(completion.changed_id)
 
275
 
 
276
    # Mask & Fields
 
277
 
 
278
    def set_mask(self, mask):
 
279
        """
 
280
        Sets the mask of the Entry.
 
281
        Supported format characters are:
 
282
          - '0' digit
 
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
 
287
 
 
288
        This is similar to MaskedTextBox:
 
289
        U{http://msdn2.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask(VS.80).aspx}
 
290
 
 
291
        Example mask for a ISO-8601 date
 
292
        >>> entry.set_mask('0000-00-00')
 
293
 
 
294
        @param mask: the mask to set
 
295
        """
 
296
 
 
297
        if not mask:
 
298
            self.modify_font(pango.FontDescription("sans"))
 
299
            self._mask = mask
 
300
            return
 
301
 
 
302
        # First, reset
 
303
        self._mask_validators = []
 
304
        self._mask_fields = []
 
305
        self._current_field = -1
 
306
 
 
307
        mask = unicode(mask)
 
308
        input_length = len(mask)
 
309
        lenght = 0
 
310
        pos = 0
 
311
        field_begin = 0
 
312
        field_end = 0
 
313
        while True:
 
314
            if pos >= input_length:
 
315
                break
 
316
            if mask[pos] in INPUT_FORMATS:
 
317
                self._mask_validators += [INPUT_FORMATS[mask[pos]]]
 
318
                field_end += 1
 
319
            else:
 
320
                self._mask_validators.append(mask[pos])
 
321
                if field_begin != field_end:
 
322
                    self._mask_fields.append((field_begin, field_end))
 
323
                field_end += 1
 
324
                field_begin = field_end
 
325
            pos += 1
 
326
 
 
327
        self._mask_fields.append((field_begin, field_end))
 
328
        self.modify_font(pango.FontDescription("monospace"))
 
329
 
 
330
        self._really_delete_text(0, -1)
 
331
        self._insert_mask(0, input_length)
 
332
        self._mask = mask
 
333
 
 
334
    def get_mask(self):
 
335
        """
 
336
        @returns: the mask
 
337
        """
 
338
        return self._mask
 
339
 
 
340
    def get_field_text(self, field):
 
341
        if not self._mask:
 
342
            raise MaskError("a mask must be set before calling get_field_text")
 
343
 
 
344
        text = self.get_text()
 
345
        start, end = self._mask_fields[field]
 
346
        return text[start: end].strip()
 
347
 
 
348
    def get_fields(self):
 
349
        """
 
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
 
353
        separated by a dash.
 
354
        if a field is empty it'll return an empty string
 
355
        otherwise it'll include the content
 
356
 
 
357
        @returns: fields
 
358
        @rtype: list of strings
 
359
        """
 
360
        if not self._mask:
 
361
            raise MaskError("a mask must be set before calling get_fields")
 
362
 
 
363
        fields = []
 
364
 
 
365
        text = unicode(self.get_text())
 
366
        for start, end in self._mask_fields:
 
367
            fields.append(text[start:end].strip())
 
368
 
 
369
        return fields
 
370
 
 
371
    def get_empty_mask(self, start=None, end=None):
 
372
        """
 
373
        Gets the empty mask between start and end
 
374
 
 
375
        @param start:
 
376
        @param end:
 
377
        @returns: mask
 
378
        @rtype: string
 
379
        """
 
380
 
 
381
        if start is None:
 
382
            start = 0
 
383
        if end is None:
 
384
            end = len(self._mask_validators)
 
385
 
 
386
        s = ''
 
387
        for validator in self._mask_validators[start:end]:
 
388
            if isinstance(validator, int):
 
389
                s += ' '
 
390
            elif isinstance(validator, unicode):
 
391
                s += validator
 
392
            else:
 
393
                raise AssertionError
 
394
        return s
 
395
 
 
396
    def get_field_pos(self, field):
 
397
        """
 
398
        Get the position at the specified field.
 
399
        """
 
400
        if field >= len(self._mask_fields):
 
401
            return None
 
402
 
 
403
        start, end = self._mask_fields[field]
 
404
 
 
405
        return start
 
406
 
 
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)
 
411
        return pos
 
412
 
 
413
    def get_field(self):
 
414
        if self._current_field >= 0:
 
415
            return self._current_field
 
416
        else:
 
417
            return None
 
418
 
 
419
    def set_field(self, field, select=False):
 
420
        if field >= len(self._mask_fields):
 
421
            return
 
422
 
 
423
        pos = self._get_field_ideal_pos(field)
 
424
        self.set_position(pos)
 
425
 
 
426
        if select:
 
427
            field_text = self.get_field_text(field)
 
428
            start, end = self._mask_fields[field]
 
429
            self.select_region(start, pos)
 
430
 
 
431
        self._current_field = field
 
432
 
 
433
    def get_field_length(self, field):
 
434
        if 0 <= field < len(self._mask_fields):
 
435
            start, end = self._mask_fields[field]
 
436
            return end - start
 
437
 
 
438
    def _shift_text(self, start, end, direction=Direction.LEFT,
 
439
                    positions=1):
 
440
        """
 
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.
 
443
 
 
444
        @param start:
 
445
        @param end:
 
446
        @param direction:   see L{kiwi.enums.Direction}
 
447
        @param positions:   the number of positions to shift.
 
448
 
 
449
        @return:        returns the text between start and end, shifted to
 
450
                        the direction provided.
 
451
        """
 
452
        text = self.get_text()
 
453
        new_text = ''
 
454
        validators = self._mask_validators
 
455
 
 
456
        if direction == Direction.LEFT:
 
457
            i = start
 
458
        else:
 
459
            i = end - 1
 
460
 
 
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.)
 
467
                #
 
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
 
471
                # prepended.
 
472
                next_pos = self._get_next_non_static_char_pos(i, direction,
 
473
                                                              positions-1)
 
474
 
 
475
                # If its outside the bounds of the region, ignore it.
 
476
                if not start <= next_pos <= end:
 
477
                    next_pos = None
 
478
 
 
479
                if next_pos is not None:
 
480
                    if direction == Direction.LEFT:
 
481
                        new_text = new_text + text[next_pos]
 
482
                    else:
 
483
                        new_text = text[next_pos] + new_text
 
484
                else:
 
485
                    if direction == Direction.LEFT:
 
486
                        new_text = new_text + ' '
 
487
                    else:
 
488
                        new_text = ' ' + new_text
 
489
 
 
490
            else:
 
491
                # Keep the static char where it is.
 
492
                if direction == Direction.LEFT:
 
493
                   new_text = new_text + text[i]
 
494
                else:
 
495
                   new_text = text[i] + new_text
 
496
            i += direction
 
497
 
 
498
        return new_text
 
499
 
 
500
    def _get_next_non_static_char_pos(self, pos, direction=Direction.LEFT,
 
501
                                      skip=0):
 
502
        """
 
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.
 
506
        """
 
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):
 
512
                return i
 
513
            i += direction
 
514
 
 
515
        return None
 
516
 
 
517
    def _get_field_at_pos(self, pos, dir=None):
 
518
        """
 
519
        Return the field index at position pos.
 
520
        """
 
521
        for p in self._mask_fields:
 
522
            if p[0] <= pos <= p[1]:
 
523
                return self._mask_fields.index(p)
 
524
 
 
525
        return None
 
526
 
 
527
    def set_exact_completion(self, value):
 
528
        """
 
529
        Enable exact entry completion.
 
530
        Exact means it needs to start with the value typed
 
531
        and the case needs to be correct.
 
532
 
 
533
        @param value: enable exact completion
 
534
        @type value:  boolean
 
535
        """
 
536
 
 
537
        if value:
 
538
            match_func = self._completion_exact_match_func
 
539
        else:
 
540
            match_func = self._completion_normal_match_func
 
541
        completion = self._get_completion()
 
542
        completion.set_match_func(match_func)
 
543
 
 
544
    def is_empty(self):
 
545
        text = self.get_text()
 
546
        if self._mask:
 
547
            empty = self.get_empty_mask()
 
548
        else:
 
549
            empty = ''
 
550
 
 
551
        return text == empty
 
552
 
 
553
    # Private
 
554
 
 
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
 
560
 
 
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
 
566
 
 
567
    def _insert_mask(self, start, end):
 
568
        text = self.get_empty_mask(start, end)
 
569
        self._really_insert_text(text, position=start)
 
570
 
 
571
    def _confirms_to_mask(self, position, text):
 
572
        validators = self._mask_validators
 
573
        if position < 0 or position >= len(validators):
 
574
            return False
 
575
 
 
576
        validator = validators[position]
 
577
        if isinstance(validator, int):
 
578
            if not INPUT_CHAR_MAP[validator](text):
 
579
                return False
 
580
        if isinstance(validator, unicode):
 
581
            if validator == text:
 
582
                return True
 
583
            return False
 
584
 
 
585
        return True
 
586
 
 
587
    def _update_current_object(self, text):
 
588
        if self._mode != ENTRY_MODE_DATA:
 
589
            return
 
590
 
 
591
        for row in self.get_completion().get_model():
 
592
            if row[COL_TEXT] == text:
 
593
                self._current_object = row[COL_OBJECT]
 
594
                break
 
595
        else:
 
596
            # Customized validation
 
597
            if text:
 
598
                self.set_invalid(_("'%s' is not a valid object" % text))
 
599
            elif self.mandatory:
 
600
                self.set_blank()
 
601
            else:
 
602
                self.set_valid()
 
603
            self._current_object = None
 
604
 
 
605
    def _get_text_from_object(self, obj):
 
606
        if self._mode != ENTRY_MODE_DATA:
 
607
            return
 
608
 
 
609
        for row in self.get_completion().get_model():
 
610
            if row[COL_OBJECT] == obj:
 
611
                return row[COL_TEXT]
 
612
 
 
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()
 
618
        if completion:
 
619
            return completion
 
620
 
 
621
        completion = gtk.EntryCompletion()
 
622
        self.set_completion(completion)
 
623
        return completion
 
624
 
 
625
    def get_completion(self):
 
626
        return self._completion
 
627
 
 
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)
 
634
            return
 
635
 
 
636
        old = self.get_completion()
 
637
        if old == completion:
 
638
            return completion
 
639
 
 
640
        if old and isinstance(old, KiwiEntryCompletion):
 
641
            if old.completion_timeout:
 
642
                gobject.source_remove(old.completion_timeout)
 
643
                old.completion_timeout = 0
 
644
 
 
645
            old._disconnect_completion_signals()
 
646
 
 
647
        self._completion = completion
 
648
 
 
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
 
657
        return completion
 
658
 
 
659
    def _completion_exact_match_func(self, completion, key, iter):
 
660
        model = completion.get_model()
 
661
        if not len(model):
 
662
            return
 
663
 
 
664
        content = model[iter][COL_TEXT]
 
665
        return key.startswith(content)
 
666
 
 
667
    def _completion_normal_match_func(self, completion, key, iter):
 
668
        model = completion.get_model()
 
669
        if not len(model):
 
670
            return
 
671
        raw_content = model[iter][COL_TEXT]
 
672
        if raw_content is not None:
 
673
            return key.lower() in raw_content.lower()
 
674
        else:
 
675
            return False
 
676
 
 
677
    def _on_completion__match_selected(self, completion, model, iter):
 
678
        if not len(model):
 
679
            return
 
680
 
 
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
 
685
        #self.activate()
 
686
 
 
687
    def _appers_later(self, char, start):
 
688
        """
 
689
        Check if a char appers later on the mask. If it does, return
 
690
        the field it appers at. returns False otherwise.
 
691
        """
 
692
        validators = self._mask_validators
 
693
        i = start
 
694
        while i < len(validators):
 
695
            if self._mask_validators[i] == char:
 
696
                field = self._get_field_at_pos(i)
 
697
                if field is None:
 
698
                    return False
 
699
 
 
700
                return field
 
701
 
 
702
            i += 1
 
703
 
 
704
        return False
 
705
 
 
706
    def _can_insert_at_pos(self, new, pos):
 
707
        """
 
708
        Check if a chararcter can be inserted at some position
 
709
 
 
710
        @param new: The char that wants to be inserted.
 
711
        @param pos: The position where it wants to be inserted.
 
712
 
 
713
        @return: Returns None if it can be inserted. If it cannot be,
 
714
                 return the next position where it can be successfuly
 
715
                 inserted.
 
716
        """
 
717
        validators = self._mask_validators
 
718
 
 
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:
 
725
                gtk.gdk.beep()
 
726
                return pos
 
727
 
 
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)):
 
732
            return pos+1
 
733
 
 
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)
 
741
                if pos is not None:
 
742
                    gobject.idle_add(self.set_position, pos)
 
743
            return pos
 
744
 
 
745
        return None
 
746
 
 
747
    def _insert_at_pos(self, text, new, pos):
 
748
        """
 
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.
 
751
 
 
752
        @param text:    Text that it will be inserted into.
 
753
        @param new:     New text to insert.
 
754
        @param pos:     Positon to insert at
 
755
 
 
756
        @return:    Returns a tuple, with the position after the insetion
 
757
                    and the new text.
 
758
        """
 
759
        field = self._get_field_at_pos(pos)
 
760
        length = len(new)
 
761
        new_pos = pos
 
762
        start, end = self._mask_fields[field]
 
763
 
 
764
        # Shift Right
 
765
        new_text = (text[:pos] + new +
 
766
                    self._shift_text(pos, end, Direction.RIGHT)[1:] +
 
767
                    text[end:])
 
768
 
 
769
        # Overwrite Right
 
770
#        new_text = (text[:pos] + new +
 
771
#                    text[pos+length:end]+
 
772
#                    text[end:])
 
773
        new_pos = pos+1
 
774
        gobject.idle_add(self.set_position, new_pos)
 
775
 
 
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)
 
780
 
 
781
        return new_pos, new_text
 
782
 
 
783
    # Callbacks
 
784
    def _on_insert_text(self, editable, new, length, position):
 
785
        if not self._mask or self._block_insert:
 
786
            return
 
787
        new = unicode(new)
 
788
        pos = self.get_position()
 
789
 
 
790
        self.stop_emission('insert-text')
 
791
 
 
792
        text = self.get_text()
 
793
        # Insert one char at a time
 
794
        for c in new:
 
795
            _pos = self._can_insert_at_pos(c, pos)
 
796
            if _pos is None:
 
797
                pos, text = self._insert_at_pos(text, c, pos)
 
798
            else:
 
799
                pos = _pos
 
800
 
 
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
 
805
 
 
806
        self._really_insert_text(text, 0)
 
807
 
 
808
    def _on_delete_text(self, editable, start, end):
 
809
        if not self._mask or self._block_delete:
 
810
            return
 
811
 
 
812
        self.stop_emission('delete-text')
 
813
 
 
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)
 
818
            and pos != start):
 
819
            self._on_delete_text(editable, start-1, start)
 
820
            return
 
821
 
 
822
        # we just tried to delete, stop the selection.
 
823
        self._selecting = False
 
824
 
 
825
        field = self._get_field_at_pos(end-1)
 
826
        # Outside a field. Cannot delete.
 
827
        if field is None:
 
828
            self.set_position(end-1)
 
829
            return
 
830
        _start, _end = self._mask_fields[field]
 
831
 
 
832
        # Deleting from outside the bounds of the field.
 
833
        if start < _start or end > _end:
 
834
            _start, _end = start, end
 
835
 
 
836
        # Change the text
 
837
        text = self.get_text()
 
838
 
 
839
        # Shift Left
 
840
        new_text = (text[:start] +
 
841
                    self._shift_text(start, _end, Direction.LEFT,
 
842
                                     end-start) +
 
843
                    text[_end:])
 
844
 
 
845
        # Overwrite 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] +
 
851
#                    text[_end:])
 
852
 
 
853
        new_pos = start
 
854
 
 
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)
 
859
 
 
860
        # Position the cursor on the right place.
 
861
        self.set_position(new_pos)
 
862
        if pos == new_pos:
 
863
            self._handle_position_change()
 
864
 
 
865
    def _after_grab_focus(self, widget):
 
866
        # The text is selectet in grab-focus, so this needs to be done after
 
867
        # that:
 
868
        if self.is_empty():
 
869
            if self._mask:
 
870
                self.set_field(0)
 
871
            else:
 
872
                self.set_position(0)
 
873
 
 
874
    def _on_focus(self, widget, direction):
 
875
        if not self._mask:
 
876
            return
 
877
 
 
878
        if (direction == gtk.DIR_TAB_FORWARD or
 
879
            direction == gtk.DIR_DOWN):
 
880
            inc = 1
 
881
        if (direction == gtk.DIR_TAB_BACKWARD or
 
882
            direction == gtk.DIR_UP):
 
883
            inc = -1
 
884
 
 
885
        field = self._current_field
 
886
 
 
887
        field += inc
 
888
        # Leaving the entry
 
889
        if field == len(self._mask_fields) or field == -1:
 
890
            self.select_region(0, 0)
 
891
            self._current_field = -1
 
892
            return False
 
893
 
 
894
        if field < 0:
 
895
            field = len(self._mask_fields)-1
 
896
 
 
897
        # grab_focus changes the selection, so we need to grab_focus before
 
898
        # making the selection.
 
899
        self.grab_focus()
 
900
        self.set_field(field, select=True)
 
901
 
 
902
        return True
 
903
 
 
904
    def _on_notify_selection_bound(self, widget, pspec):
 
905
        if not self._mask:
 
906
            return
 
907
 
 
908
        if not self.is_focus():
 
909
            return
 
910
 
 
911
        if self._selecting:
 
912
            return
 
913
 
 
914
        self._handle_position_change()
 
915
 
 
916
    def _handle_position_change(self):
 
917
        pos = self.get_position()
 
918
        field = self._get_field_at_pos(pos)
 
919
 
 
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
 
922
        # moving
 
923
        diff = pos - self._pos
 
924
        # but move only one position at a time.
 
925
        if diff:
 
926
            diff /= abs(diff)
 
927
 
 
928
        _field = field
 
929
        if diff:
 
930
            while _field is None and pos >= 0:
 
931
                pos += diff
 
932
                _field = self._get_field_at_pos(pos)
 
933
                self._pos = pos
 
934
            if pos < 0:
 
935
                self._pos = self.get_field_pos(0)
 
936
 
 
937
        if field is None:
 
938
            self.set_position(self._pos)
 
939
        else:
 
940
            self._current_field = field
 
941
            self._pos = pos
 
942
 
 
943
    def _on_changed(self, widget):
 
944
        if self._block_changed:
 
945
            self.stop_emission('changed')
 
946
 
 
947
    def _on_focus_out_event(self, widget, event):
 
948
        if not self._mask:
 
949
            return
 
950
 
 
951
        self._current_field = -1
 
952
 
 
953
    def _on_move_cursor(self, entry, step, count, extend_selection):
 
954
        self._selecting = extend_selection
 
955
 
 
956
    # IconEntry
 
957
 
 
958
    def set_tooltip(self, text):
 
959
        self._icon.set_tooltip(text)
 
960
 
 
961
    def set_pixbuf(self, pixbuf):
 
962
        self._icon.set_pixbuf(pixbuf)
 
963
 
 
964
    def update_background(self, color):
 
965
        self._icon.update_background(color)
 
966
 
 
967
    def get_background(self):
 
968
        return self._icon.get_background()
 
969
 
 
970
    def get_icon_window(self):
 
971
        return self._icon.get_icon_window()
 
972
 
 
973
    # IComboMixin
 
974
 
 
975
    def prefill(self, itemdata, sort=False):
 
976
        """
 
977
        See L{kiwi.interfaces.IEasyCombo.prefill}
 
978
        """
 
979
 
 
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)
 
983
 
 
984
        completion = self._get_completion()
 
985
        model = completion.get_model()
 
986
 
 
987
        if len(itemdata) == 0:
 
988
            model.clear()
 
989
            return
 
990
 
 
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
 
995
        else:
 
996
            mode = self._mode
 
997
 
 
998
        values = set()
 
999
        if mode == ENTRY_MODE_TEXT:
 
1000
            if sort:
 
1001
                itemdata.sort()
 
1002
 
 
1003
            for item in itemdata:
 
1004
                if item in values:
 
1005
                    raise KeyError("Tried to insert duplicate value "
 
1006
                                   "%r into the entry" % item)
 
1007
                else:
 
1008
                    values.add(item)
 
1009
 
 
1010
                model.append((item, None))
 
1011
        elif mode == ENTRY_MODE_DATA:
 
1012
            if sort:
 
1013
                itemdata.sort(lambda x, y: cmp(x[0], y[0]))
 
1014
 
 
1015
            for item in itemdata:
 
1016
                text, data = item
 
1017
                # Add (n) to the end in case of duplicates
 
1018
                count = 1
 
1019
                orig = text
 
1020
                while text in values:
 
1021
                    text = orig + ' (%d)' % count
 
1022
                    count += 1
 
1023
 
 
1024
                values.add(text)
 
1025
                model.append((text, data))
 
1026
        else:
 
1027
            raise TypeError("Incorrect format for itemdata; see "
 
1028
                            "docstring for more information")
 
1029
 
 
1030
    def get_iter_by_data(self, data):
 
1031
        if self._mode != ENTRY_MODE_DATA:
 
1032
            raise TypeError(
 
1033
                "select_item_by_data can only be used in data mode")
 
1034
 
 
1035
        completion = self._get_completion()
 
1036
        model = completion.get_model()
 
1037
 
 
1038
        for row in model:
 
1039
            if row[COL_OBJECT] == data:
 
1040
                return row.iter
 
1041
                break
 
1042
        else:
 
1043
            raise KeyError("No item correspond to data %r in the combo %s"
 
1044
                           % (data, self.name))
 
1045
 
 
1046
    def get_iter_by_label(self, label):
 
1047
        completion = self._get_completion()
 
1048
        model = completion.get_model()
 
1049
        for row in model:
 
1050
            if row[COL_TEXT] == label:
 
1051
                return row.iter
 
1052
        else:
 
1053
            raise KeyError("No item correspond to label %r in the combo %s"
 
1054
                           % (label, self.name))
 
1055
 
 
1056
    def get_selected_by_iter(self, treeiter):
 
1057
        completion = self._get_completion()
 
1058
        model = completion.get_model()
 
1059
        mode = self._mode
 
1060
        text = model[treeiter][COL_TEXT]
 
1061
        if text != self.get_text():
 
1062
            return
 
1063
 
 
1064
        if mode == ENTRY_MODE_TEXT:
 
1065
            return text
 
1066
        elif mode == ENTRY_MODE_DATA:
 
1067
            return model[treeiter][COL_OBJECT]
 
1068
        else:
 
1069
            raise AssertionError
 
1070
 
 
1071
    def get_selected_label(self, treeiter):
 
1072
        completion = self._get_completion()
 
1073
        model = completion.get_model()
 
1074
        return model[treeiter][COL_TEXT]
 
1075
 
 
1076
    def get_selected_data(self, treeiter):
 
1077
        completion = self._get_completion()
 
1078
        model = completion.get_model()
 
1079
        return model[treeiter][COL_OBJECT]
 
1080
 
 
1081
    def get_iter_from_obj(self, obj):
 
1082
        mode = self._mode
 
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)
 
1087
        else:
 
1088
            # XXX: When setting the datatype to non string, automatically go to
 
1089
            #      data mode
 
1090
            raise TypeError("unknown Entry mode. Did you call prefill?")
 
1091
 
 
1092
    def get_mode(self):
 
1093
        return self._mode
 
1094
 
 
1095
type_register(KiwiEntry)
 
1096
 
 
1097
def main(args):
 
1098
    win = gtk.Window()
 
1099
    win.set_title('gtk.Entry subclass')
 
1100
    def cb(window, event):
 
1101
        print 'fields', widget.get_field_text()
 
1102
        gtk.main_quit()
 
1103
    win.connect('delete-event', cb)
 
1104
 
 
1105
    widget = KiwiEntry()
 
1106
    widget.set_mask('000.000.000.000')
 
1107
 
 
1108
    win.add(widget)
 
1109
 
 
1110
    win.show_all()
 
1111
 
 
1112
    widget.select_region(0, 0)
 
1113
    gtk.main()
 
1114
 
 
1115
if __name__ == '__main__':
 
1116
    import sys
 
1117
    sys.exit(main(sys.argv))