def testDigitMask(self): e = ProxyEntry() e.set_mask('000.000') self.assertEqual(e.get_text(), ' . ') e.set_text('123.456') self.assertEqual(e.get_text(), '123.456') e.delete_text(0, 2) self.assertEqual(e.get_text(), '345.6 ')
def testMaskSmallFields(self): e = ProxyEntry() e.set_mask('0.0.0') self.assertEqual(e.get_text(), ' . . ') self.assertEqual(e.get_fields(), ['', '', '']) e.set_text('1.2.3') self.assertEqual(e.get_text(), '1.2.3') self.assertEqual(e.get_fields(), ['1', '2', '3'])
def testAlphaNumericMask(self): e = ProxyEntry() e.set_mask('&&&-aaa') self.assertEqual(e.get_text(), ' - ') self.assertEqual(e.get_fields(), ['', '']) e.set_text('aáé-á1e') self.assertEqual(e.get_text(), 'aáé-á1e') self.assertEqual(e.get_fields(), ['aáé', 'á1e'])
def testAsciiMask(self): e = ProxyEntry() e.set_mask('LLLL-L') self.assertEqual(e.get_text(), ' - ') self.assertEqual(e.get_fields(), ['', '']) e.set_text('abcd-e') self.assertEqual(e.get_text(), 'abcd-e') self.assertEqual(e.get_fields(), ['abcd', 'e'])
def testDigitMask(self): e = ProxyEntry() e.set_mask('000.000') self.assertEqual(e.get_text(), ' . ') self.assertEqual(e.get_fields(), ['', '']) e.set_text('123.456') self.assertEqual(e.get_text(), '123.456') self.assertEqual(e.get_fields(), ['123', '456']) e.delete_text(0, 2) self.assertEqual(e.get_text(), '3 .456') self.assertEqual(e.get_fields(), ['3', '456'])
class CalculatorPopup(PopupWindow): """A popup calculator for entries Right now it supports both :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` and :class:`kiwi.ui.widgets.entry.ProxyEntry`, as long as their data types are numeric (e.g. int, currency, Decimal, etc) """ #: The add mode. Any value typed on the entry will be added to the #: original value. e.g. 10% means +10% MODE_ADD = 0 #: The sub mode. Any value typed on the entry will be subtracted from the #: original value. e.g. 10% means -10% MODE_SUB = 1 _mode = None _data_type_mapper = { 'currency': currency, 'Decimal': decimal.Decimal, } def __init__(self, entry, mode): """ :param entry: a :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` or a :class:`kiwi.ui.widgets.entry.ProxyEntry` subclass :param mode: one of :attr:`.MODE_ADD`, :attr:`.MODE_SUB` """ self._mode = mode self._new_value = None self._data_type = self._data_type_mapper[entry.data_type] self._converter = converter.get_converter(self._data_type) super(CalculatorPopup, self).__init__(entry) # # Public API # def get_main_widget(self): # This is done on entry to check where to put the validation/mandatory # icons. We should put the calculator on the other side. # Note that spinbuttons are always right aligned and thus # xalign will always be 1.0 if self.attached_widget.get_alignment() > 0.5: self._icon_pos = 'secondary-icon' else: self._icon_pos = 'primary-icon' self.attached_widget.set_property(self._icon_pos + '-activatable', True) self.attached_widget.set_property( self._icon_pos + '-tooltip-text', _("Do calculations on top of this value")) self.attached_widget.connect('notify::sensitive', self._on_entry_sensitive__notify) self.attached_widget.connect('icon-press', self._on_entry__icon_press) self._toggle_calculator_icon() vbox = Gtk.VBox(spacing=6) vbox.show() self._main_label = Gtk.Label() self._main_label.set_ellipsize(Pango.EllipsizeMode.END) vbox.pack_start(self._main_label, True, True, 0) self._main_label.show() self._entry = ProxyEntry() # FIXME: We need a model_attribute here or else the entry.is_valid() # will always return None. Check proxywidget.py's FIXME to see why self._entry.model_attribute = 'not_used' self._entry.connect('validate', self._on_entry__validate) self._entry.connect_after('changed', self._after_entry__changed) self._entry.set_alignment(1.0) vbox.pack_start(self._entry, True, True, 0) self._entry.show() hbox = Gtk.HBox(spacing=6) vbox.pack_start(hbox, True, True, 0) hbox.show() self._label = Gtk.Label() self._label.set_property('xalign', 1.0) self._label.set_use_markup(True) hbox.pack_start(self._label, True, True, 0) self._label.show() self._warning = Gtk.Image() hbox.pack_start(self._warning, False, False, 0) return vbox def validate_popup(self): try: self._new_value = self._data_type(self.attached_widget.get_text()) except decimal.InvalidOperation: return False self._entry.set_text('') self._entry.set_tooltip_text(_("Use absolute or percentage (%) value")) self._preview_new_value() self._main_label.set_text(self._get_main_label()) return True def confirm(self): self._maybe_apply_new_value() # # Private # def _get_main_label(self): if self._mode == self.MODE_ADD: return (_("Surcharge") if self._data_type == currency else _("Addition")) elif self._mode == self.MODE_SUB: return (_("Discount") if self._data_type == currency else _("Subtraction")) else: raise AssertionError def _set_warning(self, warning): if warning is None: self._warning.hide() else: self._warning.set_from_stock(Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU) self._warning.set_tooltip_text(warning) self._warning.show() def _get_new_value(self): operation = self._entry.get_text().strip() operation = operation.replace(',', '.') if operation.endswith('%'): op_value = operation[:-1] percentage = True else: op_value = operation percentage = False if not operation: return if operation[0] in ['+', '-']: raise ValueError(_("Operator signals are not supported...")) if self._mode == self.MODE_SUB: op = operator.sub elif self._mode == self.MODE_ADD: op = operator.add try: op_value = decimal.Decimal(op_value) except decimal.InvalidOperation: raise ValueError( _("'%s' is not a valid operation...") % (operation, )) if percentage: value = op(self._new_value, self._new_value * (op_value / 100)) else: value = op(self._new_value, op_value) return value def _update_new_value(self): if not self._entry.is_valid(): return self._new_value = self._get_new_value() self._entry.set_text('') self._preview_new_value() def _preview_new_value(self): self._label.set_markup('<b>%s</b>' % (self._converter.as_string(self._new_value), )) def _maybe_apply_new_value(self): if self._entry.get_text(): self._update_new_value() return self.attached_widget.update(self._new_value) self.popdown() def _toggle_calculator_icon(self): if self.attached_widget.get_sensitive(): pixbuf = self.render_icon(STOQ_CALC, Gtk.IconSize.MENU) else: pixbuf = None self.attached_widget.set_property(self._icon_pos + '-pixbuf', pixbuf) # # Callbacks # def _on_entry__validate(self, entry, value): try: value = self._get_new_value() except ValueError as err: return ValidationError( '%s\n%s' % (err, _("Use absolute or percentage (%) value"))) if value: warning = self.attached_widget.emit('validate', value) warning = warning and str(warning) else: warning = None self._set_warning(warning) def _after_entry__changed(self, entry): entry.validate(force=True) def _on_entry_sensitive__notify(self, obj, pspec): self._toggle_calculator_icon() def _on_entry__icon_press(self, entry, icon_pos, event): if icon_pos != Gtk.EntryIconPosition.SECONDARY: return self.popup()
class CalculatorPopup(gtk.Window): """A popup calculator for entries Right now it supports both :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` and :class:`kiwi.ui.widgets.entry.ProxyEntry`, as long as their data types are numeric (e.g. int, currency, Decimal, etc) """ #: The add mode. Any value typed on the entry will be added to the #: original value. e.g. 10% means +10% MODE_ADD = 0 #: The sub mode. Any value typed on the entry will be subtracted from the #: original value. e.g. 10% means -10% MODE_SUB = 1 _mode = None _data_type_mapper = { 'currency': currency, 'Decimal': decimal.Decimal, } def __init__(self, entry, mode): """ :param entry: a :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` or a :class:`kiwi.ui.widgets.entry.ProxyEntry` subclass :param mode: one of :attr:`.MODE_ADD`, :attr:`.MODE_SUB` """ self._mode = mode self._new_value = None self._popped_entry = entry self._data_type = self._data_type_mapper[entry.data_type] self._converter = converter.get_converter(self._data_type) gtk.Window.__init__(self, gtk.WINDOW_POPUP) self._create_ui() # # Public API # def popup(self): if not self._popped_entry.get_realized(): return toplevel = self._popped_entry.get_toplevel().get_toplevel() if (isinstance(toplevel, (gtk.Window, gtk.Dialog)) and toplevel.get_group()): toplevel.get_group().add_window(self) # width is meant for the popup window x, y, width, height = self._get_position() self.set_size_request(width, -1) self.move(x, y) self.show_all() if not self._popup_grab_window(): self.hide() return self.grab_add() self._update_ui() def popdown(self): if not self._popped_entry.get_realized(): return self.grab_remove() self.hide() self._popped_entry.grab_focus() # # Private # def _create_ui(self): self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.KEY_PRESS_MASK) self.connect('key-press-event', self._on__key_press_event) self.connect('button-press-event', self._on__button_press_event) # This is done on entry to check where to put the validation/mandatory # icons. We should put the calculator on the other side. # Note that spinbuttons are always right aligned and thus # xalign will always be 1.0 if self._popped_entry.get_property('xalign') > 0.5: self._icon_pos = 'secondary-icon' else: self._icon_pos = 'primary-icon' self._popped_entry.set_property(self._icon_pos + '-activatable', True) self._popped_entry.set_property(self._icon_pos + '-tooltip-text', _("Do calculations on top of this value")) self._popped_entry.connect('notify::sensitive', self._on_popped_entry_sensitive__notify) self._popped_entry.connect('icon-press', self._on_popped_entry__icon_press) self._toggle_calculator_icon() frame = gtk.Frame() frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.add(frame) frame.show() alignment = gtk.Alignment(0.5, 0.5, 1.0, 1.0) alignment.set_padding(6, 6, 2, 2) frame.add(alignment) alignment.show() vbox = gtk.VBox(spacing=6) alignment.add(vbox) vbox.show() self._main_label = gtk.Label() self._main_label.set_ellipsize(pango.ELLIPSIZE_END) vbox.pack_start(self._main_label, True, True) self._main_label.show() self._entry = ProxyEntry() # FIXME: We need a model_attribute here or else the entry.is_valid() # will always return None. Check proxywidget.py's FIXME to see why self._entry.model_attribute = 'not_used' self._entry.connect('validate', self._on_entry__validate) self._entry.connect_after('changed', self._after_entry__changed) self._entry.set_alignment(1.0) vbox.pack_start(self._entry, True, True) self._entry.show() hbox = gtk.HBox(spacing=6) vbox.pack_start(hbox, True, True) hbox.show() self._label = gtk.Label() self._label.set_property('xalign', 1.0) self._label.set_use_markup(True) hbox.pack_start(self._label, True, True) self._label.show() self._warning = gtk.Image() hbox.pack_start(self._warning, False, False) self.set_resizable(False) self.set_screen(self._popped_entry.get_screen()) def _update_ui(self): self._new_value = self._data_type(self._popped_entry.get_text()) self._entry.set_text('') self._entry.set_tooltip_text(_("Use absolute or percentage (%) value")) self._preview_new_value() self._main_label.set_text(self._get_main_label()) def _get_main_label(self): if self._mode == self.MODE_ADD: return (_("Surcharge") if self._data_type == currency else _("Addition")) elif self._mode == self.MODE_SUB: return (_("Discount") if self._data_type == currency else _("Subtraction")) else: raise AssertionError def _set_warning(self, warning): if warning is None: self._warning.hide() else: self._warning.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) self._warning.set_tooltip_text(warning) self._warning.show() def _popup_grab_window(self): activate_time = 0L window = self.get_window() grab_status = gtk.gdk.pointer_grab(window, True, (gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK), None, None, activate_time) if grab_status == gtk.gdk.GRAB_SUCCESS: if gtk.gdk.keyboard_grab(window, True, activate_time) == 0: return True else: window.get_display().pointer_ungrab(activate_time) return False return False def _get_position(self): widget = self._popped_entry allocation = widget.get_allocation() window = widget.get_window() if hasattr(window, 'get_root_coords'): x = 0 y = 0 if not widget.get_has_window(): x += allocation.x y += allocation.y x, y = window.get_root_coords(x, y) else: # PyGTK lacks gdk_window_get_root_coords(), # but we can use get_origin() instead, which is the # same thing in our case. x, y = widget.window.get_origin() return x, y + allocation.height, allocation.width, allocation.height def _get_new_value(self): operation = self._entry.get_text().strip() operation = operation.replace(',', '.') if operation.endswith('%'): op_value = operation[:-1] percentage = True else: op_value = operation percentage = False if not operation: return if operation[0] in ['+', '-']: raise ValueError(_("Operator signals are not supported...")) if self._mode == self.MODE_SUB: op = operator.sub elif self._mode == self.MODE_ADD: op = operator.add try: op_value = decimal.Decimal(op_value) except decimal.InvalidOperation: raise ValueError( _("'%s' is not a valid operation...") % (operation,)) if percentage: value = op(self._new_value, self._new_value * (op_value / 100)) else: value = op(self._new_value, op_value) return value def _update_new_value(self): if not self._entry.is_valid(): return self._new_value = self._get_new_value() self._entry.set_text('') self._preview_new_value() def _preview_new_value(self): self._label.set_markup('<b>%s</b>' % ( self._converter.as_string(self._new_value), )) def _maybe_apply_new_value(self): if self._entry.get_text(): self._update_new_value() return self._popped_entry.update(self._new_value) self.popdown() def _toggle_calculator_icon(self): if self._popped_entry.get_sensitive(): pixbuf = self.render_icon(STOQ_CALC, gtk.ICON_SIZE_MENU) else: pixbuf = None self._popped_entry.set_property(self._icon_pos + '-pixbuf', pixbuf) # # Callbacks # def _on__key_press_event(self, window, event): keyval = event.keyval if keyval == gtk.keysyms.Escape: self.popdown() return True elif keyval in [gtk.keysyms.Return, gtk.keysyms.KP_Enter, gtk.keysyms.Tab]: self._maybe_apply_new_value() return True return False def _on__button_press_event(self, window, event): # If we're clicking outside of the window # close the popup if (event.window != self.get_window() or (tuple(self.allocation.intersect( gtk.gdk.Rectangle(x=int(event.x), y=int(event.y), width=1, height=1)))) == (0, 0, 0, 0)): self.popdown() def _on_entry__validate(self, entry, value): try: value = self._get_new_value() except ValueError as err: return ValidationError('%s\n%s' % (err, _("Use absolute or percentage (%) value"))) if value: warning = self._popped_entry.emit('validate', value) warning = warning and str(warning) else: warning = None self._set_warning(warning) def _after_entry__changed(self, entry): entry.validate(force=True) def _on_popped_entry_sensitive__notify(self, obj, pspec): self._toggle_calculator_icon() def _on_popped_entry__icon_press(self, entry, icon_pos, event): if icon_pos != gtk.ENTRY_ICON_SECONDARY: return self.popup()
class CalculatorPopup(gtk.Window): """A popup calculator for entries Right now it supports both :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` and :class:`kiwi.ui.widgets.entry.ProxyEntry`, as long as their data types are numeric (e.g. int, currency, Decimal, etc) """ #: The add mode. Any value typed on the entry will be added to the #: original value. e.g. 10% means +10% MODE_ADD = 0 #: The sub mode. Any value typed on the entry will be subtracted from the #: original value. e.g. 10% means -10% MODE_SUB = 1 _mode = None _data_type_mapper = { 'currency': currency, 'Decimal': decimal.Decimal, } def __init__(self, entry, mode): """ :param entry: a :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` or a :class:`kiwi.ui.widgets.entry.ProxyEntry` subclass :param mode: one of :attr:`.MODE_ADD`, :attr:`.MODE_SUB` """ self._mode = mode self._new_value = None self._popped_entry = entry self._data_type = self._data_type_mapper[entry.data_type] self._converter = converter.get_converter(self._data_type) gtk.Window.__init__(self, gtk.WINDOW_POPUP) self._create_ui() # # Public API # def popup(self): if not self._popped_entry.get_realized(): return if not self._update_ui(): return toplevel = self._popped_entry.get_toplevel().get_toplevel() if (isinstance(toplevel, (gtk.Window, gtk.Dialog)) and toplevel.get_group()): toplevel.get_group().add_window(self) # width is meant for the popup window x, y, width, height = self._get_position() self.set_size_request(width, -1) self.move(x, y) self.show_all() if not self._popup_grab_window(): self.hide() return self.grab_add() def popdown(self): if not self._popped_entry.get_realized(): return self.grab_remove() self.hide() self._popped_entry.grab_focus() # # Private # def _create_ui(self): self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.KEY_PRESS_MASK) self.connect('key-press-event', self._on__key_press_event) self.connect('button-press-event', self._on__button_press_event) # This is done on entry to check where to put the validation/mandatory # icons. We should put the calculator on the other side. # Note that spinbuttons are always right aligned and thus # xalign will always be 1.0 if self._popped_entry.get_property('xalign') > 0.5: self._icon_pos = 'secondary-icon' else: self._icon_pos = 'primary-icon' self._popped_entry.set_property(self._icon_pos + '-activatable', True) self._popped_entry.set_property(self._icon_pos + '-tooltip-text', _("Do calculations on top of this value")) self._popped_entry.connect('notify::sensitive', self._on_popped_entry_sensitive__notify) self._popped_entry.connect('icon-press', self._on_popped_entry__icon_press) self._toggle_calculator_icon() frame = gtk.Frame() frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.add(frame) frame.show() alignment = gtk.Alignment(0.5, 0.5, 1.0, 1.0) alignment.set_padding(6, 6, 2, 2) frame.add(alignment) alignment.show() vbox = gtk.VBox(spacing=6) alignment.add(vbox) vbox.show() self._main_label = gtk.Label() self._main_label.set_ellipsize(pango.ELLIPSIZE_END) vbox.pack_start(self._main_label, True, True) self._main_label.show() self._entry = ProxyEntry() # FIXME: We need a model_attribute here or else the entry.is_valid() # will always return None. Check proxywidget.py's FIXME to see why self._entry.model_attribute = 'not_used' self._entry.connect('validate', self._on_entry__validate) self._entry.connect_after('changed', self._after_entry__changed) self._entry.set_alignment(1.0) vbox.pack_start(self._entry, True, True) self._entry.show() hbox = gtk.HBox(spacing=6) vbox.pack_start(hbox, True, True) hbox.show() self._label = gtk.Label() self._label.set_property('xalign', 1.0) self._label.set_use_markup(True) hbox.pack_start(self._label, True, True) self._label.show() self._warning = gtk.Image() hbox.pack_start(self._warning, False, False) self.set_resizable(False) self.set_screen(self._popped_entry.get_screen()) def _update_ui(self): try: self._new_value = self._data_type(self._popped_entry.get_text()) except decimal.InvalidOperation: return False self._entry.set_text('') self._entry.set_tooltip_text(_("Use absolute or percentage (%) value")) self._preview_new_value() self._main_label.set_text(self._get_main_label()) return True def _get_main_label(self): if self._mode == self.MODE_ADD: return (_("Surcharge") if self._data_type == currency else _("Addition")) elif self._mode == self.MODE_SUB: return (_("Discount") if self._data_type == currency else _("Subtraction")) else: raise AssertionError def _set_warning(self, warning): if warning is None: self._warning.hide() else: self._warning.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU) self._warning.set_tooltip_text(warning) self._warning.show() def _popup_grab_window(self): activate_time = 0L window = self.get_window() grab_status = gtk.gdk.pointer_grab(window, True, (gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK), None, None, activate_time) if grab_status == gtk.gdk.GRAB_SUCCESS: if gtk.gdk.keyboard_grab(window, True, activate_time) == 0: return True else: window.get_display().pointer_ungrab(activate_time) return False return False def _get_position(self): widget = self._popped_entry allocation = widget.get_allocation() window = widget.get_window() if hasattr(window, 'get_root_coords'): x = 0 y = 0 if not widget.get_has_window(): x += allocation.x y += allocation.y x, y = window.get_root_coords(x, y) else: # PyGTK lacks gdk_window_get_root_coords(), # but we can use get_origin() instead, which is the # same thing in our case. x, y = widget.window.get_origin() return x, y + allocation.height, allocation.width, allocation.height def _get_new_value(self): operation = self._entry.get_text().strip() operation = operation.replace(',', '.') if operation.endswith('%'): op_value = operation[:-1] percentage = True else: op_value = operation percentage = False if not operation: return if operation[0] in ['+', '-']: raise ValueError(_("Operator signals are not supported...")) if self._mode == self.MODE_SUB: op = operator.sub elif self._mode == self.MODE_ADD: op = operator.add try: op_value = decimal.Decimal(op_value) except decimal.InvalidOperation: raise ValueError( _("'%s' is not a valid operation...") % (operation,)) if percentage: value = op(self._new_value, self._new_value * (op_value / 100)) else: value = op(self._new_value, op_value) return value def _update_new_value(self): if not self._entry.is_valid(): return self._new_value = self._get_new_value() self._entry.set_text('') self._preview_new_value() def _preview_new_value(self): self._label.set_markup('<b>%s</b>' % ( self._converter.as_string(self._new_value), )) def _maybe_apply_new_value(self): if self._entry.get_text(): self._update_new_value() return self._popped_entry.update(self._new_value) self.popdown() def _toggle_calculator_icon(self): if self._popped_entry.get_sensitive(): pixbuf = self.render_icon(STOQ_CALC, gtk.ICON_SIZE_MENU) else: pixbuf = None self._popped_entry.set_property(self._icon_pos + '-pixbuf', pixbuf) # # Callbacks # def _on__key_press_event(self, window, event): keyval = event.keyval if keyval == gtk.keysyms.Escape: self.popdown() return True elif keyval in [gtk.keysyms.Return, gtk.keysyms.KP_Enter, gtk.keysyms.Tab]: self._maybe_apply_new_value() return True return False def _on__button_press_event(self, window, event): # If we're clicking outside of the window # close the popup if (event.window != self.get_window() or (tuple(self.allocation.intersect( gtk.gdk.Rectangle(x=int(event.x), y=int(event.y), width=1, height=1)))) == (0, 0, 0, 0)): self.popdown() def _on_entry__validate(self, entry, value): try: value = self._get_new_value() except ValueError as err: return ValidationError('%s\n%s' % (err, _("Use absolute or percentage (%) value"))) if value: warning = self._popped_entry.emit('validate', value) warning = warning and str(warning) else: warning = None self._set_warning(warning) def _after_entry__changed(self, entry): entry.validate(force=True) def _on_popped_entry_sensitive__notify(self, obj, pspec): self._toggle_calculator_icon() def _on_popped_entry__icon_press(self, entry, icon_pos, event): if icon_pos != gtk.ENTRY_ICON_SECONDARY: return self.popup()
class DateEntry(Gtk.Box): """I am an entry which you can input a date on. In addition to an Gtk.Entry I also contain a button with an arrow you can click to get popup window with a Gtk.Calendar for which you can use to select the date """ gsignal('changed') gsignal('activate') def __init__(self): super(DateEntry, self).__init__(orientation=Gtk.Orientation.HORIZONTAL) self._popping_down = False self._old_date = None self._block_changed = False # This will force both the entry and the button have the same height self._sizegroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL) # bootstrap problems, kiwi.ui.widgets.entry imports dateentry # we need to use a proxy entry because we want the mask from kiwi.ui.widgets.entry import ProxyEntry self.entry = ProxyEntry() # Set datatype before connecting to change event, to not get when the # mask is set self.entry.set_property('data-type', datetime.date) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('activate', self._on_entry__activate) mask = self.entry.get_mask() if mask: self.entry.set_width_chars(len(mask)) self.pack_start(self.entry, True, True, 0) self.entry.set_valign(Gtk.Align.CENTER) self._sizegroup.add_widget(self.entry) self.entry.show() self._button = Gtk.ToggleButton() self._button.set_valign(Gtk.Align.CENTER) self._button.connect('scroll-event', self._on_entry__scroll_event) self._button.connect('toggled', self._on_button__toggled) self._button.set_focus_on_click(False) self.pack_start(self._button, False, False, 0) self._sizegroup.add_widget(self._button) self._button.show() arrow = Gtk.Arrow(Gtk.ArrowType.DOWN, Gtk.ShadowType.NONE) self._button.add(arrow) arrow.show() self._popup = _DateEntryPopup(self) self._popup.connect('date-selected', self._on_popup__date_selected) self._popup.connect('hide', self._on_popup__hide) self._popup.set_size_request(-1, 24) self.set_valign(Gtk.Align.CENTER) # Virtual methods def do_grab_focus(self): self.entry.grab_focus() # Callbacks def _on_entry__changed(self, entry): try: date = self.get_date() except ValidationError: date = None self._changed(date) def _on_entry__activate(self, entry): self.emit('activate') def _on_entry__scroll_event(self, entry, event): if event.direction == Gdk.ScrollDirection.UP: days = 1 elif event.direction == Gdk.ScrollDirection.DOWN: days = -1 else: return try: date = self.get_date() except ValidationError: date = None if not date: newdate = datetime.date.today() else: newdate = date + datetime.timedelta(days=days) self.set_date(newdate) def _on_button__toggled(self, button): if self._popping_down: return try: date = self.get_date() except ValidationError: date = None self._popup.popup(date) def _on_popup__hide(self, popup): self._popping_down = True self._button.set_active(False) self._popping_down = False def _on_popup__date_selected(self, popup, date): self.set_date(date) popup.popdown() self.entry.grab_focus() self.entry.set_position(len(self.entry.get_text())) self._changed(date) def _changed(self, date): if self._block_changed: return if self._old_date != date: self.emit('changed') self._old_date = date # Public API def set_date(self, date): """Sets the date. :param date: date to set :type date: a datetime.date instance or None """ if not isinstance(date, datetime.date) and date not in [ None, ValueUnset ]: raise TypeError( "date must be a datetime.date instance or None, not %r" % (date, )) if date in [None, ValueUnset]: value = '' else: value = date_converter.as_string(date) # We're block the changed call and doing it manually because # set_text() triggers a delete-text and then an insert-text, # both which are emitting an entry::changed signal self._block_changed = True self.entry.set_text(value) self._block_changed = False self._changed(date) def get_date(self): """Get the selected date :returns: the date. :rtype: datetime.date or None """ try: date = self.entry.read() except ValidationError: date = None if date == ValueUnset: date = None return date
class TestSearchEntryGadget(GUITest): def _create_interface(self, run_editor=None): self.sale = self.create_sale() self.window = gtk.Window() self.entry = ProxyEntry() self.window.add(self.entry) self.client_gadget = SearchEntryGadget( self.entry, self.store, model=self.sale, model_property='client', search_columns=['name'], search_class=ClientSearch, parent=self.window, run_editor=run_editor) self.client_gadget.get_model_obj = lambda obj: obj and obj.client def test_create(self): window = gtk.Window() box = gtk.VBox() window.add(box) entry = ProxyEntry() box.pack_start(entry) self.check_dialog(window, 'search-entry-before-replace') sale = self.create_sale() SearchEntryGadget(entry, self.store, model=sale, model_property='client', search_columns=['name'], search_class=ClientSearch, parent=window) self.check_dialog(window, 'search-entry-after-replace') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_run_search(self, run_dialog): self._create_interface() run_dialog.return_value = None self.click(self.client_gadget.find_button) run_dialog.assert_called_once_with( ClientSearch, self.window, self.store, initial_string='', double_click_confirm=True) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_person_role_dialog') def test_run_editor(self, run_dialog, new_store): new_store.return_value = self.store self._create_interface() client = self.create_client(name=u'Fulano de Tal') run_dialog.return_value = self.store.find( ClientView, ClientView.id == client.id).one() with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): self.click(self.client_gadget.edit_button) run_dialog.assert_called_once_with( ClientEditor, self.window, self.store, None) self.assertEquals(self.entry.read(), client) self.assertEquals(self.entry.get_text(), u'Fulano de Tal') @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') def test_run_editor_override(self, new_store): new_store.return_value = self.store run_editor = mock.MagicMock() run_editor.return_value = None self.assertEquals(run_editor.call_count, 0) self._create_interface(run_editor=run_editor) with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): self.click(self.client_gadget.edit_button) self.assertEquals(run_editor.call_count, 1) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_entry_activate(self, run_dialog, new_store): new_store.return_value = self.store self._create_interface() fulano = self.create_client(u'Fulano de Tal') ciclano = self.create_client(u'Cicrano de Tal') # There should be only one match for Fulano, then the entry should be # updated with this only match self.entry.set_text('Fulano') self.entry.activate() self.assertEquals(self.entry.get_text(), 'Fulano de Tal') self.assertEquals(self.entry.read(), fulano) # Now when we use 'de tal', there are two clients that match. The # search should be displayed run_dialog.return_value = self.store.find( ClientView, ClientView.id == ciclano.id).one() self.entry.set_text('de tal') self.entry.activate() run_dialog.assert_called_once_with( ClientSearch, self.window, self.store, initial_string='de tal', double_click_confirm=True) self.assertEquals(self.entry.get_text(), 'Cicrano de Tal') self.assertEquals(self.entry.read(), ciclano) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_with_cfop(self, run_dialog, new_store): new_store.return_value = self.store window = gtk.Window() entry = ProxyEntry() window.add(entry) sale = self.create_sale() gadget = SearchEntryGadget( entry, self.store, model=sale, model_property='cfop', search_columns=['name'], search_class=CfopSearch, parent=window) with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): run_dialog.return_value = None self.click(gadget.edit_button) run_dialog.assert_called_once_with( CfopEditor, window, self.store, sale.cfop)
class DateEntry(Gtk.Box): """I am an entry which you can input a date on. In addition to an Gtk.Entry I also contain a button with an arrow you can click to get popup window with a Gtk.Calendar for which you can use to select the date """ gsignal('changed') gsignal('activate') def __init__(self): super(DateEntry, self).__init__(orientation=Gtk.Orientation.HORIZONTAL) self.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED) self._popping_down = False self._old_date = None self._block_changed = False # This will force both the entry and the button have the same height self._sizegroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL) # bootstrap problems, kiwi.ui.widgets.entry imports dateentry # we need to use a proxy entry because we want the mask from kiwi.ui.widgets.entry import ProxyEntry self.entry = ProxyEntry() # Set datatype before connecting to change event, to not get when the # mask is set self.entry.set_property('data-type', datetime.date) self.entry.connect('changed', self._on_entry__changed) self.entry.connect('activate', self._on_entry__activate) mask = self.entry.get_mask() if mask: self.entry.set_width_chars(len(mask)) self.pack_start(self.entry, True, True, 0) self.entry.set_valign(Gtk.Align.CENTER) self._sizegroup.add_widget(self.entry) self.entry.show() self._button = Gtk.ToggleButton() self._button.set_valign(Gtk.Align.CENTER) self._button.connect('scroll-event', self._on_entry__scroll_event) self._button.connect('toggled', self._on_button__toggled) self._button.set_focus_on_click(False) self.pack_start(self._button, False, False, 0) self._sizegroup.add_widget(self._button) self._button.show() arrow = Gtk.Arrow(arrow_type=Gtk.ArrowType.DOWN, shadow_type=Gtk.ShadowType.NONE) self._button.add(arrow) arrow.show() self._popup = _DateEntryPopup(self) self._popup.connect('date-selected', self._on_popup__date_selected) self._popup.connect('hide', self._on_popup__hide) self._popup.set_size_request(-1, 24) self.set_valign(Gtk.Align.CENTER) # Virtual methods def do_grab_focus(self): self.entry.grab_focus() # Callbacks def _on_entry__changed(self, entry): try: date = self.get_date() except ValidationError: date = None self._changed(date) def _on_entry__activate(self, entry): self.emit('activate') def _on_entry__scroll_event(self, entry, event): if event.direction == Gdk.ScrollDirection.UP: days = 1 elif event.direction == Gdk.ScrollDirection.DOWN: days = -1 else: return try: date = self.get_date() except ValidationError: date = None if not date: newdate = datetime.date.today() else: newdate = date + datetime.timedelta(days=days) self.set_date(newdate) def _on_button__toggled(self, button): if self._popping_down: return try: date = self.get_date() except ValidationError: date = None self._popup.popup(date) def _on_popup__hide(self, popup): self._popping_down = True self._button.set_active(False) self._popping_down = False def _on_popup__date_selected(self, popup, date): self.set_date(date) popup.popdown() self.entry.grab_focus() self.entry.set_position(len(self.entry.get_text())) self._changed(date) def _changed(self, date): if self._block_changed: return if self._old_date != date: self.emit('changed') self._old_date = date # Public API def set_date(self, date): """Sets the date. :param date: date to set :type date: a datetime.date instance or None """ if not isinstance(date, datetime.date) and date not in [None, ValueUnset]: raise TypeError( "date must be a datetime.date instance or None, not %r" % ( date,)) if date in [None, ValueUnset]: value = '' else: value = date_converter.as_string(date) # We're block the changed call and doing it manually because # set_text() triggers a delete-text and then an insert-text, # both which are emitting an entry::changed signal self._block_changed = True self.entry.set_text(value) self._block_changed = False self._changed(date) def get_date(self): """Get the selected date :returns: the date. :rtype: datetime.date or None """ try: date = self.entry.read() except ValidationError: date = None if date == ValueUnset: date = None return date
def testMaskSmallFields(self): e = ProxyEntry() e.set_mask('0.0.0') self.assertEqual(e.get_text(), ' . . ') e.set_text('1.2.3') self.assertEqual(e.get_text(), '1.2.3')
def testAlphaNumericMask(self): e = ProxyEntry() e.set_mask('&&&-aaa') self.assertEqual(e.get_text(), ' - ') e.set_text('aáé-á1e') self.assertEqual(e.get_text(), 'aáé-á1e')
def testAsciiMask(self): e = ProxyEntry() e.set_mask('LLLL-L') self.assertEqual(e.get_text(), ' - ') e.set_text('abcd-e') self.assertEqual(e.get_text(), 'abcd-e')
class CalculatorPopup(PopupWindow): """A popup calculator for entries Right now it supports both :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` and :class:`kiwi.ui.widgets.entry.ProxyEntry`, as long as their data types are numeric (e.g. int, currency, Decimal, etc) """ #: The add mode. Any value typed on the entry will be added to the #: original value. e.g. 10% means +10% MODE_ADD = 0 #: The sub mode. Any value typed on the entry will be subtracted from the #: original value. e.g. 10% means -10% MODE_SUB = 1 _mode = None _data_type_mapper = { 'currency': currency, 'Decimal': decimal.Decimal, } def __init__(self, entry, mode): """ :param entry: a :class:`kiwi.ui.widgets.spinbutton.ProxySpinButton` or a :class:`kiwi.ui.widgets.entry.ProxyEntry` subclass :param mode: one of :attr:`.MODE_ADD`, :attr:`.MODE_SUB` """ self._mode = mode self._new_value = None self._data_type = self._data_type_mapper[entry.data_type] self._converter = converter.get_converter(self._data_type) super(CalculatorPopup, self).__init__(entry) # # Public API # def get_main_widget(self): # This is done on entry to check where to put the validation/mandatory # icons. We should put the calculator on the other side. # Note that spinbuttons are always right aligned and thus # xalign will always be 1.0 if self.attached_widget.get_alignment() > 0.5: self._icon_pos = 'secondary-icon' else: self._icon_pos = 'primary-icon' self.attached_widget.set_property(self._icon_pos + '-activatable', True) self.attached_widget.set_property( self._icon_pos + '-tooltip-text', _("Do calculations on top of this value")) self.attached_widget.connect( 'notify::sensitive', self._on_entry_sensitive__notify) self.attached_widget.connect('icon-press', self._on_entry__icon_press) self._toggle_calculator_icon() vbox = Gtk.VBox(spacing=6) vbox.show() self._main_label = Gtk.Label() self._main_label.set_ellipsize(Pango.EllipsizeMode.END) vbox.pack_start(self._main_label, True, True, 0) self._main_label.show() self._entry = ProxyEntry() # FIXME: We need a model_attribute here or else the entry.is_valid() # will always return None. Check proxywidget.py's FIXME to see why self._entry.model_attribute = 'not_used' self._entry.connect('validate', self._on_entry__validate) self._entry.connect_after('changed', self._after_entry__changed) self._entry.set_alignment(1.0) vbox.pack_start(self._entry, True, True, 0) self._entry.show() hbox = Gtk.HBox(spacing=6) vbox.pack_start(hbox, True, True, 0) hbox.show() self._label = Gtk.Label() self._label.set_property('xalign', 1.0) self._label.set_use_markup(True) hbox.pack_start(self._label, True, True, 0) self._label.show() self._warning = Gtk.Image() hbox.pack_start(self._warning, False, False, 0) return vbox def validate_popup(self): try: self._new_value = self._data_type(self.attached_widget.get_text()) except decimal.InvalidOperation: return False self._entry.set_text('') self._entry.set_tooltip_text(_("Use absolute or percentage (%) value")) self._preview_new_value() self._main_label.set_text(self._get_main_label()) return True def confirm(self): self._maybe_apply_new_value() # # Private # def _get_main_label(self): if self._mode == self.MODE_ADD: return (_("Surcharge") if self._data_type == currency else _("Addition")) elif self._mode == self.MODE_SUB: return (_("Discount") if self._data_type == currency else _("Subtraction")) else: raise AssertionError def _set_warning(self, warning): if warning is None: self._warning.hide() else: self._warning.set_from_stock(Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.MENU) self._warning.set_tooltip_text(warning) self._warning.show() def _get_new_value(self): operation = self._entry.get_text().strip() operation = operation.replace(',', '.') if operation.endswith('%'): op_value = operation[:-1] percentage = True else: op_value = operation percentage = False if not operation: return if operation[0] in ['+', '-']: raise ValueError(_("Operator signals are not supported...")) if self._mode == self.MODE_SUB: op = operator.sub elif self._mode == self.MODE_ADD: op = operator.add try: op_value = decimal.Decimal(op_value) except decimal.InvalidOperation: raise ValueError( _("'%s' is not a valid operation...") % (operation,)) if percentage: value = op(self._new_value, self._new_value * (op_value / 100)) else: value = op(self._new_value, op_value) return value def _update_new_value(self): if not self._entry.is_valid(): return self._new_value = self._get_new_value() self._entry.set_text('') self._preview_new_value() def _preview_new_value(self): self._label.set_markup('<b>%s</b>' % ( self._converter.as_string(self._new_value), )) def _maybe_apply_new_value(self): if self._entry.get_text(): self._update_new_value() return self.attached_widget.update(self._new_value) self.popdown() def _toggle_calculator_icon(self): if self.attached_widget.get_sensitive(): icon = STOQ_CALC else: icon = None self.attached_widget.set_property(self._icon_pos + '-name', icon) # # Callbacks # def _on_entry__validate(self, entry, value): try: value = self._get_new_value() except ValueError as err: return ValidationError('%s\n%s' % (err, _("Use absolute or percentage (%) value"))) if value: warning = self.attached_widget.emit('validate', value) warning = warning and str(warning) else: warning = None self._set_warning(warning) def _after_entry__changed(self, entry): entry.validate(force=True) def _on_entry_sensitive__notify(self, obj, pspec): self._toggle_calculator_icon() def _on_entry__icon_press(self, entry, icon_pos, event): if icon_pos != Gtk.EntryIconPosition.SECONDARY: return self.popup()
class DateEntry(gtk.HBox): """I am an entry which you can input a date on. In addition to an gtk.Entry I also contain a button with an arrow you can click to get popup window with a gtk.Calendar for which you can use to select the date """ gsignal('changed') gsignal('activate') def __init__(self): gtk.HBox.__init__(self) self._popping_down = False self._old_date = None # bootstrap problems, kiwi.ui.widgets.entry imports dateentry # we need to use a proxy entry because we want the mask from kiwi.ui.widgets.entry import ProxyEntry self.entry = ProxyEntry() self.entry.connect('changed', self._on_entry__changed) self.entry.connect('activate', self._on_entry__activate) self.entry.set_property('data-type', datetime.date) mask = self.entry.get_mask() if mask: self.entry.set_width_chars(len(mask)) self.pack_start(self.entry, False, False) self.entry.show() self._button = gtk.ToggleButton() self._button.connect('scroll-event', self._on_entry__scroll_event) self._button.connect('toggled', self._on_button__toggled) self._button.set_focus_on_click(False) self.pack_start(self._button, False, False) self._button.show() arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE) self._button.add(arrow) arrow.show() self._popup = _DateEntryPopup(self) self._popup.connect('date-selected', self._on_popup__date_selected) self._popup.connect('hide', self._on_popup__hide) self._popup.set_size_request(-1, 24) # Virtual methods def do_grab_focus(self): self.entry.grab_focus() # Callbacks def _on_entry__changed(self, entry): try: date = self.get_date() except ValidationError: date = None self._changed(date) def _on_entry__activate(self, entry): self.emit('activate') def _on_entry__scroll_event(self, entry, event): if event.direction == gdk.SCROLL_UP: days = 1 elif event.direction == gdk.SCROLL_DOWN: days = -1 else: return try: date = self.get_date() except ValidationError: date = None if not date: newdate = datetime.date.today() else: newdate = date + datetime.timedelta(days=days) self.set_date(newdate) def _on_button__toggled(self, button): if self._popping_down: return try: date = self.get_date() except ValidationError: date = None self._popup.popup(date) def _on_popup__hide(self, popup): self._popping_down = True self._button.set_active(False) self._popping_down = False def _on_popup__date_selected(self, popup, date): self.set_date(date) popup.popdown() self.entry.grab_focus() self.entry.set_position(len(self.entry.get_text())) self._changed(date) def _changed(self, date): if self._old_date != date: self.emit('changed') self._old_date = date # Public API def set_date(self, date): """Sets the date. @param date: date to set @type date: a datetime.date instance or None """ if not isinstance(date, datetime.date) and date is not None: raise TypeError( "date must be a datetime.date instance or None, not %r" % ( date,)) if date is None: value = '' else: value = date_converter.as_string(date) self.entry.set_text(value) def get_date(self): """Get the selected date @returns: the date. @rtype: datetime.date or None """ try: date = self.entry.read() except ValidationError: date = None if date == ValueUnset: date = None return date
class TestSearchEntryGadget(GUITest): def _create_interface(self, run_editor=None): self.sale = self.create_sale() self.window = gtk.Window() self.entry = ProxyEntry() self.window.add(self.entry) self.client_gadget = SearchEntryGadget(self.entry, self.store, model=self.sale, model_property='client', search_columns=['name'], search_class=ClientSearch, parent=self.window, run_editor=run_editor) self.client_gadget.get_model_obj = lambda obj: obj and obj.client def test_create(self): window = gtk.Window() box = gtk.VBox() window.add(box) entry = ProxyEntry() box.pack_start(entry) self.check_dialog(window, 'search-entry-before-replace') sale = self.create_sale() SearchEntryGadget(entry, self.store, model=sale, model_property='client', search_columns=['name'], search_class=ClientSearch, parent=window) self.check_dialog(window, 'search-entry-after-replace') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_run_search(self, run_dialog): self._create_interface() run_dialog.return_value = None self.click(self.client_gadget.find_button) run_dialog.assert_called_once_with(ClientSearch, self.window, self.store, initial_string='', double_click_confirm=True) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_person_role_dialog') def test_run_editor(self, run_dialog, new_store): new_store.return_value = self.store self._create_interface() client = self.create_client(name=u'Fulano de Tal') run_dialog.return_value = self.store.find( ClientView, ClientView.id == client.id).one() with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): self.click(self.client_gadget.edit_button) run_dialog.assert_called_once_with(ClientEditor, self.window, self.store, None) self.assertEquals(self.entry.read(), client) self.assertEquals(self.entry.get_text(), u'Fulano de Tal') @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') def test_run_editor_override(self, new_store): new_store.return_value = self.store run_editor = mock.MagicMock() run_editor.return_value = None self.assertEquals(run_editor.call_count, 0) self._create_interface(run_editor=run_editor) with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): self.click(self.client_gadget.edit_button) self.assertEquals(run_editor.call_count, 1) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_entry_activate(self, run_dialog, new_store): new_store.return_value = self.store self._create_interface() fulano = self.create_client(u'Fulano de Tal') ciclano = self.create_client(u'Cicrano de Tal') # There should be only one match for Fulano, then the entry should be # updated with this only match self.entry.set_text('Fulano') self.entry.activate() self.assertEquals(self.entry.get_text(), 'Fulano de Tal') self.assertEquals(self.entry.read(), fulano) # Now when we use 'de tal', there are two clients that match. The # search should be displayed run_dialog.return_value = self.store.find( ClientView, ClientView.id == ciclano.id).one() self.entry.set_text('de tal') self.entry.activate() run_dialog.assert_called_once_with(ClientSearch, self.window, self.store, initial_string='de tal', double_click_confirm=True) self.assertEquals(self.entry.get_text(), 'Cicrano de Tal') self.assertEquals(self.entry.read(), ciclano) @mock.patch('stoqlib.gui.widgets.searchentry.api.new_store') @mock.patch('stoqlib.gui.widgets.searchentry.run_dialog') def test_with_cfop(self, run_dialog, new_store): new_store.return_value = self.store window = gtk.Window() entry = ProxyEntry() window.add(entry) sale = self.create_sale() gadget = SearchEntryGadget(entry, self.store, model=sale, model_property='cfop', search_columns=['name'], search_class=CfopSearch, parent=window) with mock.patch.object(self.store, 'commit'): with mock.patch.object(self.store, 'close'): run_dialog.return_value = None self.click(gadget.edit_button) run_dialog.assert_called_once_with(CfopEditor, window, self.store, sale.cfop)
class DateEntry(gtk.HBox): """I am an entry which you can input a date on. In addition to an gtk.Entry I also contain a button with an arrow you can click to get popup window with a gtk.Calendar for which you can use to select the date """ gsignal('changed') gsignal('activate') def __init__(self): gtk.HBox.__init__(self) self._popping_down = False self._old_date = None # bootstrap problems, kiwi.ui.widgets.entry imports dateentry # we need to use a proxy entry because we want the mask from kiwi.ui.widgets.entry import ProxyEntry self.entry = ProxyEntry() self.entry.connect('changed', self._on_entry__changed) self.entry.connect('activate', self._on_entry__activate) self.entry.set_property('data-type', datetime.date) mask = self.entry.get_mask() if mask: self.entry.set_width_chars(len(mask)) self.pack_start(self.entry, False, False) self.entry.show() self._button = gtk.ToggleButton() self._button.connect('scroll-event', self._on_entry__scroll_event) self._button.connect('toggled', self._on_button__toggled) self._button.set_focus_on_click(False) self.pack_start(self._button, False, False) self._button.show() arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE) self._button.add(arrow) arrow.show() self._popup = _DateEntryPopup(self) self._popup.connect('date-selected', self._on_popup__date_selected) self._popup.connect('hide', self._on_popup__hide) self._popup.set_size_request(-1, 24) # Virtual methods def do_grab_focus(self): self.entry.grab_focus() # Callbacks def _on_entry__changed(self, entry): try: date = self.get_date() except ValidationError: date = None self._changed(date) def _on_entry__activate(self, entry): self.emit('activate') def _on_entry__scroll_event(self, entry, event): if event.direction == gdk.SCROLL_UP: days = 1 elif event.direction == gdk.SCROLL_DOWN: days = -1 else: return try: date = self.get_date() except ValidationError: date = None if not date: newdate = datetime.date.today() else: newdate = date + datetime.timedelta(days=days) self.set_date(newdate) def _on_button__toggled(self, button): if self._popping_down: return try: date = self.get_date() except ValidationError: date = None self._popup.popup(date) def _on_popup__hide(self, popup): self._popping_down = True self._button.set_active(False) self._popping_down = False def _on_popup__date_selected(self, popup, date): self.set_date(date) popup.popdown() self.entry.grab_focus() self.entry.set_position(len(self.entry.get_text())) self._changed(date) def _changed(self, date): if self._old_date != date: self.emit('changed') self._old_date = date # Public API def set_date(self, date): """Sets the date. @param date: date to set @type date: a datetime.date instance or None """ if not isinstance(date, datetime.date) and date is not None: raise TypeError( "date must be a datetime.date instance or None, not %r" % (date, )) if date is None: value = '' else: value = date_converter.as_string(date) self.entry.set_text(value) def get_date(self): """Get the selected date @returns: the date. @rtype: datetime.date or None """ try: date = self.entry.read() except ValidationError: date = None if date == ValueUnset: date = None return date