Esempio n. 1
0
 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  ')
Esempio n. 2
0
 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'])
Esempio n. 3
0
 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'])
Esempio n. 4
0
 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'])
Esempio n. 5
0
 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'])
Esempio n. 6
0
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()
Esempio n. 7
0
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()
Esempio n. 8
0
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()
Esempio n. 9
0
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
Esempio n. 10
0
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)
Esempio n. 11
0
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
Esempio n. 12
0
 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')
Esempio n. 13
0
 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')
Esempio n. 14
0
 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')
Esempio n. 15
0
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()
Esempio n. 16
0
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
Esempio n. 17
0
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)
Esempio n. 18
0
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