Source code for stoqlib.gui.widgets.calculator
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2013 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
import decimal
import operator
import gtk
from kiwi.currency import currency
from kiwi.datatypes import converter, ValidationError
from kiwi.ui.widgets.entry import ProxyEntry
from kiwi.ui.popup import PopupWindow
import pango
from stoqlib.gui.stockicons import STOQ_CALC
from stoqlib.lib.translation import stoqlib_gettext as _
[docs]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
#
[docs] 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.widget.get_property('xalign') > 0.5:
self._icon_pos = 'secondary-icon'
else:
self._icon_pos = 'primary-icon'
self.widget.set_property(self._icon_pos + '-activatable', True)
self.widget.set_property(self._icon_pos + '-tooltip-text',
_("Do calculations on top of this value"))
self.widget.connect('notify::sensitive',
self._on_entry_sensitive__notify)
self.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.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)
return vbox
[docs] def validate_popup(self):
try:
self._new_value = self._data_type(self.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
#
# 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.ICON_SIZE_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.widget.update(self._new_value)
self.popdown()
def _toggle_calculator_icon(self):
if self.widget.get_sensitive():
pixbuf = self.render_icon(STOQ_CALC, gtk.ICON_SIZE_MENU)
else:
pixbuf = None
self.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.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.ENTRY_ICON_SECONDARY:
return
self.popup()