2
# Gramps - a GTK+/GNOME based genealogy program
4
# Copyright (C) 2007-2008 Zsolt Foldvari
5
# Copyright (C) 2012 Benny Malengier
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.
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.
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
24
__all__ = ["MaskedEntry", "ValidatableMaskedEntry"]
26
#-------------------------------------------------------------------------
28
# Standard python modules
30
#-------------------------------------------------------------------------
31
from gramps.gen.const import GRAMPS_LOCALE as glocale
32
_ = glocale.translation.gettext
37
_LOG = logging.getLogger(".widgets.validatedmaskedentry")
39
#-------------------------------------------------------------------------
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
50
#-------------------------------------------------------------------------
54
#-------------------------------------------------------------------------
55
from gramps.gen.errors import MaskError, ValidationError, WindowActiveError
56
from gramps.gen.constfunc import cuni, UNITYPE
57
from .undoableentry import UndoableEntry
59
#-------------------------------------------------------------------------
63
#-------------------------------------------------------------------------
64
# STOCK_INFO was added only in Gtk 2.8
66
INFO_ICON = Gtk.STOCK_INFO
67
except AttributeError:
68
INFO_ICON = Gtk.STOCK_DIALOG_INFO
70
#============================================================================
72
# MaskedEntry and ValidatableMaskedEntry copied and merged from the Kiwi
73
# project's ValidatableProxyWidgetMixin, KiwiEntry and ProxyEntry.
75
# http://www.async.com.br/projects/kiwi
77
#============================================================================
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.
84
'done': (GObject.SignalFlags.RUN_FIRST,
87
'color-changed': (GObject.SignalFlags.RUN_FIRST,
92
# How long time it'll take before we start (in ms)
95
MERGE_COLORS_DELAY = 100
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
106
def _merge_colors(self, src_color, dst_color, steps=10):
108
Change the background of widget from src_color to dst_color
109
in the number of steps specified
111
##_LOG.debug('_merge_colors: %s -> %s' % (src_color, dst_color))
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):
122
col = Gdk.color_parse("#%02X%02X%02X" % (int(rs) >> 8,
125
self.emit('color-changed', col)
129
self._background_timeout_id = -1
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')
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
145
func = generator.__next__
146
self._background_timeout_id = (
147
GObject.timeout_add(FadeOut.MERGE_COLORS_DELAY, func))
148
self._countdown_timeout_id = -1
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
155
if self._background_timeout_id != -1:
156
##_LOG.debug('start: Background change already running')
158
if self._countdown_timeout_id != -1:
159
##_LOG.debug('start: Countdown already running')
162
##_LOG.debug('start: Not running, already set')
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)
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
182
self._widget.update_background(self._start_color, unset=True)
185
(DIRECTION_LEFT, DIRECTION_RIGHT) = (1, -1)
190
INPUT_DIGIT) = list(range(4))
194
'L': INPUT_ASCII_LETTER,
195
'A': INPUT_ALPHANUMERIC,
196
'a': INPUT_ALPHANUMERIC,
200
# Todo list: Other useful Masks
201
# 9 - Digit, optional
202
# ? - Ascii letter, optional
203
# C - Alpha, optional
205
if sys.version_info[0] < 3:
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,
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,
221
COL_OBJECT) = list(range(2))
223
class MaskedEntry(UndoableEntry):
225
The MaskedEntry is an Entry subclass with 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
232
Note: Gramps does not use the mask feature at the moment, so that code
235
__gtype_name__ = 'MaskedEntry'
238
self._block_changed = False
239
UndoableEntry.__init__(self)
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)
246
self.connect('changed', self._on_changed)
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)
255
self._completion = None
256
self._exact_completion = False
258
## self._icon = IconEntry(self)
261
# str -> static characters
262
# int -> dynamic, according to constants above
263
self._mask_validators = []
265
# Fields defined by mask
266
# each item is a tuble, containing the begining and the end of the
268
self._mask_fields = []
269
self._current_field = -1
271
self._selecting = False
273
self._block_insert = False
274
self._block_delete = False
275
self.in_do_draw = False
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)
281
## if self.get_realized():
282
## self._icon.resize_windows()
284
## def do_draw(self, cairo_t):
285
## Gtk.Entry.do_draw(self, cairo_t)
287
## if Gtk.cairo_should_draw_window(cairo_t, self.get_window()):
288
## self._icon.draw_pixbuf()
290
## def do_realize(self):
291
## Gtk.Entry.do_realize(self)
292
## self._icon.construct()
294
## def do_unrealize(self):
295
## self._icon.deconstruct()
296
## Gtk.Entry.do_unrealize(self)
300
def set_mask(self, mask):
302
Set the mask of the Entry.
304
Supported format characters are:
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
311
This is similar to MaskedTextBox:
312
U{http://msdn2.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask(VS.80).aspx}
314
Example mask for a ISO-8601 date
315
>>> entry.set_mask('0000-00-00')
317
@param mask: the mask to set
320
self.modify_font(Pango.FontDescription("sans"))
325
self._mask_validators = []
326
self._mask_fields = []
327
self._current_field = -1
330
input_length = len(mask)
336
if pos >= input_length:
338
if mask[pos] in INPUT_FORMATS:
339
self._mask_validators += [INPUT_FORMATS[mask[pos]]]
342
self._mask_validators.append(mask[pos])
343
if field_begin != field_end:
344
self._mask_fields.append((field_begin, field_end))
346
field_begin = field_end
349
self._mask_fields.append((field_begin, field_end))
350
self.modify_font(Pango.FontDescription("monospace"))
352
self._really_delete_text(0, -1)
353
self._insert_mask(0, input_length)
362
def get_field_text(self, field):
364
raise MaskError("a mask must be set before calling get_field_text")
366
text = self.get_text()
367
start, end = self._mask_fields[field]
368
return text[start: end].strip()
370
def get_fields(self):
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
376
if a field is empty it'll return an empty string
377
otherwise it'll include the content
380
@rtype: list of strings
383
raise MaskError("a mask must be set before calling get_fields")
388
text = cuni(self.get_text())
389
for start, end in self._mask_fields:
390
fields.append(text[start:end].strip())
394
def get_empty_mask(self, start=None, end=None):
396
Get the empty mask between start and end
407
end = len(self._mask_validators)
410
for validator in self._mask_validators[start:end]:
411
if isinstance(validator, int):
413
elif isinstance(validator, UNITYPE):
419
def get_field_pos(self, field):
421
Get the position at the specified field.
423
if field >= len(self._mask_fields):
426
start, end = self._mask_fields[field]
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)
437
if self._current_field >= 0:
438
return self._current_field
442
def set_field(self, field, select=False):
443
if field >= len(self._mask_fields):
446
pos = self._get_field_ideal_pos(field)
447
self.set_position(pos)
450
field_text = self.get_field_text(field)
451
start, end = self._mask_fields[field]
452
self.select_region(start, pos)
454
self._current_field = field
456
def get_field_length(self, field):
457
if 0 <= field < len(self._mask_fields):
458
start, end = self._mask_fields[field]
461
def _shift_text(self, start, end, direction=DIRECTION_LEFT,
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.
469
@param direction: DIRECTION_LEFT or DIRECTION_RIGHT
470
@param positions: the number of positions to shift.
472
@return: returns the text between start and end, shifted to
473
the direction provided.
475
text = self.get_text()
477
validators = self._mask_validators
479
if direction == DIRECTION_LEFT:
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.)
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
495
next_pos = self._get_next_non_static_char_pos(i, direction,
498
# If its outside the bounds of the region, ignore it.
499
if not start <= next_pos <= end:
502
if next_pos is not None:
503
if direction == DIRECTION_LEFT:
504
new_text = new_text + text[next_pos]
506
new_text = text[next_pos] + new_text
508
if direction == DIRECTION_LEFT:
509
new_text = new_text + ' '
511
new_text = ' ' + new_text
514
# Keep the static char where it is.
515
if direction == DIRECTION_LEFT:
516
new_text = new_text + text[i]
518
new_text = text[i] + new_text
523
def _get_next_non_static_char_pos(self, pos, direction=DIRECTION_LEFT,
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.
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):
540
def _get_field_at_pos(self, pos, dir=None):
542
Return the field index at position pos.
544
for p in self._mask_fields:
545
if p[0] <= pos <= p[1]:
546
return self._mask_fields.index(p)
550
def set_exact_completion(self, value):
552
Enable exact entry completion.
553
Exact means it needs to start with the value typed
554
and the case needs to be correct.
556
@param value: enable exact completion
560
self._exact_completion = value
562
match_func = self._completion_exact_match_func
564
match_func = self._completion_normal_match_func
565
completion = self._get_completion()
566
completion.set_match_func(match_func, None)
569
text = self.get_text()
571
empty = self.get_empty_mask()
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
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
591
def _insert_mask(self, start, end):
592
text = self.get_empty_mask(start, end)
593
self._really_insert_text(text, position=start)
595
def _confirms_to_mask(self, position, text):
596
validators = self._mask_validators
597
if position < 0 or position >= len(validators):
600
validator = validators[position]
601
if isinstance(validator, int):
602
if not INPUT_CHAR_MAP[validator](text):
604
if isinstance(validator, UNITYPE):
605
if validator == text:
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()
619
completion = Gtk.EntryCompletion()
620
self.set_completion(completion)
623
def get_completion(self):
624
return self._completion
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)
635
self._completion = Gtk.Entry.get_completion(self)
636
self.set_exact_completion(self._exact_completion)
639
def set_completion_mode(self, popup=None, inline=None):
641
Set the way how completion is presented.
643
@param popup: enable completion in popup window
645
@param inline: enable inline completion
646
@type inline: boolean
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)
654
def _completion_exact_match_func(self, completion, key, iter):
655
model = completion.get_model()
659
content = model[iter][COL_TEXT]
660
return content.startswith(self.get_text())
662
def _completion_normal_match_func(self, completion, key, iter, data=None):
663
model = completion.get_model()
667
content = model[iter][COL_TEXT].lower()
668
return key.lower() in content
670
def _on_completion__match_selected(self, completion, model, iter, data=None):
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
680
def _appers_later(self, char, start):
682
Check if a char appers later on the mask. If it does, return
683
the field it appers at. returns False otherwise.
685
validators = self._mask_validators
687
while i < len(validators):
688
if self._mask_validators[i] == char:
689
field = self._get_field_at_pos(i)
699
def _can_insert_at_pos(self, new, pos):
701
Check if a chararcter can be inserted at some position
703
@param new: The char that wants to be inserted.
704
@param pos: The position where it wants to be inserted.
706
@return: Returns None if it can be inserted. If it cannot be,
707
return the next position where it can be successfuly
710
validators = self._mask_validators
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:
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)):
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)
735
GObject.idle_add(self.set_position, pos)
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
# --------------------------------
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)
752
# So, the new text will be:
754
# the old text, from 0 until P
756
# + the old text, from P until the end of the field, shifted to the
758
# + the old text, from the end of the field, to the end of the text.
760
# After inserting, the text will be this:
761
# --------------------------------
762
# | ( 1 2 ) 3 0 4 5 - 6 7 8 9 |
763
# --------------------------------
768
def _insert_at_pos(self, text, new, pos):
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.
773
@param text: Text that it will be inserted into.
774
@param new: New text to insert.
775
@param pos: Positon to insert at
777
@return: Returns a tuple, with the position after the insetion
780
field = self._get_field_at_pos(pos)
783
start, end = self._mask_fields[field]
786
new_text = (text[:pos] + new +
787
self._shift_text(pos, end, DIRECTION_RIGHT)[1:] +
791
# new_text = (text[:pos] + new +
792
# text[pos+length:end]+
795
GObject.idle_add(self.set_position, new_pos)
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)
802
return new_pos, new_text
805
def _on_insert_text(self, editable, new, length, position):
806
if self._block_insert:
809
UndoableEntry._on_insert_text(self, editable, new, length, position)
812
pos = self.get_position()
814
self.stop_emission('insert-text')
816
text = self.get_text()
817
# Insert one char at a time
819
_pos = self._can_insert_at_pos(c, pos)
821
pos, text = self._insert_at_pos(text, c, pos)
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
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)
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
# --------------------------------
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)
848
# end - start -> the number of characters being deleted.
850
# So, the new text will be:
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.
857
# So, after the text is deleted, the entry will look like this:
859
# --------------------------------
860
# | ( 1 2 ) 3 5 6 - 7 8 9 0 |
861
# --------------------------------
865
# P = the position of the cursor after the deletion, witch is equal to
866
# start (s at the previous ilustration)
868
def _on_delete_text(self, editable, start, end):
869
if self._block_delete:
872
UndoableEntry._on_delete_text(self, editable, start, end)
875
self.stop_emission('delete-text')
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)
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)
887
field = self._get_field_at_pos(end-1)
888
# Outside a field. Cannot delete.
890
self.set_position(end-1)
892
_start, _end = self._mask_fields[field]
894
# Deleting from outside the bounds of the field.
895
if start < _start or end > _end:
896
_start, _end = start, end
899
text = self.get_text()
902
new_text = (text[:start] +
903
self._shift_text(start, _end, DIRECTION_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] +
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)
926
# Position the cursor on the right place.
927
self.set_position(new_pos)
930
pos = self.get_field_pos(0)
931
self.set_position(pos)
933
def _after_grab_focus(self, widget):
934
# The text is selectet in grab-focus, so this needs to be done after
942
def _on_focus(self, widget, direction):
946
if (direction == Gtk.DIR_TAB_FORWARD or
947
direction == Gtk.DIR_DOWN):
949
if (direction == Gtk.DIR_TAB_BACKWARD or
950
direction == Gtk.DIR_UP):
953
field = self._current_field
957
if field == len(self._mask_fields) or field == -1:
958
self.select_region(0, 0)
959
self._current_field = -1
963
field = len(self._mask_fields)-1
965
# grab_focus changes the selection, so we need to grab_focus before
966
# making the selection.
968
self.set_field(field, select=True)
972
def _on_notify_cursor_position(self, widget, pspec):
976
if not self.is_focus():
982
pos = self.get_position()
983
field = self._get_field_at_pos(pos)
986
self.set_position(self.get_field_pos(0))
989
text = self.get_text()
990
field = self._get_field_at_pos(pos)
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
995
diff = pos - self._pos
997
while _field is None and (len(text) > pos > 0) and diff:
999
_field = self._get_field_at_pos(pos)
1003
self.set_position(self._pos)
1005
self._current_field = field
1008
def _on_changed(self, widget):
1009
if self._block_changed:
1010
self.stop_emission('changed')
1012
def _on_focus_out_event(self, widget, event):
1016
self._current_field = -1
1018
def _on_move_cursor(self, entry, step, count, extend_selection):
1019
self._selecting = extend_selection
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
1029
def set_tooltip(self, text):
1030
self.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, text)
1032
def set_pixbuf(self, pixbuf):
1033
self.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, pixbuf)
1035
def set_stock(self, stock_name):
1036
self.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, stock_name)
1038
def update_background(self, color, unset=False):
1041
red = int(color.red/ maxvalcol*255)
1042
green = int(color.green/ maxvalcol*255)
1043
blue = int(color.blue/ maxvalcol*255)
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()
1058
font_description.set_weight(Pango.Weight.NORMAL)
1060
font_description.set_weight(Pango.Weight.BOLD)
1061
self.override_font(font_description)
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)
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)
1084
# Gtk.EntryCompletion convenience function
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)
1091
completion = self._get_completion()
1092
model = completion.get_model()
1094
if len(itemdata) == 0:
1102
for item in itemdata:
1104
raise KeyError("Tried to insert duplicate value "
1105
"%r into the entry" % item)
1109
model.append((item, ))
1111
#number = (int, float, long)
1113
VALIDATION_ICON_WIDTH = 16
1114
MANDATORY_ICON = INFO_ICON
1115
ERROR_ICON = Gtk.STOCK_STOP
1117
class ValidatableMaskedEntry(MaskedEntry):
1118
"""It extends the MaskedEntry with validation feature.
1120
Merged from Kiwi's ValidatableProxyWidgetMixin and ProxyEntry.
1121
To provide custom validation connect to the 'validate' signal
1125
__gtype_name__ = 'ValidatableMaskedEntry'
1128
'content-changed': (GObject.SignalFlags.RUN_FIRST,
1131
'validation-changed': (GObject.SignalFlags.RUN_FIRST,
1133
(GObject.TYPE_BOOLEAN, )),
1134
'validate': (GObject.SignalFlags.RUN_LAST,
1135
GObject.TYPE_PYOBJECT,
1136
(GObject.TYPE_PYOBJECT, )),
1137
'changed': 'override',
1141
'data-type': (GObject.TYPE_PYOBJECT,
1142
'Data Type of the widget',
1144
GObject.PARAM_READWRITE),
1145
'mandatory': (GObject.TYPE_BOOLEAN,
1149
GObject.PARAM_READWRITE),
1152
# FIXME put the data type support back
1153
#allowed_data_types = (basestring, datetime.date, datetime.time,
1154
#datetime.datetime, object) + number
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
1162
MaskedEntry.__init__(self)
1165
self._def_error_msg = None
1166
self._fade = FadeOut(self, err_color)
1167
self._fade.connect('color-changed', self._on_fadeout__color_changed)
1169
# FIXME put data type support back
1170
#self.set_property('data-type', data_type)
1173
def do_changed(self):
1174
block_changed = getattr(self, '_block_changed', True)
1176
self.emit_stop_by_name('changed')
1178
self.emit('content-changed')
1181
def do_get_property(self, prop):
1182
"""Return the gproperty's value."""
1184
if prop.name == 'data-type':
1185
return self.data_type
1186
elif prop.name == 'mandatory':
1187
return self.mandatory
1189
raise AttributeError('unknown property %s' % prop.name)
1191
def do_set_property(self, prop, value):
1192
"""Set the property of writable properties."""
1194
if prop.name == 'data-type':
1196
self.data_type = value
1199
# FIXME put the data type support back
1200
#if not issubclass(value, self.allowed_data_types):
1202
#"%s only accept %s types, not %r"
1204
#' or '.join([t.__name__ for t in self.allowed_data_types]),
1206
self.data_type = value
1207
elif prop.name == 'mandatory':
1208
self.mandatory = value
1210
raise AttributeError('unknown or read only property %s' % prop.name)
1214
def set_default_error_msg(self, text):
1216
Set default message for validation error.
1218
Default error message for an instance is useful when completion is
1219
used, because this case custom validation is not called.
1221
@param text: can contain one and only one '%s', where the actual value
1222
of the Entry will be inserted.
1225
if not isinstance(text, str):
1226
raise TypeError("text must be a string")
1228
self._def_error_msg = text
1232
@returns: True if the widget is in validated state
1236
def validate(self, force=False):
1237
"""Checks if the data is valid.
1238
Validates data-type and custom validation.
1240
@param force: if True, force validation
1241
@returns: validated data or ValueUnset if it failed
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')):
1251
text = self.get_text()
1252
##_LOG.debug('Read %r for %s' % (data, self.model_attribute))
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():
1261
if self._completion:
1262
for row in self.get_completion().get_model():
1263
if row[COL_TEXT] == text:
1267
raise ValidationError()
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)
1278
except ValidationError as e:
1279
self.set_invalid(str(e))
1282
def set_valid(self):
1283
"""Change the validation state to valid, which will remove icons and
1284
reset the background color
1286
##_LOG.debug('Setting state for %s to VALID' % self.model_attribute)
1287
self._set_valid_state(True)
1290
self.set_pixbuf(None)
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
1297
##_LOG.debug('Setting state for %s to INVALID' % self.model_attribute)
1299
self._set_valid_state(False)
1301
generic_text = _("'%s' is not a valid value "
1302
"for this field") % self.get_text()
1304
# If there is no error text, let's try with the default or
1305
# fall back to a generic one
1307
text = self._def_error_msg
1313
text = text % self.get_text()
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
1321
# if text does not contain '%s'
1324
self.set_tooltip(text)
1328
self.set_stock(self.error_icon)
1329
self.update_background(Gdk.color_parse(self._fade.ERROR_COLOR))
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
1336
def done(fadeout, c):
1338
self.set_stock(self.error_icon)
1340
fadeout.disconnect(c.signal_id)
1342
class SignalContainer(object):
1344
c = SignalContainer()
1345
c.signal_id = self._fade.connect('done', done, c)
1347
if self._fade.start(self.get_background()):
1348
self.set_pixbuf(None)
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"""
1354
##_LOG.debug('Setting state for %s to BLANK' % self.model_attribute)
1357
self.set_stock(MANDATORY_ICON)
1359
self.set_tooltip(_('This field is mandatory'))
1365
self._set_valid_state(valid)
1367
def set_text(self, text):
1369
Set the text of the entry
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')
1382
self.set_position(-1)
1386
def _set_valid_state(self, state):
1387
"""Updates the validation state and emits a signal if it changed"""
1389
if self._valid == state:
1392
self.emit('validation-changed', state)
1397
def _on_fadeout__color_changed(self, fadeout, color):
1398
self.update_background(color)
1402
from gramps.gen.datehandler import parser
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"))
1411
win.set_title('ValidatableMaskedEntry test window')
1412
win.set_position(Gtk.WindowPosition.CENTER)
1413
def cb(window, event):
1415
win.connect('delete-event', cb)
1420
label = Gtk.Label(label='Pre-filled entry validated against the given list:')
1421
vbox.pack_start(label, True, True, 0)
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)
1431
label = Gtk.Label(label='Mandatory masked entry validated against user function:')
1432
vbox.pack_start(label, True, True, 0)
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)
1444
if __name__ == '__main__':
1446
# fall back to root logger for testing
1448
sys.exit(main(sys.argv))