~ubuntu-branches/ubuntu/trusty/gramps/trusty-proposed

« back to all changes in this revision

Viewing changes to gramps/gui/widgets/validatedmaskedentry.py

  • Committer: Package Import Robot
  • Author(s): Ross Gammon
  • Date: 2014-02-03 17:28:04 UTC
  • mfrom: (39.1.7 sid)
  • Revision ID: package-import@ubuntu.com-20140203172804-76y7nwxiw92zhlnj
Tags: 4.0.3+dfsg-1
* New upstream release (Closes: #720858)
* To-do notes improved and made persistent (Closes: #680692)
* Applied patch to setup.py to fix resource path problem
* Applied patch to disable the optional HTML View & prevent a crash
* Remove sourceless javascript files (Closes: #736436)
* Gramps uses Bat Mitzva internally (Closes: #502532)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Gramps - a GTK+/GNOME based genealogy program
 
3
#
 
4
# Copyright (C) 2007-2008  Zsolt Foldvari
 
5
# Copyright (C) 2012       Benny Malengier
 
6
#
 
7
# This program is free software; you can redistribute it and/or modify
 
8
# it under the terms of the GNU General Public License as published by
 
9
# the Free Software Foundation; either version 2 of the License, or
 
10
# (at your option) any later version.
 
11
#
 
12
# This program is distributed in the hope that it will be useful, 
 
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
# GNU General Public License for more details.
 
16
#
 
17
# You should have received a copy of the GNU General Public License
 
18
# along with this program; if not, write to the Free Software
 
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
20
#
 
21
 
 
22
# $Id$
 
23
 
 
24
__all__ = ["MaskedEntry", "ValidatableMaskedEntry"]
 
25
 
 
26
#-------------------------------------------------------------------------
 
27
#
 
28
# Standard python modules
 
29
#
 
30
#-------------------------------------------------------------------------
 
31
from gramps.gen.const import GRAMPS_LOCALE as glocale
 
32
_ = glocale.translation.gettext
 
33
import string
 
34
import sys
 
35
 
 
36
import logging
 
37
_LOG = logging.getLogger(".widgets.validatedmaskedentry")
 
38
 
 
39
#-------------------------------------------------------------------------
 
40
#
 
41
# GTK/Gnome modules
 
42
#
 
43
#-------------------------------------------------------------------------
 
44
from gi.repository import GObject
 
45
from gi.repository import Gdk
 
46
from gi.repository import Gtk
 
47
from gi.repository import GdkPixbuf
 
48
from gi.repository import Pango
 
49
 
 
50
#-------------------------------------------------------------------------
 
51
#
 
52
# Gramps modules
 
53
#
 
54
#-------------------------------------------------------------------------
 
55
from gramps.gen.errors import MaskError, ValidationError, WindowActiveError
 
56
from gramps.gen.constfunc import cuni, UNITYPE
 
57
from .undoableentry import UndoableEntry
 
58
 
 
59
#-------------------------------------------------------------------------
 
60
#
 
61
# Constants
 
62
#
 
63
#-------------------------------------------------------------------------
 
64
# STOCK_INFO was added only in Gtk 2.8
 
65
try:
 
66
    INFO_ICON = Gtk.STOCK_INFO
 
67
except AttributeError:
 
68
    INFO_ICON = Gtk.STOCK_DIALOG_INFO
 
69
 
 
70
#============================================================================
 
71
#
 
72
# MaskedEntry and ValidatableMaskedEntry copied and merged from the Kiwi
 
73
# project's ValidatableProxyWidgetMixin, KiwiEntry and ProxyEntry.
 
74
#
 
75
# http://www.async.com.br/projects/kiwi
 
76
#
 
77
#============================================================================
 
78
 
 
79
class FadeOut(GObject.GObject):
 
80
    """I am a helper class to draw the fading effect of the background
 
81
    Call my methods start() and stop() to control the fading.
 
82
    """
 
83
    __gsignals__ = {
 
84
        'done': (GObject.SignalFlags.RUN_FIRST, 
 
85
                 None, 
 
86
                 ()), 
 
87
        'color-changed': (GObject.SignalFlags.RUN_FIRST, 
 
88
                          None, 
 
89
                          (Gdk.Color, )), 
 
90
    }
 
91
    
 
92
    # How long time it'll take before we start (in ms)
 
93
    COMPLAIN_DELAY = 500
 
94
 
 
95
    MERGE_COLORS_DELAY = 100
 
96
 
 
97
    def __init__(self, widget, err_color = "#ffd5d5"):
 
98
        GObject.GObject.__init__(self)
 
99
        self.ERROR_COLOR = err_color
 
100
        self._widget = widget
 
101
        self._start_color = None
 
102
        self._background_timeout_id = -1
 
103
        self._countdown_timeout_id = -1
 
104
        self._done = False
 
105
 
 
106
    def _merge_colors(self, src_color, dst_color, steps=10):
 
107
        """
 
108
        Change the background of widget from src_color to dst_color
 
109
        in the number of steps specified
 
110
        """
 
111
        ##_LOG.debug('_merge_colors: %s -> %s' % (src_color, dst_color))
 
112
 
 
113
        rs, gs, bs = src_color.red, src_color.green, src_color.blue
 
114
        rd, gd, bd = dst_color.red, dst_color.green, dst_color.blue
 
115
        rinc = (rd - rs) / float(steps)
 
116
        ginc = (gd - gs) / float(steps)
 
117
        binc = (bd - bs) / float(steps)
 
118
        for dummy in range(steps):
 
119
            rs += rinc
 
120
            gs += ginc
 
121
            bs += binc
 
122
            col = Gdk.color_parse("#%02X%02X%02X" % (int(rs) >> 8, 
 
123
                                                         int(gs) >> 8, 
 
124
                                                         int(bs) >> 8))
 
125
            self.emit('color-changed', col)
 
126
            yield True
 
127
 
 
128
        self.emit('done')
 
129
        self._background_timeout_id = -1
 
130
        self._done = True
 
131
        yield False
 
132
 
 
133
    def _start_merging(self):
 
134
        # If we changed during the delay
 
135
        if self._background_timeout_id != -1:
 
136
            ##_LOG.debug('_start_merging: Already running')
 
137
            return
 
138
 
 
139
        ##_LOG.debug('_start_merging: Starting')
 
140
        generator = self._merge_colors(self._start_color, 
 
141
                                  Gdk.color_parse(self.ERROR_COLOR))
 
142
        if sys.version_info[0] < 3:
 
143
            func = generator.next
 
144
        else:
 
145
            func = generator.__next__
 
146
        self._background_timeout_id = (
 
147
            GObject.timeout_add(FadeOut.MERGE_COLORS_DELAY, func))
 
148
        self._countdown_timeout_id = -1
 
149
 
 
150
    def start(self, color):
 
151
        """Schedules a start of the countdown.
 
152
        @param color: initial background color
 
153
        @returns: True if we could start, False if was already in progress
 
154
        """
 
155
        if self._background_timeout_id != -1:
 
156
            ##_LOG.debug('start: Background change already running')
 
157
            return False
 
158
        if self._countdown_timeout_id != -1:
 
159
            ##_LOG.debug('start: Countdown already running')
 
160
            return False
 
161
        if self._done:
 
162
            ##_LOG.debug('start: Not running, already set')
 
163
            return False
 
164
 
 
165
        self._start_color = color
 
166
        ##_LOG.debug('start: Scheduling')
 
167
        self._countdown_timeout_id = GObject.timeout_add(
 
168
            FadeOut.COMPLAIN_DELAY, self._start_merging)
 
169
 
 
170
        return True
 
171
 
 
172
    def stop(self):
 
173
        """Stops the fadeout and restores the background color"""
 
174
        ##_LOG.debug('Stopping')
 
175
        if self._background_timeout_id != -1:
 
176
            GObject.source_remove(self._background_timeout_id)
 
177
            self._background_timeout_id = -1
 
178
        if self._countdown_timeout_id != -1:
 
179
            GObject.source_remove(self._countdown_timeout_id)
 
180
            self._countdown_timeout_id = -1
 
181
 
 
182
        self._widget.update_background(self._start_color, unset=True)
 
183
        self._done = False
 
184
 
 
185
(DIRECTION_LEFT, DIRECTION_RIGHT) = (1, -1)
 
186
 
 
187
(INPUT_ASCII_LETTER, 
 
188
 INPUT_ALPHA, 
 
189
 INPUT_ALPHANUMERIC, 
 
190
 INPUT_DIGIT) = list(range(4))
 
191
 
 
192
INPUT_FORMATS = {
 
193
    '0': INPUT_DIGIT, 
 
194
    'L': INPUT_ASCII_LETTER, 
 
195
    'A': INPUT_ALPHANUMERIC, 
 
196
    'a': INPUT_ALPHANUMERIC, 
 
197
    '&': INPUT_ALPHA, 
 
198
    }
 
199
 
 
200
# Todo list: Other useful Masks
 
201
#  9 - Digit, optional
 
202
#  ? - Ascii letter, optional
 
203
#  C - Alpha, optional
 
204
 
 
205
if sys.version_info[0] < 3:
 
206
    INPUT_CHAR_MAP = {
 
207
        INPUT_ASCII_LETTER:     lambda text: text in string.ascii_letters, 
 
208
        INPUT_ALPHA:            unicode.isalpha, 
 
209
        INPUT_ALPHANUMERIC:     unicode.isalnum, 
 
210
        INPUT_DIGIT:            unicode.isdigit, 
 
211
        }
 
212
else:
 
213
    INPUT_CHAR_MAP = {
 
214
        INPUT_ASCII_LETTER:     lambda text: text in string.ascii_letters, 
 
215
        INPUT_ALPHA:            str.isalpha, 
 
216
        INPUT_ALPHANUMERIC:     str.isalnum, 
 
217
        INPUT_DIGIT:            str.isdigit, 
 
218
        }
 
219
 
 
220
(COL_TEXT, 
 
221
 COL_OBJECT) = list(range(2))
 
222
 
 
223
class MaskedEntry(UndoableEntry):
 
224
    """
 
225
    The MaskedEntry is an Entry subclass with additional features.
 
226
 
 
227
    Additional features:
 
228
      - Mask, force the input to meet certain requirements
 
229
      - IconEntry, allows you to have an icon inside the entry
 
230
      - convenience functions for completion
 
231
    
 
232
    Note: Gramps does not use the mask feature at the moment, so that code
 
233
          path is not tested
 
234
    """
 
235
    __gtype_name__ = 'MaskedEntry'
 
236
 
 
237
    def __init__(self):
 
238
        self._block_changed = False
 
239
        UndoableEntry.__init__(self)
 
240
 
 
241
        # connect in UndoableEntry:
 
242
        #self.connect('insert-text', self._on_insert_text)
 
243
        #self.connect('delete-text', self._on_delete_text)
 
244
        self.connect_after('grab-focus', self._after_grab_focus)
 
245
 
 
246
        self.connect('changed', self._on_changed)
 
247
 
 
248
        self.connect('focus', self._on_focus)
 
249
        self.connect('focus-out-event', self._on_focus_out_event)
 
250
        self.connect('move-cursor', self._on_move_cursor)
 
251
        self.connect('button-press-event', self._on_button_press_event)
 
252
        self.connect('notify::cursor-position', 
 
253
                     self._on_notify_cursor_position)
 
254
 
 
255
        self._completion = None
 
256
        self._exact_completion = False
 
257
        self.hasicon = False
 
258
##        self._icon = IconEntry(self)
 
259
 
 
260
        # List of validators
 
261
        #  str -> static characters
 
262
        #  int -> dynamic, according to constants above
 
263
        self._mask_validators = []
 
264
        self._mask = None
 
265
        # Fields defined by mask
 
266
        # each item is a tuble, containing the begining and the end of the
 
267
        # field in the text
 
268
        self._mask_fields = []
 
269
        self._current_field = -1
 
270
        self._pos = 0
 
271
        self._selecting = False
 
272
 
 
273
        self._block_insert = False
 
274
        self._block_delete = False
 
275
        self.in_do_draw = False
 
276
 
 
277
    # Virtual methods, note do_size_alloc needs gtk 2.9 +
 
278
##    def do_size_allocate(self, allocation):
 
279
##        Gtk.Entry.do_size_allocate(self, allocation)
 
280
##
 
281
##        if self.get_realized():
 
282
##            self._icon.resize_windows()
 
283
 
 
284
##    def do_draw(self, cairo_t):
 
285
##        Gtk.Entry.do_draw(self, cairo_t)
 
286
##
 
287
##        if Gtk.cairo_should_draw_window(cairo_t, self.get_window()):
 
288
##            self._icon.draw_pixbuf()
 
289
 
 
290
##    def do_realize(self):
 
291
##        Gtk.Entry.do_realize(self)
 
292
##        self._icon.construct()
 
293
 
 
294
##    def do_unrealize(self):
 
295
##        self._icon.deconstruct()
 
296
##        Gtk.Entry.do_unrealize(self)
 
297
 
 
298
    # Mask & Fields
 
299
 
 
300
    def set_mask(self, mask):
 
301
        """
 
302
        Set the mask of the Entry.
 
303
        
 
304
        Supported format characters are:
 
305
          - '0' digit
 
306
          - 'L' ascii letter (a-z and A-Z)
 
307
          - '&' alphabet, honors the locale
 
308
          - 'a' alphanumeric, honors the locale
 
309
          - 'A' alphanumeric, honors the locale
 
310
 
 
311
        This is similar to MaskedTextBox: 
 
312
        U{http://msdn2.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask(VS.80).aspx}
 
313
 
 
314
        Example mask for a ISO-8601 date
 
315
        >>> entry.set_mask('0000-00-00')
 
316
 
 
317
        @param mask: the mask to set
 
318
        """
 
319
        if not mask:
 
320
            self.modify_font(Pango.FontDescription("sans"))
 
321
            self._mask = mask
 
322
            return
 
323
 
 
324
        # First, reset
 
325
        self._mask_validators = []
 
326
        self._mask_fields = []
 
327
        self._current_field = -1
 
328
 
 
329
        mask = cuni(mask)
 
330
        input_length = len(mask)
 
331
        lenght = 0
 
332
        pos = 0
 
333
        field_begin = 0
 
334
        field_end = 0
 
335
        while True:
 
336
            if pos >= input_length:
 
337
                break
 
338
            if mask[pos] in INPUT_FORMATS:
 
339
                self._mask_validators += [INPUT_FORMATS[mask[pos]]]
 
340
                field_end += 1
 
341
            else:
 
342
                self._mask_validators.append(mask[pos])
 
343
                if field_begin != field_end:
 
344
                    self._mask_fields.append((field_begin, field_end))
 
345
                field_end += 1
 
346
                field_begin = field_end
 
347
            pos += 1
 
348
 
 
349
        self._mask_fields.append((field_begin, field_end))
 
350
        self.modify_font(Pango.FontDescription("monospace"))
 
351
 
 
352
        self._really_delete_text(0, -1)
 
353
        self._insert_mask(0, input_length)
 
354
        self._mask = mask
 
355
 
 
356
    def get_mask(self):
 
357
        """
 
358
        @returns: the mask
 
359
        """
 
360
        return self._mask
 
361
 
 
362
    def get_field_text(self, field):
 
363
        if not self._mask:
 
364
            raise MaskError("a mask must be set before calling get_field_text")
 
365
        #assert self._mask
 
366
        text = self.get_text()
 
367
        start, end = self._mask_fields[field]
 
368
        return text[start: end].strip()
 
369
 
 
370
    def get_fields(self):
 
371
        """
 
372
        Get the fields assosiated with the entry.
 
373
        A field is dynamic content separated by static.
 
374
        For example, the format string 000-000 has two fields
 
375
        separated by a dash.
 
376
        if a field is empty it'll return an empty string
 
377
        otherwise it'll include the content
 
378
 
 
379
        @returns: fields
 
380
        @rtype: list of strings
 
381
        """
 
382
        if not self._mask:
 
383
            raise MaskError("a mask must be set before calling get_fields")
 
384
        #assert self._mask
 
385
 
 
386
        fields = []
 
387
 
 
388
        text = cuni(self.get_text())
 
389
        for start, end in self._mask_fields:
 
390
            fields.append(text[start:end].strip())
 
391
 
 
392
        return fields
 
393
 
 
394
    def get_empty_mask(self, start=None, end=None):
 
395
        """
 
396
        Get the empty mask between start and end
 
397
 
 
398
        @param start:
 
399
        @param end:
 
400
        @returns: mask
 
401
        @rtype: string
 
402
        """
 
403
 
 
404
        if start is None:
 
405
            start = 0
 
406
        if end is None:
 
407
            end = len(self._mask_validators)
 
408
 
 
409
        s = ''
 
410
        for validator in self._mask_validators[start:end]:
 
411
            if isinstance(validator, int):
 
412
                s += ' '
 
413
            elif isinstance(validator, UNITYPE):
 
414
                s += validator
 
415
            else:
 
416
                raise AssertionError
 
417
        return s
 
418
 
 
419
    def get_field_pos(self, field):
 
420
        """
 
421
        Get the position at the specified field.
 
422
        """
 
423
        if field >= len(self._mask_fields):
 
424
            return None
 
425
 
 
426
        start, end = self._mask_fields[field]
 
427
 
 
428
        return start
 
429
 
 
430
    def _get_field_ideal_pos(self, field):
 
431
        start, end = self._mask_fields[field]
 
432
        text = self.get_field_text(field)
 
433
        pos = start+len(text)
 
434
        return pos
 
435
 
 
436
    def get_field(self):
 
437
        if self._current_field >= 0:
 
438
            return self._current_field
 
439
        else:
 
440
            return None
 
441
 
 
442
    def set_field(self, field, select=False):
 
443
        if field >= len(self._mask_fields):
 
444
            return
 
445
 
 
446
        pos = self._get_field_ideal_pos(field)
 
447
        self.set_position(pos)
 
448
 
 
449
        if select:
 
450
            field_text = self.get_field_text(field)
 
451
            start, end = self._mask_fields[field]
 
452
            self.select_region(start, pos)
 
453
 
 
454
        self._current_field = field
 
455
 
 
456
    def get_field_length(self, field):
 
457
        if 0 <= field < len(self._mask_fields):
 
458
            start, end = self._mask_fields[field]
 
459
            return end - start
 
460
 
 
461
    def _shift_text(self, start, end, direction=DIRECTION_LEFT, 
 
462
                    positions=1):
 
463
        """
 
464
        Shift the text, to the right or left, n positions. Note that this
 
465
        does not change the entry text. It returns the shifted text.
 
466
 
 
467
        @param start:
 
468
        @param end:
 
469
        @param direction:   DIRECTION_LEFT or DIRECTION_RIGHT
 
470
        @param positions:   the number of positions to shift.
 
471
 
 
472
        @return:        returns the text between start and end, shifted to
 
473
                        the direction provided.
 
474
        """
 
475
        text = self.get_text()
 
476
        new_text = ''
 
477
        validators = self._mask_validators
 
478
 
 
479
        if direction == DIRECTION_LEFT:
 
480
            i = start
 
481
        else:
 
482
            i = end - 1
 
483
 
 
484
        # When shifting a text, we wanna keep the static chars where they
 
485
        # are, and move the non-static chars to the right position.
 
486
        while start <= i < end:
 
487
            if isinstance(validators[i], int):
 
488
                # Non-static char shoud be here. Get the next one (depending
 
489
                # on the direction, and the number of positions to skip.)
 
490
                #
 
491
                # When shifting left, the next char will be on the right, 
 
492
                # so, it will be appended, to the new text.
 
493
                # Otherwise, when shifting right, the char will be
 
494
                # prepended.
 
495
                next_pos = self._get_next_non_static_char_pos(i, direction, 
 
496
                                                              positions-1)
 
497
 
 
498
                # If its outside the bounds of the region, ignore it.
 
499
                if not start <= next_pos <= end:
 
500
                    next_pos = None
 
501
 
 
502
                if next_pos is not None:
 
503
                    if direction == DIRECTION_LEFT:
 
504
                        new_text = new_text + text[next_pos]
 
505
                    else:
 
506
                        new_text = text[next_pos] + new_text
 
507
                else:
 
508
                    if direction == DIRECTION_LEFT:
 
509
                        new_text = new_text + ' '
 
510
                    else:
 
511
                        new_text = ' ' + new_text
 
512
 
 
513
            else:
 
514
                # Keep the static char where it is.
 
515
                if direction == DIRECTION_LEFT:
 
516
                    new_text = new_text + text[i]
 
517
                else:
 
518
                    new_text = text[i] + new_text
 
519
            i += direction
 
520
 
 
521
        return new_text
 
522
 
 
523
    def _get_next_non_static_char_pos(self, pos, direction=DIRECTION_LEFT, 
 
524
                                      skip=0):
 
525
        """
 
526
        Get next non-static char position, skiping some chars, if necessary.
 
527
        @param skip:        skip first n chars
 
528
        @param direction:   direction of the search.
 
529
        """
 
530
        text = self.get_text()
 
531
        validators = self._mask_validators
 
532
        i = pos+direction+skip
 
533
        while 0 <= i < len(text):
 
534
            if isinstance(validators[i], int):
 
535
                return i
 
536
            i += direction
 
537
 
 
538
        return None
 
539
 
 
540
    def _get_field_at_pos(self, pos, dir=None):
 
541
        """
 
542
        Return the field index at position pos.
 
543
        """
 
544
        for p in self._mask_fields:
 
545
            if p[0] <= pos <= p[1]:
 
546
                return self._mask_fields.index(p)
 
547
 
 
548
        return None
 
549
 
 
550
    def set_exact_completion(self, value):
 
551
        """
 
552
        Enable exact entry completion.
 
553
        Exact means it needs to start with the value typed
 
554
        and the case needs to be correct.
 
555
 
 
556
        @param value: enable exact completion
 
557
        @type value:  boolean
 
558
        """
 
559
 
 
560
        self._exact_completion = value
 
561
        if value:
 
562
            match_func = self._completion_exact_match_func
 
563
        else:
 
564
            match_func = self._completion_normal_match_func
 
565
        completion = self._get_completion()
 
566
        completion.set_match_func(match_func, None)
 
567
 
 
568
    def is_empty(self):
 
569
        text = self.get_text()
 
570
        if self._mask:
 
571
            empty = self.get_empty_mask()
 
572
        else:
 
573
            empty = ''
 
574
 
 
575
        return text == empty
 
576
 
 
577
    # Private
 
578
 
 
579
    def _really_delete_text(self, start, end):
 
580
        # A variant of delete_text() that never is blocked by us
 
581
        self._block_delete = True
 
582
        self.delete_text(start, end)
 
583
        self._block_delete = False
 
584
 
 
585
    def _really_insert_text(self, text, position):
 
586
        # A variant of insert_text() that never is blocked by us
 
587
        self._block_insert = True
 
588
        self.insert_text(text, position)
 
589
        self._block_insert = False
 
590
 
 
591
    def _insert_mask(self, start, end):
 
592
        text = self.get_empty_mask(start, end)
 
593
        self._really_insert_text(text, position=start)
 
594
 
 
595
    def _confirms_to_mask(self, position, text):
 
596
        validators = self._mask_validators
 
597
        if position < 0 or position >= len(validators):
 
598
            return False
 
599
 
 
600
        validator = validators[position]
 
601
        if isinstance(validator, int):
 
602
            if not INPUT_CHAR_MAP[validator](text):
 
603
                return False
 
604
        if isinstance(validator, UNITYPE):
 
605
            if validator == text:
 
606
                return True
 
607
            return False
 
608
 
 
609
        return True
 
610
 
 
611
    def _get_completion(self):
 
612
        # Check so we have completion enabled, not this does not
 
613
        # depend on the property, the user can manually override it, 
 
614
        # as long as there is a completion object set
 
615
        completion = self.get_completion()
 
616
        if completion:
 
617
            return completion
 
618
 
 
619
        completion = Gtk.EntryCompletion()
 
620
        self.set_completion(completion)
 
621
        return completion
 
622
 
 
623
    def get_completion(self):
 
624
        return self._completion
 
625
 
 
626
    def set_completion(self, completion):
 
627
        Gtk.Entry.set_completion(self, completion)
 
628
        # FIXME objects not supported yet, should it be at all?
 
629
        #completion.set_model(Gtk.ListStore(str, object))
 
630
        completion.set_model(Gtk.ListStore(GObject.TYPE_STRING))
 
631
        completion.set_text_column(0)
 
632
        #completion.connect("match-selected", 
 
633
                           #self._on_completion__match_selected)
 
634
 
 
635
        self._completion = Gtk.Entry.get_completion(self)
 
636
        self.set_exact_completion(self._exact_completion)
 
637
        return
 
638
 
 
639
    def set_completion_mode(self, popup=None, inline=None):
 
640
        """
 
641
        Set the way how completion is presented.
 
642
        
 
643
        @param popup: enable completion in popup window
 
644
        @type popup: boolean
 
645
        @param inline: enable inline completion
 
646
        @type inline: boolean
 
647
        """
 
648
        completion = self._get_completion()
 
649
        if popup is not None:
 
650
            completion.set_popup_completion(popup)
 
651
        if inline is not None:
 
652
            completion.set_inline_completion(inline)
 
653
            
 
654
    def _completion_exact_match_func(self, completion, key, iter):
 
655
        model = completion.get_model()
 
656
        if not len(model):
 
657
            return
 
658
 
 
659
        content = model[iter][COL_TEXT]
 
660
        return content.startswith(self.get_text())
 
661
 
 
662
    def _completion_normal_match_func(self, completion, key, iter, data=None):
 
663
        model = completion.get_model()
 
664
        if not len(model):
 
665
            return
 
666
 
 
667
        content = model[iter][COL_TEXT].lower()
 
668
        return key.lower() in content
 
669
 
 
670
    def _on_completion__match_selected(self, completion, model, iter, data=None):
 
671
        if not len(model):
 
672
            return
 
673
 
 
674
        # this updates current_object and triggers content-changed
 
675
        self.set_text(model[iter][COL_TEXT])
 
676
        self.set_position(-1)
 
677
        # FIXME: Enable this at some point
 
678
        #self.activate()
 
679
 
 
680
    def _appers_later(self, char, start):
 
681
        """
 
682
        Check if a char appers later on the mask. If it does, return
 
683
        the field it appers at. returns False otherwise.
 
684
        """
 
685
        validators = self._mask_validators
 
686
        i = start
 
687
        while i < len(validators):
 
688
            if self._mask_validators[i] == char:
 
689
                field = self._get_field_at_pos(i)
 
690
                if field is None:
 
691
                    return False
 
692
 
 
693
                return field
 
694
 
 
695
            i += 1
 
696
 
 
697
        return False
 
698
 
 
699
    def _can_insert_at_pos(self, new, pos):
 
700
        """
 
701
        Check if a chararcter can be inserted at some position
 
702
 
 
703
        @param new: The char that wants to be inserted.
 
704
        @param pos: The position where it wants to be inserted.
 
705
 
 
706
        @return: Returns None if it can be inserted. If it cannot be, 
 
707
                 return the next position where it can be successfuly
 
708
                 inserted.
 
709
        """
 
710
        validators = self._mask_validators
 
711
 
 
712
        # Do not let insert if the field is full
 
713
        field = self._get_field_at_pos(pos)
 
714
        if field is not None:
 
715
            text = self.get_field_text(field)
 
716
            length = self.get_field_length(field)
 
717
            if len(text) == length:
 
718
                Gdk.beep()
 
719
                return pos
 
720
 
 
721
        # If the char confirms to the mask, but is a static char, return the
 
722
        # position after that static char.
 
723
        if (self._confirms_to_mask(pos, new) and
 
724
            not isinstance(validators[pos], int)):
 
725
            return pos+1
 
726
 
 
727
        # If does not confirms to mask:
 
728
        #  - Check if the char the user just tried to enter appers later.
 
729
        #  - If it does, Jump to the start of the field after that
 
730
        if not self._confirms_to_mask(pos, new):
 
731
            field = self._appers_later(new, pos)
 
732
            if field is not False:
 
733
                pos = self.get_field_pos(field+1)
 
734
                if pos is not None:
 
735
                    GObject.idle_add(self.set_position, pos)
 
736
            return pos
 
737
 
 
738
        return None
 
739
 
 
740
#   When inserting new text, supose, the entry, at some time is like this, 
 
741
#   ahd the user presses '0', for instance:
 
742
#   --------------------------------
 
743
#   | ( 1 2 )   3 4 5   - 6 7 8 9  |
 
744
#   --------------------------------
 
745
#              ^ ^     ^
 
746
#              S P     E
 
747
#
 
748
#   S - start of the field (start)
 
749
#   E - end of the field (end)
 
750
#   P - pos - where the new text is being inserted. (pos)
 
751
#
 
752
#   So, the new text will be:
 
753
#
 
754
#     the old text, from 0 until P
 
755
#   + the new text
 
756
#   + the old text, from P until the end of the field, shifted to the
 
757
#     right
 
758
#   + the old text, from the end of the field, to the end of the text.
 
759
#
 
760
#   After inserting, the text will be this:
 
761
#   --------------------------------
 
762
#   | ( 1 2 )   3 0 4 5 - 6 7 8 9  |
 
763
#   --------------------------------
 
764
#              ^   ^   ^
 
765
#              S   P   E
 
766
#
 
767
 
 
768
    def _insert_at_pos(self, text, new, pos):
 
769
        """
 
770
        Inserts the character at the give position in text. Note that the
 
771
        insertion won't be applied to the entry, but to the text provided.
 
772
 
 
773
        @param text:    Text that it will be inserted into.
 
774
        @param new:     New text to insert.
 
775
        @param pos:     Positon to insert at
 
776
 
 
777
        @return:    Returns a tuple, with the position after the insetion
 
778
                    and the new text.
 
779
        """
 
780
        field = self._get_field_at_pos(pos)
 
781
        length = len(new)
 
782
        new_pos = pos
 
783
        start, end = self._mask_fields[field]
 
784
 
 
785
        # Shift Right
 
786
        new_text = (text[:pos] + new +
 
787
                    self._shift_text(pos, end, DIRECTION_RIGHT)[1:] +
 
788
                    text[end:])
 
789
 
 
790
        # Overwrite Right
 
791
#        new_text = (text[:pos] + new +
 
792
#                    text[pos+length:end]+
 
793
#                    text[end:])
 
794
        new_pos = pos+1
 
795
        GObject.idle_add(self.set_position, new_pos)
 
796
 
 
797
        # If the field is full, jump to the next field
 
798
        if len(self.get_field_text(field)) == self.get_field_length(field)-1:
 
799
            GObject.idle_add(self.set_field, field+1, True)
 
800
            self.set_field(field+1)
 
801
 
 
802
        return new_pos, new_text
 
803
 
 
804
    # Callbacks
 
805
    def _on_insert_text(self, editable, new, length, position):
 
806
        if self._block_insert:
 
807
            return
 
808
        if not self._mask:
 
809
            UndoableEntry._on_insert_text(self, editable, new, length, position)
 
810
            return
 
811
        new = cuni(new)
 
812
        pos = self.get_position()
 
813
 
 
814
        self.stop_emission('insert-text')
 
815
 
 
816
        text = self.get_text()
 
817
        # Insert one char at a time
 
818
        for c in new:
 
819
            _pos = self._can_insert_at_pos(c, pos)
 
820
            if _pos is None:
 
821
                pos, text = self._insert_at_pos(text, c, pos)
 
822
            else:
 
823
                pos = _pos
 
824
 
 
825
        # Change the text with the new text.
 
826
        self._block_changed = True
 
827
        self._really_delete_text(0, -1)
 
828
        ### mask not used in Gramps, following should work though
 
829
        ##UndoableEntry._on_delete_text(self, editable, 0, -1)
 
830
        self._block_changed = False
 
831
 
 
832
        self._really_insert_text(text, 0)
 
833
        ### mask not used in Gramps, following should work though
 
834
        ##UndoableEntry._on_insert_text(self, editable, text, len(text),0)
 
835
 
 
836
#   When deleting some text, supose, the entry, at some time is like this:
 
837
#   --------------------------------
 
838
#   | ( 1 2 )   3 4 5 6 - 7 8 9 0  |
 
839
#   --------------------------------
 
840
#              ^ ^ ^   ^
 
841
#              S s e   E
 
842
#
 
843
#   S - start of the field (_start)
 
844
#   E - end of the field (_end)
 
845
#   s - start of the text being deleted (start)
 
846
#   e - end of the text being deleted (end)
 
847
#
 
848
#   end - start -> the number of characters being deleted.
 
849
#
 
850
#   So, the new text will be:
 
851
#
 
852
#     the old text, from 0 until the start of the text being deleted.
 
853
#   + the old text, from the start of where the text is being deleted, until
 
854
#     the end of the field, shifted to the left, end-start positions
 
855
#   + the old text, from the end of the field, to the end of the text.
 
856
#
 
857
#   So, after the text is deleted, the entry will look like this:
 
858
#
 
859
#   --------------------------------
 
860
#   | ( 1 2 )   3 5 6   - 7 8 9 0  |
 
861
#   --------------------------------
 
862
#                ^
 
863
#                P
 
864
#
 
865
#   P = the position of the cursor after the deletion, witch is equal to
 
866
#   start (s at the previous ilustration)
 
867
 
 
868
    def _on_delete_text(self, editable, start, end):
 
869
        if self._block_delete:
 
870
            return
 
871
        if not self._mask:
 
872
            UndoableEntry._on_delete_text(self, editable, start, end)
 
873
            return
 
874
 
 
875
        self.stop_emission('delete-text')
 
876
 
 
877
        pos = self.get_position()
 
878
        # Trying to delete an static char. Delete the char before that
 
879
        if (0 < start < len(self._mask_validators)
 
880
            and not isinstance(self._mask_validators[start], int)
 
881
            and pos != start):
 
882
            self._on_delete_text(editable, start-1, start)
 
883
            ### mask not used in Gramps, following should work though
 
884
            ##UndoableEntry._on_delete_text(self, editable, start-1, start)
 
885
            return
 
886
 
 
887
        field = self._get_field_at_pos(end-1)
 
888
        # Outside a field. Cannot delete.
 
889
        if field is None:
 
890
            self.set_position(end-1)
 
891
            return
 
892
        _start, _end = self._mask_fields[field]
 
893
 
 
894
        # Deleting from outside the bounds of the field.
 
895
        if start < _start or end > _end:
 
896
            _start, _end = start, end
 
897
 
 
898
        # Change the text
 
899
        text = self.get_text()
 
900
 
 
901
        # Shift Left
 
902
        new_text = (text[:start] +
 
903
                    self._shift_text(start, _end, DIRECTION_LEFT, 
 
904
                                     end-start) +
 
905
                    text[_end:])
 
906
 
 
907
        # Overwrite Left
 
908
#        empty_mask = self.get_empty_mask()
 
909
#        new_text = (text[:_start] +
 
910
#                    text[_start:start] +
 
911
#                    empty_mask[start:start+(end-start)] +
 
912
#                    text[start+(end-start):_end] +
 
913
#                    text[_end:])
 
914
 
 
915
        new_pos = start
 
916
 
 
917
        self._block_changed = True
 
918
        self._really_delete_text(0, -1)
 
919
        ### mask not used in Gramps, following should work though
 
920
        ##UndoableEntry._on_delete_text(self, editable, 0, -1)
 
921
        self._block_changed = False
 
922
        self._really_insert_text(new_text, 0)
 
923
        ### mask not used in Gramps, following should work though
 
924
        ##UndoableEntry._on_insert_text(self, editable, text, len(text),0)
 
925
 
 
926
        # Position the cursor on the right place.
 
927
        self.set_position(new_pos)
 
928
 
 
929
        if self.is_empty():
 
930
            pos = self.get_field_pos(0)
 
931
            self.set_position(pos)
 
932
 
 
933
    def _after_grab_focus(self, widget):
 
934
        # The text is selectet in grab-focus, so this needs to be done after
 
935
        # that:
 
936
        if self.is_empty():
 
937
            if self._mask:
 
938
                self.set_field(0)
 
939
            else:
 
940
                self.set_position(0)
 
941
 
 
942
    def _on_focus(self, widget, direction):
 
943
        if not self._mask:
 
944
            return
 
945
 
 
946
        if (direction == Gtk.DIR_TAB_FORWARD or
 
947
            direction == Gtk.DIR_DOWN):
 
948
            inc = 1
 
949
        if (direction == Gtk.DIR_TAB_BACKWARD or
 
950
            direction == Gtk.DIR_UP):
 
951
            inc = -1
 
952
 
 
953
        field = self._current_field
 
954
 
 
955
        field += inc
 
956
        # Leaving the entry
 
957
        if field == len(self._mask_fields) or field == -1:
 
958
            self.select_region(0, 0)
 
959
            self._current_field = -1
 
960
            return False
 
961
 
 
962
        if field < 0:
 
963
            field = len(self._mask_fields)-1
 
964
 
 
965
        # grab_focus changes the selection, so we need to grab_focus before
 
966
        # making the selection.
 
967
        self.grab_focus()
 
968
        self.set_field(field, select=True)
 
969
 
 
970
        return True
 
971
 
 
972
    def _on_notify_cursor_position(self, widget, pspec):
 
973
        if not self._mask:
 
974
            return
 
975
 
 
976
        if not self.is_focus():
 
977
            return
 
978
 
 
979
        if self._selecting:
 
980
            return
 
981
 
 
982
        pos = self.get_position()
 
983
        field = self._get_field_at_pos(pos)
 
984
 
 
985
        if pos == 0:
 
986
            self.set_position(self.get_field_pos(0))
 
987
            return
 
988
 
 
989
        text = self.get_text()
 
990
        field = self._get_field_at_pos(pos)
 
991
 
 
992
        # Humm, the pos is not inside any field. Get the next pos inside
 
993
        # some field, depending on the direction that the cursor is
 
994
        # moving
 
995
        diff = pos - self._pos
 
996
        _field = field
 
997
        while _field is None and (len(text) > pos > 0) and diff:
 
998
            pos += diff
 
999
            _field = self._get_field_at_pos(pos)
 
1000
            self._pos = pos
 
1001
 
 
1002
        if field is None:
 
1003
            self.set_position(self._pos)
 
1004
        else:
 
1005
            self._current_field = field
 
1006
            self._pos = pos
 
1007
 
 
1008
    def _on_changed(self, widget):
 
1009
        if self._block_changed:
 
1010
            self.stop_emission('changed')
 
1011
 
 
1012
    def _on_focus_out_event(self, widget, event):
 
1013
        if not self._mask:
 
1014
            return
 
1015
 
 
1016
        self._current_field = -1
 
1017
 
 
1018
    def _on_move_cursor(self, entry, step, count, extend_selection):
 
1019
        self._selecting = extend_selection
 
1020
 
 
1021
    def _on_button_press_event(self, entry, event ):
 
1022
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1:
 
1023
            self._selecting = True
 
1024
        elif event.type == Gdk.EventType.BUTTON_RELEASE and event.button == 1:
 
1025
            self._selecting = True
 
1026
 
 
1027
    # IconEntry
 
1028
 
 
1029
    def set_tooltip(self, text):
 
1030
        self.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, text)
 
1031
 
 
1032
    def set_pixbuf(self, pixbuf):
 
1033
        self.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, pixbuf)
 
1034
 
 
1035
    def set_stock(self, stock_name):
 
1036
        self.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, stock_name)
 
1037
 
 
1038
    def update_background(self, color, unset=False):
 
1039
        maxvalcol = 65535.
 
1040
        if color:
 
1041
            red = int(color.red/ maxvalcol*255)
 
1042
            green = int(color.green/ maxvalcol*255)
 
1043
            blue = int(color.blue/ maxvalcol*255)
 
1044
            rgba = Gdk.RGBA()
 
1045
            Gdk.RGBA.parse(rgba, 'rgb(%f,%f,%f)'%(red, green, blue))
 
1046
            self.override_background_color(Gtk.StateFlags.NORMAL |
 
1047
                Gtk.StateFlags.ACTIVE | Gtk.StateFlags.SELECTED | 
 
1048
                Gtk.StateFlags.FOCUSED, rgba)
 
1049
            #GTK 3: workaround, background not changing in themes, use symbolic
 
1050
            self.override_symbolic_color('bg_color', rgba)
 
1051
            self.override_symbolic_color('base_color', rgba)
 
1052
            self.override_symbolic_color('theme_bg_color', rgba)
 
1053
            self.override_symbolic_color('theme_base_color', rgba)
 
1054
            ##self.get_window().set_background_rgba(rgba)
 
1055
            pango_context = self.get_layout().get_context()
 
1056
            font_description = pango_context.get_font_description()
 
1057
            if unset:
 
1058
                font_description.set_weight(Pango.Weight.NORMAL)
 
1059
            else:
 
1060
                font_description.set_weight(Pango.Weight.BOLD)
 
1061
            self.override_font(font_description)
 
1062
        else:
 
1063
            self.override_background_color(Gtk.StateFlags.NORMAL |
 
1064
                Gtk.StateFlags.ACTIVE | Gtk.StateFlags.SELECTED | 
 
1065
                Gtk.StateFlags.FOCUSED, None)
 
1066
            # Setting the following to None causes an error (bug #6353).
 
1067
            #self.override_symbolic_color('bg_color', None)
 
1068
            #self.override_symbolic_color('base_color', None)
 
1069
            #self.override_symbolic_color('theme_bg_color', None)
 
1070
            #self.override_symbolic_color('theme_base_color', None)
 
1071
            pango_context = self.get_layout().get_context()
 
1072
            font_description = pango_context.get_font_description()
 
1073
            font_description.set_weight(Pango.Weight.NORMAL)
 
1074
            self.override_font(font_description)
 
1075
 
 
1076
    def get_background(self):
 
1077
        backcol = self.get_style_context().get_background_color(Gtk.StateType.NORMAL)
 
1078
        bcol= Gdk.Color.parse('#fff')[1]
 
1079
        bcol.red = int(backcol.red * 65535)
 
1080
        bcol.green = int(backcol.green * 65535)
 
1081
        bcol.blue = int(backcol.blue * 65535)
 
1082
        return bcol
 
1083
 
 
1084
    # Gtk.EntryCompletion convenience function
 
1085
    
 
1086
    def prefill(self, itemdata, sort=False):
 
1087
        if not isinstance(itemdata, (list, tuple)):
 
1088
            raise TypeError("'data' parameter must be a list or tuple of item "
 
1089
                            "descriptions, found %s") % type(itemdata)
 
1090
 
 
1091
        completion = self._get_completion()
 
1092
        model = completion.get_model()
 
1093
 
 
1094
        if len(itemdata) == 0:
 
1095
            model.clear()
 
1096
            return
 
1097
 
 
1098
        values = {}
 
1099
        if sort:
 
1100
            itemdata.sort()
 
1101
 
 
1102
        for item in itemdata:
 
1103
            if item in values:
 
1104
                raise KeyError("Tried to insert duplicate value "
 
1105
                                   "%r into the entry" % item)
 
1106
            else:
 
1107
                values[item] = None
 
1108
 
 
1109
            model.append((item, ))
 
1110
 
 
1111
#number = (int, float, long)
 
1112
 
 
1113
VALIDATION_ICON_WIDTH = 16
 
1114
MANDATORY_ICON = INFO_ICON
 
1115
ERROR_ICON = Gtk.STOCK_STOP
 
1116
 
 
1117
class ValidatableMaskedEntry(MaskedEntry):
 
1118
    """It extends the MaskedEntry with validation feature.
 
1119
 
 
1120
    Merged from Kiwi's ValidatableProxyWidgetMixin and ProxyEntry.
 
1121
    To provide custom validation connect to the 'validate' signal
 
1122
    of the instance.
 
1123
    """
 
1124
 
 
1125
    __gtype_name__ = 'ValidatableMaskedEntry'
 
1126
 
 
1127
    __gsignals__ = {
 
1128
        'content-changed': (GObject.SignalFlags.RUN_FIRST, 
 
1129
                            None, 
 
1130
                            ()), 
 
1131
        'validation-changed': (GObject.SignalFlags.RUN_FIRST, 
 
1132
                               None, 
 
1133
                               (GObject.TYPE_BOOLEAN, )), 
 
1134
        'validate': (GObject.SignalFlags.RUN_LAST, 
 
1135
                     GObject.TYPE_PYOBJECT, 
 
1136
                     (GObject.TYPE_PYOBJECT, )), 
 
1137
        'changed': 'override', 
 
1138
    }
 
1139
 
 
1140
    __gproperties__ = {
 
1141
        'data-type': (GObject.TYPE_PYOBJECT, 
 
1142
                       'Data Type of the widget', 
 
1143
                       'Type object', 
 
1144
                       GObject.PARAM_READWRITE), 
 
1145
        'mandatory': (GObject.TYPE_BOOLEAN, 
 
1146
                      'Mandatory', 
 
1147
                      'Mandatory', 
 
1148
                      False, 
 
1149
                      GObject.PARAM_READWRITE), 
 
1150
    }
 
1151
                            
 
1152
    # FIXME put the data type support back
 
1153
    #allowed_data_types = (basestring, datetime.date, datetime.time, 
 
1154
                          #datetime.datetime, object) + number
 
1155
 
 
1156
    def __init__(self, data_type=None, err_color = "#ffd5d5", error_icon=ERROR_ICON):
 
1157
        self.data_type = None
 
1158
        self.mandatory = False
 
1159
        self.error_icon = error_icon
 
1160
        self._block_changed = False
 
1161
 
 
1162
        MaskedEntry.__init__(self)
 
1163
        
 
1164
        self._valid = True
 
1165
        self._def_error_msg = None
 
1166
        self._fade = FadeOut(self, err_color)
 
1167
        self._fade.connect('color-changed', self._on_fadeout__color_changed)
 
1168
        
 
1169
        # FIXME put data type support back
 
1170
        #self.set_property('data-type', data_type)
 
1171
 
 
1172
    # Virtual methods
 
1173
    def do_changed(self):
 
1174
        block_changed = getattr(self, '_block_changed', True)
 
1175
        if block_changed:
 
1176
            self.emit_stop_by_name('changed')
 
1177
            return
 
1178
        self.emit('content-changed')
 
1179
        self.validate()
 
1180
 
 
1181
    def do_get_property(self, prop):
 
1182
        """Return the gproperty's value."""
 
1183
        
 
1184
        if prop.name == 'data-type':
 
1185
            return self.data_type
 
1186
        elif prop.name == 'mandatory':
 
1187
            return self.mandatory
 
1188
        else:
 
1189
            raise AttributeError('unknown property %s' % prop.name)
 
1190
 
 
1191
    def do_set_property(self, prop, value):
 
1192
        """Set the property of writable properties."""
 
1193
        
 
1194
        if prop.name == 'data-type':
 
1195
            if value is None:
 
1196
                self.data_type = value
 
1197
                return
 
1198
        
 
1199
            # FIXME put the data type support back
 
1200
            #if not issubclass(value, self.allowed_data_types):
 
1201
                #raise TypeError(
 
1202
                    #"%s only accept %s types, not %r"
 
1203
                    #% (self, 
 
1204
                       #' or '.join([t.__name__ for t in self.allowed_data_types]), 
 
1205
                       #value))
 
1206
            self.data_type = value
 
1207
        elif prop.name == 'mandatory':
 
1208
            self.mandatory = value
 
1209
        else:
 
1210
            raise AttributeError('unknown or read only property %s' % prop.name)
 
1211
 
 
1212
    # Public API
 
1213
 
 
1214
    def set_default_error_msg(self, text):
 
1215
        """
 
1216
        Set default message for validation error.
 
1217
        
 
1218
        Default error message for an instance is useful when completion is
 
1219
        used, because this case custom validation is not called.
 
1220
                
 
1221
        @param text: can contain one and only one '%s', where the actual value
 
1222
        of the Entry will be inserted.
 
1223
        @type text: str
 
1224
        """
 
1225
        if not isinstance(text, str):
 
1226
            raise TypeError("text must be a string")
 
1227
            
 
1228
        self._def_error_msg = text
 
1229
        
 
1230
    def is_valid(self):
 
1231
        """
 
1232
        @returns: True if the widget is in validated state
 
1233
        """
 
1234
        return self._valid
 
1235
 
 
1236
    def validate(self, force=False):
 
1237
        """Checks if the data is valid.
 
1238
        Validates data-type and custom validation.
 
1239
 
 
1240
        @param force: if True, force validation
 
1241
        @returns:     validated data or ValueUnset if it failed
 
1242
        """
 
1243
 
 
1244
        # If we're not visible or sensitive return a blank value, except
 
1245
        # when forcing the validation
 
1246
        if not force and (not self.get_property('visible') or
 
1247
                          not self.get_property('sensitive')):
 
1248
            return None
 
1249
 
 
1250
        try:
 
1251
            text = self.get_text()
 
1252
            ##_LOG.debug('Read %r for %s' %  (data, self.model_attribute))
 
1253
 
 
1254
            # check if we should draw the mandatory icon
 
1255
            # this need to be done before any data conversion because we
 
1256
            # we don't want to end drawing two icons
 
1257
            if self.mandatory and self.is_empty():
 
1258
                self.set_blank()
 
1259
                return None
 
1260
            else:
 
1261
                if self._completion:
 
1262
                    for row in self.get_completion().get_model():
 
1263
                        if row[COL_TEXT] == text:
 
1264
                            break
 
1265
                    else:
 
1266
                        if text:
 
1267
                            raise ValidationError()
 
1268
                else:
 
1269
                    if not self.is_empty():
 
1270
                        # this signal calls the custom validation method
 
1271
                        # of the instance and gets the exception (if any).
 
1272
                        error = self.emit("validate", text)
 
1273
                        if error:
 
1274
                            raise error
 
1275
 
 
1276
            self.set_valid()
 
1277
            return text
 
1278
        except ValidationError as e:
 
1279
            self.set_invalid(str(e))
 
1280
            return None
 
1281
 
 
1282
    def set_valid(self):
 
1283
        """Change the validation state to valid, which will remove icons and
 
1284
        reset the background color
 
1285
        """
 
1286
        ##_LOG.debug('Setting state for %s to VALID' % self.model_attribute)
 
1287
        self._set_valid_state(True)
 
1288
 
 
1289
        self._fade.stop()
 
1290
        self.set_pixbuf(None)
 
1291
 
 
1292
    def set_invalid(self, text=None, fade=True):
 
1293
        """Change the validation state to invalid.
 
1294
        @param text: text of tooltip of None
 
1295
        @param fade: if we should fade the background
 
1296
        """
 
1297
        ##_LOG.debug('Setting state for %s to INVALID' % self.model_attribute)
 
1298
 
 
1299
        self._set_valid_state(False)
 
1300
 
 
1301
        generic_text = _("'%s' is not a valid value "
 
1302
                         "for this field") % self.get_text()
 
1303
        
 
1304
        # If there is no error text, let's try with the default or
 
1305
        # fall back to a generic one
 
1306
        if not text:
 
1307
            text = self._def_error_msg
 
1308
        if not text:
 
1309
            text = generic_text
 
1310
            
 
1311
        try:
 
1312
            text.index('%s')
 
1313
            text = text % self.get_text()
 
1314
        except TypeError:
 
1315
            # if text contains '%s' more than once
 
1316
            _LOG.error('There must be only one instance of "%s"'
 
1317
                      ' in validation error message')
 
1318
            # fall back to a generic one so the error icon still have a tooltip
 
1319
            text = generic_text
 
1320
        except ValueError:
 
1321
            # if text does not contain '%s'
 
1322
            pass
 
1323
 
 
1324
        self.set_tooltip(text)
 
1325
 
 
1326
        if not fade:
 
1327
            if self.error_icon:
 
1328
                self.set_stock(self.error_icon)
 
1329
            self.update_background(Gdk.color_parse(self._fade.ERROR_COLOR))
 
1330
            return
 
1331
 
 
1332
        # When the fading animation is finished, set the error icon
 
1333
        # We don't need to check if the state is valid, since stop()
 
1334
        # (which removes this timeout) is called as soon as the user
 
1335
        # types valid data.
 
1336
        def done(fadeout, c):
 
1337
            if self.error_icon:
 
1338
                self.set_stock(self.error_icon)
 
1339
            self.queue_draw()
 
1340
            fadeout.disconnect(c.signal_id)
 
1341
 
 
1342
        class SignalContainer(object):
 
1343
            pass
 
1344
        c = SignalContainer()
 
1345
        c.signal_id = self._fade.connect('done', done, c)
 
1346
 
 
1347
        if self._fade.start(self.get_background()):
 
1348
            self.set_pixbuf(None)
 
1349
 
 
1350
    def set_blank(self):
 
1351
        """Change the validation state to blank state, this only applies
 
1352
        for mandatory widgets, draw an icon and set a tooltip"""
 
1353
 
 
1354
        ##_LOG.debug('Setting state for %s to BLANK' % self.model_attribute)
 
1355
 
 
1356
        if self.mandatory:
 
1357
            self.set_stock(MANDATORY_ICON)
 
1358
            self.queue_draw()
 
1359
            self.set_tooltip(_('This field is mandatory'))
 
1360
            self._fade.stop()
 
1361
            valid = False
 
1362
        else:
 
1363
            valid = True
 
1364
 
 
1365
        self._set_valid_state(valid)
 
1366
 
 
1367
    def set_text(self, text):
 
1368
        """
 
1369
        Set the text of the entry
 
1370
 
 
1371
        @param text:
 
1372
        """
 
1373
 
 
1374
        # If content isn't empty set_text emitts changed twice.
 
1375
        # Protect content-changed from being updated and issue
 
1376
        # a manual emission afterwards
 
1377
        self._block_changed = True
 
1378
        MaskedEntry.set_text(self, text)
 
1379
        self._block_changed = False
 
1380
        self.emit('content-changed')
 
1381
 
 
1382
        self.set_position(-1)
 
1383
 
 
1384
    # Private
 
1385
 
 
1386
    def _set_valid_state(self, state):
 
1387
        """Updates the validation state and emits a signal if it changed"""
 
1388
 
 
1389
        if self._valid == state:
 
1390
            return
 
1391
 
 
1392
        self.emit('validation-changed', state)
 
1393
        self._valid = state
 
1394
 
 
1395
    # Callbacks
 
1396
 
 
1397
    def _on_fadeout__color_changed(self, fadeout, color):
 
1398
        self.update_background(color)
 
1399
 
 
1400
 
 
1401
def main(args):
 
1402
    from gramps.gen.datehandler import parser
 
1403
    
 
1404
    def on_validate(widget, text):
 
1405
        myDate = parser.parse(text)
 
1406
        if not myDate.is_regular():
 
1407
            # used on AgeOnDateGramplet
 
1408
            return ValidationError(_("'%s' is not a valid date value"))
 
1409
        
 
1410
    win = Gtk.Window()
 
1411
    win.set_title('ValidatableMaskedEntry test window')
 
1412
    win.set_position(Gtk.WindowPosition.CENTER)
 
1413
    def cb(window, event):
 
1414
        Gtk.main_quit()
 
1415
    win.connect('delete-event', cb)
 
1416
 
 
1417
    vbox = Gtk.VBox()
 
1418
    win.add(vbox)
 
1419
    
 
1420
    label = Gtk.Label(label='Pre-filled entry validated against the given list:')
 
1421
    vbox.pack_start(label, True, True, 0)
 
1422
    
 
1423
    widget1 = ValidatableMaskedEntry(str)
 
1424
    widget1.set_completion_mode(inline=True, popup=False)
 
1425
    widget1.set_default_error_msg("'%s' is not a default Event")
 
1426
    #widget1.set_default_error_msg(widget1)
 
1427
    widget1.prefill(('Birth', 'Death', 'Conseption'))
 
1428
    #widget1.set_exact_completion(True)
 
1429
    vbox.pack_start(widget1, True, False, 0)
 
1430
    
 
1431
    label = Gtk.Label(label='Mandatory masked entry validated against user function:')
 
1432
    vbox.pack_start(label, True, True, 0)
 
1433
    
 
1434
    #widget2 = ValidatableMaskedEntry(str, "#e0e0e0", error_icon=None)
 
1435
    widget2 = ValidatableMaskedEntry()
 
1436
    widget2.set_mask('00/00/0000')
 
1437
    widget2.connect('validate', on_validate)
 
1438
    widget2.mandatory = True
 
1439
    vbox.pack_start(widget2, True, False, 0)
 
1440
    
 
1441
    win.show_all()
 
1442
    Gtk.main()
 
1443
 
 
1444
if __name__ == '__main__':
 
1445
    import sys
 
1446
    # fall back to root logger for testing
 
1447
    _LOG = logging
 
1448
    sys.exit(main(sys.argv))