From 4e360524f44e98c50e590fb675e04186eb7c1adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 26 Jan 2015 11:31:24 +0100 Subject: [PATCH] Simplify date, time and datetime widgets - Use parse of dateutil - Remove datetime widget in list view - Allow to use widget attribute for date/time issue4457 review12311003 --- CHANGELOG | 2 + tryton/action/main.py | 3 + tryton/common/cellrendererdate.py | 119 ---- tryton/common/common.py | 2 +- tryton/common/date_widget.py | 340 ----------- tryton/common/datetime_.py | 561 ++++++++++++++++++ tryton/common/domain_parser.py | 69 ++- tryton/gui/window/form.py | 11 +- tryton/gui/window/revision.py | 66 ++- tryton/gui/window/view_form/model/field.py | 68 +-- tryton/gui/window/view_form/view/form.py | 4 +- .../view_form/view/form_gtk/calendar.py | 133 +++-- .../view_form/view/form_gtk/dictionary.py | 64 +- .../window/view_form/view/graph_gtk/graph.py | 7 +- tryton/gui/window/view_form/view/list.py | 3 +- .../view_form/view/list_gtk/editabletree.py | 16 +- .../window/view_form/view/list_gtk/widget.py | 56 +- .../window/view_form/view/screen_container.py | 58 +- tryton/translate.py | 12 - 19 files changed, 902 insertions(+), 692 deletions(-) delete mode 100644 tryton/common/cellrendererdate.py delete mode 100644 tryton/common/date_widget.py create mode 100644 tryton/common/datetime_.py diff --git a/CHANGELOG b/CHANGELOG index ffb105759b..c001b1066b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Improve date/time widgets +* Remove datetime widget on list/tree view * Allow to put many times the same field on tree view * Add search completion on dictionary widget * Merge host and port field in profile editor diff --git a/tryton/action/main.py b/tryton/action/main.py index 2c78e4a5a3..36ee40b22c 100644 --- a/tryton/action/main.py +++ b/tryton/action/main.py @@ -114,6 +114,9 @@ class Action(object): ctx.update(action_ctx) ctx.update(context) action_ctx.update(context) + if 'date_format' not in action_ctx: + action_ctx['date_format'] = rpc.CONTEXT.get( + 'locale', {}).get('date', '%x') ctx['context'] = ctx decoder = PYSONDecoder(ctx) diff --git a/tryton/common/cellrendererdate.py b/tryton/common/cellrendererdate.py deleted file mode 100644 index 6453c4fb2f..0000000000 --- a/tryton/common/cellrendererdate.py +++ /dev/null @@ -1,119 +0,0 @@ -# This file is part of Tryton. The COPYRIGHT file at the top level of -# this repository contains the full copyright notices and license terms. -import gtk -import gobject -import pango - -from date_widget import DateEntry - - -class CellRendererDate(gtk.GenericCellRenderer): - __gproperties__ = { - 'text': (gobject.TYPE_STRING, None, 'Text', - 'Text', gobject.PARAM_READWRITE), - 'foreground': (gobject.TYPE_STRING, None, 'Foreground', - 'Foreground', gobject.PARAM_WRITABLE), - 'foreground-set': (gobject.TYPE_INT, 'Foreground Set', - 'Foreground Set', 0, 10, 0, gobject.PARAM_READWRITE), - 'background': (gobject.TYPE_STRING, None, 'Background', - 'Background', gobject.PARAM_WRITABLE), - 'background-set': (gobject.TYPE_INT, 'Background Set', - 'Background Set', 0, 10, 0, gobject.PARAM_READWRITE), - 'editable': (gobject.TYPE_INT, 'Editable', - 'Editable', 0, 10, 0, gobject.PARAM_READWRITE), - 'visible': (gobject.TYPE_INT, 'Visible', - 'Visible', 0, 10, 0, gobject.PARAM_READWRITE), - 'strikethrough': (gobject.TYPE_BOOLEAN, 'Strikethrough', - 'Strikethrough', False, gobject.PARAM_WRITABLE), - } - - def __init__(self, format): - self.__gobject_init__() - self._renderer = gtk.CellRendererText() - self.set_property("mode", self._renderer.get_property("mode")) - - self.format = format - self.cmd = '' - self.text = self._renderer.get_property('text') - self.editable = self._renderer.get_property('editable') - self.visible = True - - def set_sensitive(self, value): - if hasattr(self._renderer, 'set_sensitive'): - return self._renderer.set_sensitive(value) - return self._renderer.set_property('sensitive', value) - - def do_set_property(self, pspec, value): - setattr(self, pspec.name, value) - if pspec.name == 'visible': - return - self._renderer.set_property(pspec.name, value) - self.set_property("mode", self._renderer.get_property("mode")) - - def do_get_property(self, pspec): - return getattr(self, pspec.name) - - def on_get_size(self, widget, cell_area): - return self._renderer.get_size(widget, cell_area) - - def on_render(self, window, widget, background_area, cell_area, - expose_area, flags): - if not self.visible: - return - # Handle Pixmap window as pygtk failed - if type(window) == gtk.gdk.Pixmap: - layout = widget.create_pango_layout(self.text) - layout.set_font_description(widget.style.font_desc) - w, h = layout.get_size() - xalign = self._renderer.get_property('xalign') - x = int(cell_area.x + (cell_area.width - w / pango.SCALE) * xalign) - y = int(cell_area.y + (cell_area.height - h / pango.SCALE) / 2) - window.draw_layout(widget.style.text_gc[0], x, y, layout) - return - return self._renderer.render(window, widget, background_area, - cell_area, expose_area, flags) - - def on_activate(self, event, widget, path, background_area, cell_area, - flags): - if not self.visible: - return - return self._renderer.activate(event, widget, path, background_area, - cell_area, flags) - - def on_start_editing(self, event, widget, path, background_area, - cell_area, flags): - if not self.visible: - return - editable = DateEntry(self.format) - editable.set_property('shadow-type', gtk.SHADOW_NONE) - - colormap = editable.get_colormap() - style = editable.get_style() - if hasattr(self, 'background') \ - and getattr(self, 'background') != 'white': - bg_color = colormap.alloc_color(getattr(self, 'background')) - fg_color = gtk.gdk.color_parse("black") - editable.modify_bg(gtk.STATE_ACTIVE, bg_color) - editable.modify_base(gtk.STATE_NORMAL, bg_color) - editable.modify_fg(gtk.STATE_NORMAL, fg_color) - editable.modify_text(gtk.STATE_NORMAL, fg_color) - editable.modify_text(gtk.STATE_INSENSITIVE, fg_color) - else: - editable.modify_bg(gtk.STATE_ACTIVE, style.bg[gtk.STATE_ACTIVE]) - editable.modify_base(gtk.STATE_NORMAL, - style.base[gtk.STATE_NORMAL]) - editable.modify_fg(gtk.STATE_NORMAL, style.fg[gtk.STATE_NORMAL]) - editable.modify_text(gtk.STATE_NORMAL, - style.text[gtk.STATE_NORMAL]) - editable.modify_text(gtk.STATE_INSENSITIVE, - style.text[gtk.STATE_INSENSITIVE]) - - if self.text: - editable.set_text(self.text) - else: - editable.clear() - editable.grab_focus() - editable.show() - return editable - -gobject.type_register(CellRendererDate) diff --git a/tryton/common/common.py b/tryton/common/common.py index 60245371b5..26969ef0b1 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -1501,7 +1501,7 @@ def timezoned_date(date, reverse=False): szone = dateutil.tz.tzutc() if reverse: lzone, szone = szone, lzone - return date.replace(tzinfo=szone).astimezone(lzone) + return date.replace(tzinfo=szone).astimezone(lzone).replace(tzinfo=None) def untimezoned_date(date): diff --git a/tryton/common/date_widget.py b/tryton/common/date_widget.py deleted file mode 100644 index 78d32a5d40..0000000000 --- a/tryton/common/date_widget.py +++ /dev/null @@ -1,340 +0,0 @@ -# This file is part of Tryton. The COPYRIGHT file at the top level of -# this repository contains the full copyright notices and license terms. -"Date Widget" - -import gobject -import gtk -import re -import time -import datetime -import gettext - -from dateutil.relativedelta import relativedelta - -from datetime_strftime import datetime_strftime -from common import TRYTON_ICON - -_ = gettext.gettext - -__all__ = ['DateEntry'] - -MAPPING = { - '%y': ('__', '([_ 0-9][_ 0-9])'), - '%Y': ('____', '([_ 0-9][_ 0-9][_ 0-9][_ 0-9])'), - '%m': ('__', '([_ 0-9][_ 0-9])'), - '%d': ('__', '([_ 0-9][_ 0-9])'), - '%H': ('__', '([_ 0-9][_ 0-9])'), - '%I': ('__', '([_ 0-9][_ 0-9])'), - '%M': ('__', '([_ 0-9][_ 0-9])'), - '%S': ('__', '([_ 0-9][_ 0-9])'), - '%p': ('__', '([_ AP][_ M])'), - '%f': ('______', '([_0-9][_0-9][_0-9][_0-9][_0-9][_0-9])'), - } -OPERATORS = { - gtk.keysyms.S: relativedelta(seconds=-1), - gtk.keysyms.s: relativedelta(seconds=1), - gtk.keysyms.I: relativedelta(minutes=-1), - gtk.keysyms.i: relativedelta(minutes=1), - gtk.keysyms.H: relativedelta(hours=-1), - gtk.keysyms.h: relativedelta(hours=1), - gtk.keysyms.D: relativedelta(days=-1), - gtk.keysyms.d: relativedelta(days=1), - gtk.keysyms.W: relativedelta(weeks=-1), - gtk.keysyms.w: relativedelta(weeks=1), - gtk.keysyms.M: relativedelta(months=-1), - gtk.keysyms.m: relativedelta(months=1), - gtk.keysyms.Y: relativedelta(years=-1), - gtk.keysyms.y: relativedelta(years=1), - } - - -class DateEntry(gtk.Entry): - - def __init__(self, format): - super(DateEntry, self).__init__() - - self.set_format(format) - - self.connect('key-press-event', self._on_key_press) - self.connect('insert-text', self._on_insert_text) - self.connect('delete-text', self._on_delete_text) - - self.connect('focus-in-event', self._focus_in) - self.connect('focus-out-event', self._focus_out) - - self.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, 'tryton-find') - self.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, - _('Open the calendar ')) - self.connect('icon-press', DateEntry.cal_open) - - self._interactive_input = True - self.idle_set_position(0) - - def set_format(self, format): - self.format = format - self.regex = self.initial_value = format - for key, val in MAPPING.items(): - self.regex = self.regex.replace(key, val[1]) - self.initial_value = self.initial_value.replace(key, val[0]) - - self.regex = re.compile(self.regex) - - assert self.regex.match(self.initial_value), \ - 'Error, the initial value should be validated by regex' - self.set_width_chars(len(self.initial_value) + 3) # space for icon - self.set_max_length(len(self.initial_value)) - - def idle_set_position(self, value): - def idle_func(): - with gtk.gdk.lock: - self.set_position(value) - return False - gobject.idle_add(idle_func) - - def _on_insert_text(self, editable, value, length, position): - if not self._interactive_input: - return - - pos = self.get_position() - - text = self.get_text() - if not text: - text = self.initial_value - if text == self.initial_value and pos >= len(self.initial_value): - pos = 0 - - for char in value: - if pos >= len(self.initial_value): - continue - if char not in ('_', ' ') and char in self.initial_value[pos:]: - pos += self.initial_value[pos:].index(char) - else: - while self.initial_value[pos] not in ('_', ' '): - pos += 1 - text = text[:pos] + char + text[pos + 1:] - pos += 1 - - if self.regex.match(text) and self.test_date(text): - self.set_text(text) - self.idle_set_position(pos) - self.stop_emission('insert-text') - self.show() - return - - def _on_delete_text(self, editable, start, end): - if not self._interactive_input: - return - - if start > len(self.initial_value): - start = len(self.initial_value) - if start < 0: - start = 0 - if end > len(self.initial_value): - end = len(self.initial_value) - if end < 0: - end = 0 - while (start > 0 - and self.initial_value[start] not in ['_', ' ', '0', 'X']): - start -= 1 - text = self.get_text() - text = text[:start] + self.initial_value[start:end] + text[end:] - self.set_text(text) - self.idle_set_position(start) - self.stop_emission('delete-text') - return - - def _focus_in(self, editable, event): - if not self.get_text(): - self.set_text(self.initial_value) - - def _focus_out(self, editable, event): - if self.get_text() == self.initial_value or not self.date_get(): - self.set_text('') - - def set_text(self, text): - self._interactive_input = False - try: - # Force position as _on_insert_text use it - position = self.get_position() - self.set_position(0) - gtk.Entry.set_text(self, text) - self.idle_set_position(position) - finally: - self._interactive_input = True - - def date_set(self, dt): - if dt: - self.set_text(datetime_strftime(dt, self.format)) - else: - if self.is_focus(): - self.set_text(self.initial_value) - else: - self.set_text('') - - def compute_date(self, text, default=None, year=None): - if default is None: - default = datetime_strftime(datetime.datetime.now(), self.format) - if text == self.initial_value or not text: - return '' - - match = self.regex.match(text) - if not match: - return '' - for i in range(len(match.groups())): - val = match.group(i + 1) - n = len(val) - val = val.strip() - val = val.strip('_') - if n != 4: - val = val.lstrip('0') - if not val: - continue - fchar = '0' - if n == 4: - fchar = '_' - if len(val) == 1: - val = '0' + val - if year and n == 4 and len(val) != 4: - val = year - val = (fchar * (n - len(val))) + val - start = match.start(i + 1) - end = match.end(i + 1) - text = text[:start] + val + text[end:] - - for a in range(len(self.initial_value)): - if self.initial_value[a] == text[a]: - text = text[:a] + default[a] + text[a + 1:] - return text - - def test_date(self, text): - default = datetime_strftime(datetime.datetime(2000, 1, 1), - self.format) - try: - time.strptime( - self.compute_date(text, default=default, year='2000'), - self.format) - except ValueError: - return False - return True - - def date_get(self, set_text=True): - res = None - date = self.compute_date(self.get_text()) - try: - res = datetime.datetime.strptime(date, self.format) - except ValueError: - return None - if set_text: - self.set_text(date) - return res - - def delete_text(self, start, end): - self._interactive_input = False - try: - gtk.Entry.delete_text(self, start, end) - finally: - self._interactive_input = True - - def insert_text(self, text, position=0): - self._interactive_input = False - try: - gtk.Entry.insert_text(self, text, position) - finally: - self._interactive_input = True - - def clear(self): - if self.is_focus(): - self.set_text(self.initial_value) - else: - self.set_text('') - - def _on_key_press(self, editable, event): - if not self.get_editable(): - return False - if event.keyval in (gtk.keysyms.KP_Equal, gtk.keysyms.equal): - now = datetime.datetime.now() - self.date_set(now) - self.stop_emission("key-press-event") - return True - elif event.keyval == gtk.keysyms.F2: - DateEntry.cal_open(editable) - self.stop_emission("key-press-event") - return True - elif event.keyval in OPERATORS: - date = self.date_get() - if date: - self.date_set(date + OPERATORS[event.keyval]) - self.stop_emission("key-press-event") - return True - return False - - def cal_open(self, *args): - if not self.get_editable(): - return False - parent = self.get_toplevel() - dialog = gtk.Dialog(_('Date Selection'), parent, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OK, gtk.RESPONSE_OK)) - dialog.set_has_separator(True) - dialog.set_icon(TRYTON_ICON) - dialog.set_default_response(gtk.RESPONSE_OK) - - calendar = gtk.Calendar() - calendar.set_display_options( - gtk.CALENDAR_SHOW_HEADING | - gtk.CALENDAR_SHOW_DAY_NAMES | - gtk.CALENDAR_SHOW_WEEK_NUMBERS | - gtk.CALENDAR_WEEK_START_MONDAY) - calendar.connect('day-selected-double-click', - lambda *x: dialog.response(gtk.RESPONSE_OK)) - dialog.vbox.pack_start(calendar, expand=True, fill=True) - - date = self.date_get() - if date: - calendar.select_month(date.month - 1, date.year) - calendar.select_day(date.day) - else: - date = datetime.datetime.now() - - x, y = self.window.get_origin() - allocation = self.get_allocation() - dialog.move(x, y + allocation.height) - - dialog.show_all() - - response = dialog.run() - if response == gtk.RESPONSE_OK: - year, month, day = calendar.get_date() - date += relativedelta(year=year, month=month + 1, day=day) - self.date_set(date) - - parent.present() - self.grab_focus() - dialog.destroy() - -if __name__ == '__main__': - win = gtk.Window() - win.set_title('DateEntry') - - def cb(window, event): - gtk.main_quit() - win.connect('delete-event', cb) - vbox = gtk.VBox() - win.add(vbox) - - for format in ( - '%d.%m.%Y %H:%M:%S', - '%m/%d/%Y %H:%M:%S', - '%d/%m/%Y %H:%M:%S', - '%Y-%m-%d %H:%M%S', - '%d.%m.%Y', - '%m/%d/%Y', - '%d/%m/%Y', - '%Y-%m-%d', - ): - widget = DateEntry(format) - vbox.pack_start(widget, False, False) - - win.show_all() - gtk.main() diff --git a/tryton/common/datetime_.py b/tryton/common/datetime_.py new file mode 100644 index 0000000000..8552ad4500 --- /dev/null +++ b/tryton/common/datetime_.py @@ -0,0 +1,561 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +import gettext +import datetime + +import pygtk + +pygtk.require('2.0') + +import gobject +import gtk + +from dateutil.relativedelta import relativedelta +from dateutil.parser import parse + +from datetime_strftime import datetime_strftime as strftime + +__all__ = ['Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime'] + +_ = gettext.gettext + + +def date_parse(text, format_='%x'): + dayfirst = datetime.date(1988, 8, 16).strftime(format_).index('16') == 0 + yearfirst = datetime.date(1988, 8, 16).strftime(format_).index('88') <= 2 + return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True) + + +class Date(gtk.Entry): + __gtype_name__ = 'Date' + __gproperties__ = { + 'value': (gobject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + 'format': (gobject.TYPE_STRING, + '%x', + _('Format'), + _('Display format'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + } + __gsignals__ = { + 'date-changed': (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, + gobject.TYPE_NONE, ()), + } + + def __init__(self): + self.__date = None + self.__format = '%x' + + gtk.Entry.__init__(self) + + self.set_width_chars(12) + + self.connect('focus-out-event', self.focus_out) + self.connect('activate', self.activate) + + # Calendar Popup + self.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, 'tryton-find') + self.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, + _('Open the calendar')) + self.connect('icon-press', self.icon_press) + + self.__cal_popup = gtk.Window(gtk.WINDOW_POPUP) + self.__cal_popup.set_events( + self.__cal_popup.get_events() | gtk.gdk.KEY_PRESS_MASK) + self.__cal_popup.set_resizable(False) + self.__cal_popup.connect('delete-event', self.cal_popup_closed) + self.__cal_popup.connect('key-press-event', self.cal_popup_key_pressed) + self.__cal_popup.connect('button-press-event', + self.cal_popup_button_pressed) + + self.__calendar = gtk.Calendar() + cal_options = gtk.CALENDAR_SHOW_DAY_NAMES | gtk.CALENDAR_SHOW_HEADING + self.__calendar.set_display_options(cal_options) + self.__cal_popup.add(self.__calendar) + self.__calendar.connect('day-selected', self.cal_popup_changed) + self.__calendar.connect('day-selected-double-click', + self.cal_popup_double_click) + self.__calendar.show() + + def parse(self): + text = self.get_text() + date = None + if text: + try: + date = date_parse(text, self.__format).date() + except ValueError: + pass + + self.__date = date + + def update_label(self): + if not self.__date: + self.set_text('') + return + self.set_text(strftime(self.__date, self.__format)) + + def icon_press(self, entry, icon_pos, event): + if icon_pos == gtk.ENTRY_ICON_SECONDARY: + self.cal_popup_open() + + def cal_popup_open(self): + self.parse() + if self.__date: + self.__calendar.select_month( + self.__date.month - 1, self.__date.year) + self.__calendar.select_day(self.__date.day) + popup_position(self, self.__cal_popup) + popup_show(self.__cal_popup) + + def cal_popup_changed(self, calendar): + year, month, day = self.__calendar.get_date() + self.__date = datetime.date(year, month + 1, day) + + self.update_label() + + self.emit('date-changed') + + def cal_popup_double_click(self, calendar): + self.cal_popup_hide() + + def cal_popup_key_pressed(self, calendar, event): + if event.keyval != gtk.keysyms.Escape: + return False + + self.stop_emission('key-press-event') + self.cal_popup_hide() + return True + + def cal_popup_button_pressed(self, calendar, event): + child = event.window + if child != calendar.window: + while child: + if child == calendar.window: + return False + child = child.get_parent() + self.cal_popup_hide() + return True + + def cal_popup_closed(self, popup): + self.cal_popup_hide() + return True + + def cal_popup_hide(self): + popup_hide(self.__cal_popup) + self.grab_focus() + self.emit('date-changed') + + def focus_out(self, entry, event): + self.parse() + self.update_label() + self.emit('date-changed') + return False + + def activate(self, entry): + self.parse() + self.update_label() + self.emit('date-changed') + return False + + def do_set_property(self, prop, value): + if prop.name == 'value': + if isinstance(value, basestring): + self.set_text(value) + self.parse() + value = self.__date + if value: + if isinstance(value, datetime.datetime): + value = value.date() + assert isinstance(value, datetime.date), value + self.__date = value + self.update_label() + elif prop.name == 'format': + self.__format = value + self.update_label() + + def do_get_property(self, prop): + if prop.name == 'value': + return self.__date + elif prop.name == 'format': + return self.__format + +gobject.type_register(Date) + + +class CellRendererDate(gtk.CellRendererText): + __gproperties__ = { + 'format': (gobject.TYPE_STRING, + _('Format'), + _('Display format'), + '%x', + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + } + + def __init__(self): + self.__format = '%x' + + gtk.CellRendererText.__init__(self) + + def do_set_property(self, prop, value): + if prop.name == 'format': + self.__format = value + return + gtk.CellRendererText.set_property(self, prop, value) + + def do_get_property(self, prop): + if prop.name == 'format': + return self.__format + return gtk.CellRendererText.get_property(self, prop) + + def do_start_editing( + self, event, widget, path, background_area, cell_area, flags): + widget = add_operators(Date()) # TODO add_operators has option + widget.props.format = self.props.format + widget.props.value = self.props.text + if self.props.background_set: + widget.modify_base(gtk.STATE_NORMAL, self.props.background_gdk) + if self.props.foreground_set: + widget.modify_fg(gtk.STATE_NORMAL, self.props.foreground_gdk) + widget.grab_focus() + widget.show() + return widget + +gobject.type_register(CellRendererDate) + + +class Time(gtk.ComboBoxEntry): + __gtype_name__ = 'Time' + __gproperties__ = { + 'value': (gobject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + 'format': (gobject.TYPE_STRING, + _('Format'), + _('Display format'), + '%X', + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + } + __gsignals__ = { + 'time-changed': (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, + gobject.TYPE_NONE, ()), + } + + def __init__(self): + self.__time = None + self.__format = '%X' + + gtk.ComboBoxEntry.__init__(self) + + self.__entry = self.get_child() + self.__entry.set_width_chars(10) + + self.__entry.connect('focus-out-event', self.focus_out) + self.__entry.connect('activate', self.activate) + self.connect('changed', self.changed) + + self.__model = gtk.ListStore( + gobject.TYPE_STRING, gobject.TYPE_PYOBJECT) + self.update_model() + self.set_model(self.__model) + self.set_text_column(0) + + def parse(self): + text = self.__entry.get_text() + time = None + if text: + try: + time = date_parse(text).time() + except ValueError: + pass + + self.__time = time + + def update_label(self): + if not self.__time: + self.__entry.set_text('') + return + + self.__entry.set_text(self.__time.strftime(self.__format)) + + def update_model(self): + self.__model.clear() + timelist_set_list( + self.__model, datetime.time(0, 0), datetime.time(23, 59), + self.__format) + + def focus_out(self, entry, event): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def activate(self, entry): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def changed(self, combobox): + # "changed" signal is also triggered by text editing + # so only parse when a row is active + if combobox.get_active_iter(): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def do_set_property(self, prop, value): + if prop.name == 'value': + if isinstance(value, basestring): + self.__entry.set_text(value) + self.parse() + value = self.__time + if value: + if isinstance(value, datetime.datetime): + value = value.time() + self.__time = value + self.update_label() + elif prop.name == 'format': + self.__format = value + self.update_label() + self.update_model() + + def do_get_property(self, prop): + if prop.name == 'value': + return self.__time + elif prop.name == 'format': + return self.__format + +gobject.type_register(Time) + + +class CellRendererTime(gtk.CellRendererText): + __gproperties__ = { + 'format': (gobject.TYPE_STRING, + '%X', + _('Format'), + _('Display format'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + } + + def __init__(self): + self.__format = '%X' + + gtk.CellRendererText.__init__(self) + + def do_set_property(self, prop, value): + if prop.name == 'format': + self.__format = value + return + gtk.CellRendererText.set_property(self, prop, value) + + def do_get_property(self, prop): + if prop.name == 'format': + return self.__format + return gtk.CellRendererText.get_property(self, prop) + + def do_start_editing( + self, event, widget, path, background_area, cell_area, flags): + if not self.props.editable: + return + widget = add_operators(Time()) # TODO add_operators has option + widget.props.format = self.props.format + widget.props.value = self.props.text + if self.props.background_set: + widget.modify_base(gtk.STATE_NORMAL, self.props.background_gdk) + if self.props.foreground_set: + widget.modify_fg(gtk.STATE_NORMAL, self.props.foreground_gdk) + widget.grab_focus() + widget.show() + return widget + +gobject.type_register(CellRendererTime) + + +class DateTime(gtk.HBox): + __gtype_name__ = 'DateTime' + __gproperties__ = { + 'value': (gobject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + 'date-format': (gobject.TYPE_STRING, + '%x', + _('Date Format'), + _('Displayed date format'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + 'time-format': (gobject.TYPE_STRING, + '%X', + _('Date Format'), + _('Displayed date format'), + gobject.PARAM_READABLE | gobject.PARAM_WRITABLE), + } + __gsignals__ = { + 'datetime-changed': (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, + gobject.TYPE_NONE, ()), + } + + def __init__(self): + gtk.HBox.__init__(self, spacing=4) + + self.__date = Date() + self.pack_start(self.__date, True, True, 0) + self.__date.show() + self.__date.connect('date-changed', + lambda e: self.emit('datetime-changed')) + + self.__time = Time() + self.pack_start(self.__time, True, True, 0) + self.__time.show() + self.__time.connect('time-changed', + lambda e: self.emit('datetime-changed')) + + def do_set_property(self, prop, value): + if prop.name == 'value': + self.__date.props.value = value + self.__time.props.value = value + elif prop.name == 'date-format': + self.__date.props.format = value + elif prop.name == 'time-format': + self.__time.props.format = value + + def do_get_property(self, prop): + if prop.name == 'value': + date = self.__date.props.value + time = self.__time.props.value or datetime.time() + if date: + return datetime.datetime.combine(date, time) + else: + return + elif prop.name == 'date-format': + return self.__date.props.format + elif prop.name == 'time-format': + return self.__time.props.format + +gobject.type_register(DateTime) + + +def popup_position(widget, popup): + + req_width, req_height = popup.size_request() + x, y = widget.window.get_origin() + allocation = widget.get_allocation() + width = allocation.width + height = allocation.height + + x += width - req_width + y += height + + x = max(x, 0) + y = max(y, 0) + + popup.move(x, y) + + +def popup_show(popup): + popup.show() + popup.grab_focus() + popup.grab_add() + + cursor = gtk.gdk.Cursor(gtk.gdk.ARROW) + gtk.gdk.pointer_grab(popup.window, True, + (gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.POINTER_MOTION_MASK), + None, cursor, 0L) + + +def popup_hide(popup): + popup.hide() + popup.grab_remove() + gtk.gdk.pointer_ungrab(0L) + + +def timelist_set_list(model, min_, max_, format_): + time = min_ + delta = 30 + while time < max_: + model.append((time.strftime(format_), time)) + hour = time.hour + minute = time.minute + delta + hour += minute / 60 + if hour >= 24: + break + minute = minute % 60 + time = datetime.time(hour, minute) + + +def add_operators(widget): + def key_press(editable, event): + if event.keyval in OPERATORS: + value = widget.props.value + if value: + if isinstance(value, datetime.time): + value = datetime.datetime.combine( + datetime.date.today(), value) + try: + widget.props.value = value + OPERATORS[event.keyval] + except TypeError: + return False + return True + elif event.keyval in (gtk.keysyms.KP_Equal, gtk.keysyms.equal): + widget.props.value = datetime.datetime.now() + return True + return False + + if isinstance(widget, gtk.ComboBoxEntry): + editable = widget.get_child() + else: + editable = widget + editable.connect('key-press-event', key_press) + return widget + +OPERATORS = { + gtk.keysyms.S: relativedelta(seconds=-1), + gtk.keysyms.s: relativedelta(seconds=1), + gtk.keysyms.I: relativedelta(minutes=-1), + gtk.keysyms.i: relativedelta(minutes=1), + gtk.keysyms.H: relativedelta(hours=-1), + gtk.keysyms.h: relativedelta(hours=1), + gtk.keysyms.D: relativedelta(days=-1), + gtk.keysyms.d: relativedelta(days=1), + gtk.keysyms.W: relativedelta(weeks=-1), + gtk.keysyms.w: relativedelta(weeks=1), + gtk.keysyms.M: relativedelta(months=-1), + gtk.keysyms.m: relativedelta(months=1), + gtk.keysyms.Y: relativedelta(years=-1), + gtk.keysyms.y: relativedelta(years=1), + } + +if __name__ == '__main__': + win = gtk.Window() + win.connect('delete-event', gtk.main_quit) + + v = gtk.VBox() + v.show() + + d = add_operators(Date()) + d.show() + v.pack_start(d, False, False) + + t = add_operators(Time()) + t.show() + v.pack_start(t, False, False) + + t = add_operators(Time()) + t.props.format = '%H:%M' + t.show() + v.pack_start(t, False, False) + + dt = DateTime() + [add_operators(c) for c in dt.get_children()] + dt.show() + v.pack_start(dt, False, False) + + win.add(v) + win.show() + + gtk.main() diff --git a/tryton/common/domain_parser.py b/tryton/common/domain_parser.py index 56991bb016..6179209c8e 100644 --- a/tryton/common/domain_parser.py +++ b/tryton/common/domain_parser.py @@ -7,12 +7,11 @@ import locale import decimal from decimal import Decimal import datetime -import time import io from collections import OrderedDict -from tryton.translate import date_format from tryton.common import untimezoned_date, timezoned_date, datetime_strftime +from tryton.common.datetime_ import date_parse from tryton.pyson import PYSONDecoder __all__ = ['DomainParser'] @@ -220,8 +219,10 @@ def test_split_target_value(): assert split_target_value(field, value) == result -def convert_value(field, value): +def convert_value(field, value, context=None): "Convert value for field" + if context is None: + context = {} def convert_boolean(): if isinstance(value, basestring): @@ -256,28 +257,31 @@ def convert_value(field, value): return value def convert_datetime(): + if not value: + return + format_ = context.get('date_format', '%X') + ' %x' try: - return untimezoned_date(datetime.datetime(*time.strptime(value, - date_format() + ' ' + time_format(field))[:6])) + dt = date_parse(value, format_) + if dt.time() == datetime.time.min: + return dt + return untimezoned_date(dt) except ValueError: - try: - return datetime.datetime(*time.strptime(value, - date_format())[:6]) - except ValueError: - return - except TypeError: return def convert_date(): + if not value: + return + format_ = context.get('date_format', '%x') try: - return datetime.date(*time.strptime(value, date_format())[:3]) + return date_parse(value, format_).date() except (ValueError, TypeError): return def convert_time(): + if not value: + return try: - return datetime.time(*time.strptime(value, - time_format(field))[3:6]) + return date_parse(value).time() except (ValueError, TypeError): return @@ -423,8 +427,10 @@ def test_convert_time(): assert convert_value(field, value) == result -def format_value(field, value, target=None): +def format_value(field, value, target=None, context=None): "Format value for field" + if context is None: + context = {} def format_boolean(): return _('True') if value else _('False') @@ -458,10 +464,10 @@ def format_value(field, value, target=None): def format_datetime(): if not value: return '' - format_ = date_format() + ' ' + time_format(field) + format_ = context.get('date_format', '%x') + ' ' + time_format(field) if (not isinstance(value, datetime.datetime) or value.time() == datetime.time.min): - format_ = date_format() + format_ = '%x' time = value else: time = timezoned_date(value) @@ -470,7 +476,8 @@ def format_value(field, value, target=None): def format_date(): if not value: return '' - return datetime_strftime(value, date_format()) + format_ = context.get('date_format', '%x') + return datetime_strftime(value, format_) def format_time(): if not value: @@ -495,7 +502,7 @@ def format_value(field, value, target=None): 'many2one': format_many2one, } if isinstance(value, (list, tuple)): - return ';'.join(format_value(field, x) for x in value) + return ';'.join(format_value(field, x, context=context) for x in value) return quote(converts.get(field['type'], lambda: value if value is not None else '')()) @@ -589,10 +596,10 @@ def test_format_datetime(): 'format': '"%H:%M:%S"', } for value, result in ( - (datetime.date(2002, 12, 4), '12/04/2002'), - (datetime.datetime(2002, 12, 4), '12/04/2002'), + (datetime.date(2002, 12, 4), '12/04/02'), + (datetime.datetime(2002, 12, 4), '12/04/02'), (untimezoned_date(datetime.datetime(2002, 12, 4, 12, 30)), - '"12/04/2002 12:30:00"'), + '"12/04/02 12:30:00"'), (False, ''), (None, ''), ): @@ -604,7 +611,7 @@ def test_format_date(): 'type': 'date', } for value, result in ( - (datetime.date(2002, 12, 4), '12/04/2002'), + (datetime.date(2002, 12, 4), '12/04/02'), (False, ''), (None, ''), ): @@ -807,13 +814,14 @@ def test_operatorize(): class DomainParser(object): "A parser for domain" - def __init__(self, fields): + def __init__(self, fields, context=None): self.fields = OrderedDict((name, f) for name, f in fields.iteritems() if f.get('searchable', True)) self.strings = dict((f['string'].lower(), f) for f in fields.itervalues() if f.get('searchable', True)) + self.context = context def parse(self, input_): "Return domain for the input string" @@ -895,7 +903,7 @@ class DomainParser(object): operator = '!' else: operator = '' - formatted_value = format_value(field, value, target) + formatted_value = format_value(field, value, target, self.context) if (operator in OPERATORS and field['type'] in ('char', 'text', 'selection') and value == ''): @@ -1090,20 +1098,21 @@ class DomainParser(object): 'datetime', 'date', 'time'): if value and '..' in value: lvalue, rvalue = value.split('..', 1) - lvalue = convert_value(field, lvalue) - rvalue = convert_value(field, rvalue) + lvalue = convert_value(field, lvalue, self.context) + rvalue = convert_value(field, rvalue, self.context) yield iter([ (field_name, '>=', lvalue), (field_name, '<=', rvalue), ]) continue if isinstance(value, list): - value = [convert_value(field, v) for v in value] + value = [convert_value(field, v, self.context) + for v in value] if field['type'] in ('many2one', 'one2many', 'many2many', 'one2one'): field_name += '.rec_name' else: - value = convert_value(field, value) + value = convert_value(field, value, self.context) if 'like' in operator: value = likify(value) if target: @@ -1196,7 +1205,7 @@ def test_string(): assert dom.string([]) == '' assert dom.string([('surname', 'ilike', '%Doe%')]) == '"(Sur)Name": Doe' assert dom.string([('date', '>=', datetime.date(2012, 10, 24))]) == \ - 'Date: >=10/24/2012' + 'Date: >=10/24/12' assert dom.string([('selection', '=', '')]) == 'Selection: ' assert dom.string([('selection', '=', None)]) == 'Selection: ' assert dom.string([('selection', '!=', '')]) == 'Selection: !""' diff --git a/tryton/gui/window/form.py b/tryton/gui/window/form.py index f657c4836c..6fcab9197f 100644 --- a/tryton/gui/window/form.py +++ b/tryton/gui/window/form.py @@ -15,7 +15,6 @@ from tryton.gui.window.revision import Revision from tryton.signal_event import SignalEvent from tryton.common import message, sur, sur_3b, COLOR_SCHEMES, timezoned_date import tryton.common as common -from tryton.translate import date_format from tryton.common import RPCExecute, RPCException from tryton.common.datetime_strftime import datetime_strftime from tryton import plugins @@ -253,9 +252,8 @@ class Form(SignalEvent, TabContent): value = str(line.get(key, False) or '/') if line.get(key, False) \ and key in ('create_date', 'write_date'): - display_format = date_format() + ' %H:%M:%S' date = timezoned_date(line[key]) - value = common.datetime_strftime(date, display_format) + value = common.datetime_strftime(date, '%X') message_str += val + ' ' + value + '\n' message_str += _('Model:') + ' ' + self.model message(message_str) @@ -272,7 +270,9 @@ class Form(SignalEvent, TabContent): except RPCException: return revision = self.screen.context.get('_datetime') - revision = Revision(revisions, revision).run() + format_ = self.screen.context.get('date_format', '%x') + format_ += ' %X.%f' + revision = Revision(revisions, revision, format_).run() # Prevent too old revision in form view if (self.screen.current_view.view_type == 'form' and revision @@ -294,7 +294,8 @@ class Form(SignalEvent, TabContent): def update_revision(self): revision = self.screen.context.get('_datetime') if revision: - format_ = date_format() + ' %H:%M:%S.%f' + format_ = self.screen.context.get('date_format', '%x') + format_ += ' %X.%f' revision = datetime_strftime(revision, format_) self.title.set_label('%s @ %s' % (self.name, revision)) else: diff --git a/tryton/gui/window/revision.py b/tryton/gui/window/revision.py index 5528bf2e01..483696dd85 100644 --- a/tryton/gui/window/revision.py +++ b/tryton/gui/window/revision.py @@ -5,9 +5,8 @@ import gettext from tryton.config import TRYTON_ICON from tryton.common import get_toplevel_window -from tryton.common.date_widget import DateEntry from tryton.common.datetime_strftime import datetime_strftime -from tryton.translate import date_format +from tryton.common.datetime_ import date_parse _ = gettext.gettext @@ -15,7 +14,7 @@ _ = gettext.gettext class Revision(object): 'Ask revision' - def __init__(self, revisions, revision=None): + def __init__(self, revisions, revision=None, format_='%x %X.%f'): self.parent = get_toplevel_window() self.win = gtk.Dialog(_('Revision'), self.parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, @@ -30,45 +29,74 @@ class Revision(object): self.win.vbox.pack_start(gtk.HSeparator()) hbox = gtk.HBox(spacing=3) hbox.pack_start(gtk.Label(_('Revision:')), expand=True, fill=True) - format_ = date_format() + ' %H:%M:%S.%f' - self.entry = DateEntry(format_) + list_store = gtk.ListStore(str, str) + combobox = gtk.ComboBoxEntry(list_store) + self.entry = combobox.child + self.entry.connect('focus-out-event', self.focus_out) + self.entry.connect('activate', self.activate) + combobox.connect('changed', self.changed) + self.entry.set_property('activates_default', True) + self._format = format_ if revision: - self.entry.set_text(datetime_strftime(revision, format_)) + self.entry.set_text(datetime_strftime(revision, self._format)) + self._value = revision active = -1 else: + self._value = None active = 0 - list_store = gtk.ListStore(str, str) list_store.append(('', '')) for i, (rev, id_, name) in enumerate(revisions, 1): - list_store.append((datetime_strftime(rev, format_), name)) + list_store.append((datetime_strftime(rev, self._format), name)) if rev == revision: active = i - combobox = gtk.ComboBoxEntry(list_store) - combobox.add(self.entry) combobox.set_active(active) cell = gtk.CellRendererText() combobox.pack_start(cell, True) combobox.add_attribute(cell, 'text', 1) hbox.pack_start(combobox, expand=True, fill=True) combobox.set_entry_text_column(0) - combobox.connect('changed', self.changed) - self.entry.set_property('activates_default', True) self.win.vbox.pack_start(hbox, expand=True, fill=True) self.win.show_all() + def focus_out(self, entry, event): + self.parse() + self.update() + return False + + def activate(self, entry): + self.parse() + self.update() + return False + def changed(self, combobox): - # set_text must be called because DateEntry doesn't work correctly with - # the default insert/delete text behavior of combobox - model = combobox.get_model() - idx = combobox.get_active() - if idx >= 0: - self.entry.set_text(model[idx][0]) + # "changed" signal is also triggered by text editing + # so only parse when a row is active + if combobox.get_active_iter(): + self.parse() + self.update() + return False + + def parse(self): + text = self.entry.get_text() + value = None + if text: + try: + value = date_parse(text, self._format) + except ValueError: + pass + self._value = value + + def update(self): + if not self._value: + self.entry.set_text('') + else: + self.entry.set_text(datetime_strftime(self._value, self._format)) def run(self): response = self.win.run() revision = None if response == gtk.RESPONSE_OK: - revision = self.entry.date_get() + revision = self._value self.parent.present() self.win.destroy() return revision diff --git a/tryton/gui/window/view_form/model/field.py b/tryton/gui/window/view_form/model/field.py index e62a834a73..afc5674139 100644 --- a/tryton/gui/window/view_form/model/field.py +++ b/tryton/gui/window/view_form/model/field.py @@ -4,16 +4,14 @@ import os from itertools import chain import tempfile import locale -from tryton.common import datetime_strftime, \ +from tryton.common import \ domain_inversion, eval_domain, localize_domain, \ merge, inverse_leaf, concat, simplify, EvalEnvironment import tryton.common as common -import time import datetime import decimal from decimal import Decimal import math -from tryton.translate import date_format from tryton.common import RPCExecute, RPCException @@ -202,12 +200,18 @@ class DateTimeField(Field): _default = None def set_client(self, record, value, force_change=False): - if isinstance(value, basestring): - try: - value = datetime.datetime(*time.strptime(value, - date_format() + ' ' + self.time_format(record))[:6]) - except ValueError: - value = self._default + if isinstance(value, datetime.time): + current_value = self.get_client(record) + if current_value: + value = datetime.datetime.combine( + current_value.date(), value) + else: + value = None + elif value and not isinstance(value, datetime.datetime): + current_value = self.get_client(record) + if current_value: + value = datetime.datetime.combine( + value, current_value.time()) if value: value = common.untimezoned_date(value) super(DateTimeField, self).set_client(record, value, @@ -216,10 +220,11 @@ class DateTimeField(Field): def get_client(self, record): value = super(DateTimeField, self).get_client(record) if value: - value = common.timezoned_date(value) - return datetime_strftime(value, date_format() + ' ' + - self.time_format(record)) - return '' + return common.timezoned_date(value) + + def date_format(self, record): + context = self.context_get(record) + return context.get('date_format', '%x') def time_format(self, record): return record.expr_eval(self.attrs['format']) @@ -230,23 +235,15 @@ class DateField(Field): _default = None def set_client(self, record, value, force_change=False): - if isinstance(value, basestring): - try: - value = datetime.date(*time.strptime(value, - date_format())[:3]) - except ValueError: - value = self._default - elif isinstance(value, datetime.datetime): + if isinstance(value, datetime.datetime): assert(value.time() == datetime.time()) value = value.date() super(DateField, self).set_client(record, value, force_change=force_change) - def get_client(self, record): - value = super(DateField, self).get_client(record) - if value: - return datetime_strftime(value, date_format()) - return '' + def date_format(self, record): + context = self.context_get(record) + return context.get('date_format', '%x') class TimeField(Field): @@ -254,23 +251,11 @@ class TimeField(Field): _default = None def set_client(self, record, value, force_change=False): - if isinstance(value, basestring): - try: - value = datetime.time(*time.strptime(value, - self.time_format(record))[3:6]) - except ValueError: - value = self._default - elif isinstance(value, datetime.datetime): + if isinstance(value, datetime.datetime): value = value.time() super(TimeField, self).set_client(record, value, force_change=force_change) - def get_client(self, record): - value = super(TimeField, self).get_client(record) - if value is not None: - return value.strftime(self.time_format(record)) - return '' - def time_format(self, record): return record.expr_eval(self.attrs['format']) @@ -902,6 +887,13 @@ class DictField(Field): return concat(localize_domain(inverse_leaf(screen_domain)), attr_domain) + def date_format(self, record): + context = self.context_get(record) + return context.get('date_format', '%x') + + def time_format(self, record): + return '%X' + TYPES = { 'char': CharField, 'float_time': FloatField, diff --git a/tryton/gui/window/view_form/view/form.py b/tryton/gui/window/view_form/view/form.py index 59c4e18b74..95a7a8fb4e 100644 --- a/tryton/gui/window/view_form/view/form.py +++ b/tryton/gui/window/view_form/view/form.py @@ -11,7 +11,7 @@ from tryton.common.focus import (get_invisible_ancestor, find_focused_child, from tryton.common import Tooltips, node_attributes, ICONFACTORY from tryton.common.button import Button from tryton.config import CONFIG -from .form_gtk.calendar import Calendar, DateTime, Time +from .form_gtk.calendar import Date, Time, DateTime from .form_gtk.float import Float from .form_gtk.integer import Integer from .form_gtk.selection import Selection @@ -364,7 +364,7 @@ class ViewForm(View): pack(container.table, resize=True, shrink=True) WIDGETS = { - 'date': Calendar, + 'date': Date, 'datetime': DateTime, 'time': Time, 'float': Float, diff --git a/tryton/gui/window/view_form/view/form_gtk/calendar.py b/tryton/gui/window/view_form/view/form_gtk/calendar.py index 93babdd31d..246880efbe 100644 --- a/tryton/gui/window/view_form/view/form_gtk/calendar.py +++ b/tryton/gui/window/view_form/view/form_gtk/calendar.py @@ -4,32 +4,39 @@ import gtk import gettext from .widget import Widget -from tryton.common.date_widget import DateEntry -from tryton.translate import date_format +from tryton.common.datetime_ import (Date as DateEntry, Time as TimeEntry, + DateTime as DateTimeEntry, add_operators) _ = gettext.gettext -class Calendar(Widget): - "Calendar" +class Date(Widget): - def __init__(self, view, attrs): - super(Calendar, self).__init__(view, attrs) + def __init__(self, view, attrs, _entry=DateEntry): + super(Date, self).__init__(view, attrs) self.widget = gtk.HBox() - self.entry = DateEntry('') - self.entry.set_property('activates_default', True) - self.entry.connect('key_press_event', self.sig_key_press) - self.entry.connect('activate', self.sig_activate) - self.entry.connect('changed', lambda _: self.send_modified()) - self.entry.connect('focus-out-event', lambda x, y: self._focus_out()) + self.entry = add_operators(_entry()) + self.real_entry.set_property('activates_default', True) + self.real_entry.connect('key_press_event', self.sig_key_press) + self.real_entry.connect('activate', self.sig_activate) + self.real_entry.connect('changed', lambda _: self.send_modified()) + self.real_entry.connect('focus-out-event', + lambda x, y: self._focus_out()) self.widget.pack_start(self.entry, expand=False, fill=False) + @property + def real_entry(self): + return self.entry + def _color_widget(self): return self.entry + def _set_editable(self, value): + self.real_entry.set_editable(value) + def _readonly_set(self, value): - self.entry.set_editable(not value) + self._set_editable(not value) if value: self.widget.set_focus_chain([]) else: @@ -38,8 +45,7 @@ class Calendar(Widget): @property def modified(self): if self.record and self.field: - value = self.entry.compute_date(self.entry.get_text()) - return self.field.get_client(self.record) != value + return self.field.get_client(self.record) != self.entry.props.value return False def sig_key_press(self, widget, event): @@ -49,41 +55,78 @@ class Calendar(Widget): field.set_client(record, self.get_value()) def get_value(self): - return self.entry.date_get(set_text=False) + return self.entry.props.value - def get_format(self, record, field): - return date_format() + def set_format(self, record, field): + if field and record: + format_ = field.date_format(record) + else: + format_ = '%x' + self.entry.props.format = format_ def display(self, record, field): - if not field: - self.entry.set_format('') - self.entry.clear() - return False - self.entry.set_format(self.get_format(record, field)) - super(Calendar, self).display(record, field) - value = field.get_client(record) - if not value: - self.entry.clear() + super(Date, self).display(record, field) + if field and record: + value = field.get_client(record) else: - if len(value) > self.entry.get_width_chars(): - self.entry.set_width_chars(len(value)) - self.entry.set_text(value) - return True + value = '' + self.entry.props.value = value + self.set_format(record, field) -class DateTime(Calendar): - "DateTime" - - def get_format(self, record, field): - return date_format() + ' ' + field.time_format(record) - - -class Time(Calendar): - "Time" - +class Time(Date): def __init__(self, view, attrs): - super(Time, self).__init__(view, attrs) - self.entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, None) + super(Time, self).__init__(view, attrs, _entry=TimeEntry) + self.entry.set_focus_chain([self.entry.get_child()]) - def get_format(self, record, field): - return field.time_format(record) + @property + def real_entry(self): + return self.entry.get_child() + + def display(self, record, field): + super(Time, self).display(record, field) + + def set_format(self, record, field): + if field and record: + format_ = field.time_format(record) + else: + format_ = '%X' + self.entry.props.format = format_ + + +class DateTime(Date): + def __init__(self, view, attrs): + Widget.__init__(self, view, attrs) + + self.widget = gtk.HBox() + self.entry = DateTimeEntry() + for child in self.entry.get_children(): + add_operators(child) + if isinstance(child, gtk.ComboBoxEntry): + child.set_focus_chain([child.get_child()]) + child = child.get_child() + child.set_property('activates_default', True) + child.connect('key_press_event', self.sig_key_press) + child.connect('activate', self.sig_activate) + child.connect('changed', lambda _: self.send_modified()) + child.connect('focus-out-event', lambda x, y: self._focus_out()) + self.widget.pack_start(self.entry, expand=False, fill=False) + + def _color_widget(self): + return self.entry.get_children()[0] # XXX not always sure to have Date + + def _set_editable(self, value): + for child in self.entry.get_children(): + if not hasattr(child, 'set_editable'): + child = child.child + child.set_editable(value) + + def set_format(self, record, field): + if field and record: + date_format = field.date_format(record) + time_format = field.time_format(record) + else: + date_format = '%x' + time_format = '%X' + self.entry.props.date_format = date_format + self.entry.props.time_format = time_format diff --git a/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/gui/window/view_form/view/form_gtk/dictionary.py index cfda3c563c..f6e7ac31ec 100644 --- a/tryton/gui/window/view_form/view/form_gtk/dictionary.py +++ b/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -12,13 +12,12 @@ from decimal import Decimal from .widget import Widget from tryton.config import CONFIG from tryton.gui.window.win_search import WinSearch -from tryton.common import RPCExecute, RPCException, timezoned_date, \ - datetime_strftime, Tooltips -from tryton.common.date_widget import DateEntry +from tryton.common import RPCExecute, RPCException, Tooltips, \ + timezoned_date, untimezoned_date from tryton.common.placeholder_entry import PlaceholderEntry from tryton.common.selection import selection_shortcuts from tryton.common.completion import get_completion, update_completion -from tryton.translate import date_format +from tryton.common.datetime_ import Date, DateTime from tryton.common.domain_parser import quote _ = gettext.gettext @@ -250,50 +249,45 @@ class DictDateTimeEntry(DictEntry): fill = False def create_widget(self): - widget = DateEntry('') - widget.set_format(self.get_format()) + widget = DateTime() + record = self.parent_widget.record + field = self.parent_widget.field + if record and field: + format_ = field.time_format(record) + widget.props.format = format_ widget.connect('key_press_event', self.parent_widget.send_modified) widget.connect('focus-out-event', lambda w, e: self.parent_widget._focus_out()) return widget - def modified(self, value): - if value.get(self.name): - text = datetime_strftime(timezoned_date(value[self.name]), - self.get_format()) - else: - text = '' - return self.widget.compute_date(self.widget.get_text()) != text - - def get_format(self): - return date_format() + ' %H:%M:%S' # got to find a way - def get_value(self): - return self.widget.date_get() + return untimezoned_date(self.widget.props.value) def set_value(self, value): - self.widget.date_set(value) - txt = self.widget.get_text() - if txt: - if len(txt) > self.widget.get_width_chars(): - self.widget.set_width_chars(len(txt)) + self.widget.props.value = timezoned_date(value) -class DictDateEntry(DictDateTimeEntry): +class DictDateEntry(DictEntry): + expand = False + fill = False - def modified(self, value): - if value.get(self.name): - text = datetime_strftime(value[self.name], self.get_format()) - else: - text = '' - return self.widget.compute_date(self.widget.get_text()) != text - - def get_format(self): - return date_format() + def create_widget(self): + widget = Date() + record = self.parent_widget.record + field = self.parent_widget.field + if record and field: + format_ = field.date_format(record) + widget.props.format = format_ + widget.connect('key_press_event', self.parent_widget.send_modified) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget def get_value(self): - dt = super(DictDateEntry, self).get_value() - return dt.date() if dt else None + return self.widget.props.value + + def set_value(self, value): + self.widget.props.value = value DICT_ENTRIES = { diff --git a/tryton/gui/window/view_form/view/graph_gtk/graph.py b/tryton/gui/window/view_form/view/graph_gtk/graph.py index f57a042866..8e3dc4576a 100644 --- a/tryton/gui/window/view_form/view/graph_gtk/graph.py +++ b/tryton/gui/window/view_form/view/graph_gtk/graph.py @@ -15,7 +15,6 @@ import tryton.rpc as rpc import cairo from tryton.action import Action from tryton.gui.window import Window -from tryton.translate import date_format class Popup(object): @@ -378,10 +377,12 @@ class Graph(gtk.DrawingArea): else: self.datas[x][key] += \ float(model[yfield['name']].get(model) or 0) + date_format = self.view.screen.context.get('date_format', '%x') + datetime_format = date_format + ' %X' if isinstance(minx, datetime.datetime): date = minx while date <= maxx: - self.labels[date] = datetime_strftime(date, date_format()) + self.labels[date] = datetime_strftime(date, datetime_format) self.datas.setdefault(date, {}) for yfield in self.yfields: self.datas[date].setdefault( @@ -390,7 +391,7 @@ class Graph(gtk.DrawingArea): elif isinstance(minx, datetime.date): date = minx while date <= maxx: - self.labels[date] = datetime_strftime(date, date_format()) + self.labels[date] = datetime_strftime(date, date_format) self.datas.setdefault(date, {}) for yfield in self.yfields: self.datas[date].setdefault( diff --git a/tryton/gui/window/view_form/view/list.py b/tryton/gui/window/view_form/view/list.py index f3b3d9071f..00b5dd84ae 100644 --- a/tryton/gui/window/view_form/view/list.py +++ b/tryton/gui/window/view_form/view/list.py @@ -20,7 +20,7 @@ from tryton.common.popup_menu import populate from tryton.common import RPCExecute, RPCException, node_attributes, Tooltips from . import View from .list_gtk.editabletree import EditableTreeView, TreeView -from .list_gtk.widget import (Affix, Char, Int, Boolean, URL, Date, Datetime, +from .list_gtk.widget import (Affix, Char, Int, Boolean, URL, Date, Time, Float, FloatTime, Binary, M2O, O2O, O2M, M2M, Selection, Reference, ProgressBar, Button, Image) @@ -426,7 +426,6 @@ class ViewTree(View): 'float_time': FloatTime, 'integer': Int, 'biginteger': Int, - 'datetime': Datetime, 'time': Time, 'boolean': Boolean, 'text': Char, diff --git a/tryton/gui/window/view_form/view/list_gtk/editabletree.py b/tryton/gui/window/view_form/view/list_gtk/editabletree.py index 2663f73882..6d244f34bf 100644 --- a/tryton/gui/window/view_form/view/list_gtk/editabletree.py +++ b/tryton/gui/window/view_form/view/list_gtk/editabletree.py @@ -6,7 +6,7 @@ import gobject from itertools import islice, cycle from tryton.common import MODELACCESS -from tryton.common.date_widget import DateEntry +from tryton.common.datetime_ import Date, Time _ = gettext.gettext @@ -110,6 +110,8 @@ class EditableTreeView(TreeView): field = record[column.name] if hasattr(field, 'editabletree_entry'): entry = field.editabletree_entry + if isinstance(entry, (Date, Time)): + txt = entry.props.value if isinstance(entry, gtk.Entry): txt = entry.get_text() else: @@ -141,9 +143,9 @@ class EditableTreeView(TreeView): leaving = True if event.keyval in self.leaving_events or leaving: - if isinstance(entry, gtk.Entry): - if isinstance(entry, DateEntry): - entry.date_get() + if isinstance(entry, (Date, Time)): + txt = entry.props.value + elif isinstance(entry, gtk.Entry): txt = entry.get_text() else: txt = entry.get_active_text() @@ -260,9 +262,9 @@ class EditableTreeView(TreeView): return True model = self.get_model() record = model.get_value(model.get_iter(path), 0) - if isinstance(entry, gtk.Entry): - if isinstance(entry, DateEntry): - entry.date_get() + if isinstance(entry, (Date, Time)): + self.on_quit_cell(record, column, entry.props.value) + elif isinstance(entry, gtk.Entry): self.on_quit_cell(record, column, entry.get_text()) elif isinstance(entry, (gtk.ComboBoxEntry, gtk.ComboBox)): self.on_quit_cell(record, column, entry.get_active_text()) diff --git a/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/gui/window/view_form/view/list_gtk/widget.py index 379d030c18..18f7b0a997 100644 --- a/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -17,7 +17,6 @@ import tryton.rpc as rpc from tryton.common import COLORS, file_selection, file_open, slugify import tryton.common as common from tryton.common.cellrendererbutton import CellRendererButton -from tryton.common.cellrendererdate import CellRendererDate from tryton.common.cellrenderertext import CellRendererText, \ CellRendererTextCompletion from tryton.common.cellrenderertoggle import CellRendererToggle @@ -27,10 +26,11 @@ from tryton.common.cellrendererfloat import CellRendererFloat from tryton.common.cellrendererbinary import CellRendererBinary from tryton.common.cellrendererclickablepixbuf import \ CellRendererClickablePixbuf -from tryton.translate import date_format from tryton.common import data2pixbuf from tryton.common.completion import get_completion, update_completion from tryton.common.selection import SelectionMixin, PopdownMixin +from tryton.common.datetime_ import CellRendererDate, CellRendererTime +from tryton.common.datetime_strftime import datetime_strftime from tryton.common.domain_parser import quote _ = gettext.gettext @@ -343,31 +343,53 @@ class Date(Char): def __init__(self, view, attrs, renderer=None): if renderer is None: - renderer = partial(CellRendererDate, date_format()) + renderer = CellRendererDate super(Date, self).__init__(view, attrs, renderer=renderer) - self.renderer.connect('editing-started', self.editing_started) - - -class Datetime(Date): @realized def setter(self, column, cell, store, iter): - super(Datetime, self).setter(column, cell, store, iter) record = store.get_value(iter, 0) field = record[self.attrs['name']] - time_format = field.time_format(record) - self.renderer.format = date_format() + ' ' + time_format + self.renderer.props.format = self.get_format(record, field) + super(Date, self).setter(column, cell, store, iter) + + def get_format(self, record, field): + if field and record: + return field.date_format(record) + else: + return '%x' + + def get_textual_value(self, record): + if not record: + return '' + value = record[self.attrs['name']].get_client(record) + if value: + return datetime_strftime(value, self.renderer.props.format) + else: + return '' class Time(Date): - @realized - def setter(self, column, cell, store, iter): - super(Time, self).setter(column, cell, store, iter) - record = store.get_value(iter, 0) - field = record[self.attrs['name']] - time_format = field.time_format(record) - self.renderer.format = time_format + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererTime + super(Time, self).__init__(view, attrs, renderer=renderer) + + def get_format(self, record, field): + if field and record: + return field.time_format(record) + else: + return '%X' + + def get_textual_value(self, record): + if not record: + return '' + value = record[self.attrs['name']].get_client(record) + if value: + return value.strftime(self.renderer.props.format) + else: + return '' class Float(Int): diff --git a/tryton/gui/window/view_form/view/screen_container.py b/tryton/gui/window/view_form/view/screen_container.py index f8d35502a3..b16f8365aa 100644 --- a/tryton/gui/window/view_form/view/screen_container.py +++ b/tryton/gui/window/view_form/view/screen_container.py @@ -8,7 +8,7 @@ import tryton.common as common from tryton.common.domain_parser import quote from tryton.common.placeholder_entry import PlaceholderEntry from tryton.common.treeviewcontrol import TreeViewControl -from tryton.translate import date_format +from tryton.common.datetime_ import Date, Time, DateTime, add_operators from tryton.config import TRYTON_ICON from tryton.pyson import PYSONDecoder @@ -17,17 +17,20 @@ _ = gettext.gettext class Dates(gtk.HBox): - def __init__(self, format_): + def __init__(self, format_=None, _entry=Date): super(Dates, self).__init__() - self.from_ = common.date_widget.DateEntry(format_) + self.from_ = add_operators(_entry()) self.pack_start(self.from_, expand=True, fill=True) self.pack_start(gtk.Label(_('..')), expand=False, fill=False) - self.to = common.date_widget.DateEntry(format_) + self.to = add_operators(_entry()) self.pack_start(self.to, expand=True, fill=True) + if format_: + self.from_.props.format = format_ + self.to.props.format = format_ def get_value(self): - from_ = self.from_.get_text() - to = self.to.get_text() + from_ = self.from_.props.value + to = self.to.props.value if from_ and to: if from_ != to: return '%s..%s' % (quote(from_), quote(to)) @@ -38,6 +41,30 @@ class Dates(gtk.HBox): elif to: return '<=%s' % quote(to) + def connect_activate(self, callback): + self.from_.connect('activate', callback) + self.to.connect('activate', callback) + + +class Times(Dates): + + def __init__(self, format_, _entry=Time): + super(Times, self).__init__(_entry=_entry) + + +class DateTimes(Dates): + + def __init__(self, date_format, time_format, _entry=DateTime): + super(DateTimes, self).__init__(_entry=_entry) + self.from_.props.date_format = date_format + self.to.props.date_format = date_format + self.from_.props.time_format = time_format + self.to.props.time_format = time_format + + def connect_activate(self, callback): + for widget in self.from_.get_children() + self.to.get_children(): + widget.connect('activate', callback) + class Selection(gtk.ScrolledWindow): @@ -423,10 +450,6 @@ class ScreenContainer(object): self.last_search_text = self.get_text() self.do_search() - def date_activate(entry): - entry._focus_out(entry, None) - search() - if not self.search_window: self.search_window = gtk.Window() self.search_window.set_transient_for(button.get_toplevel()) @@ -469,15 +492,16 @@ class ScreenContainer(object): entry = Selection(selections) yoptions = gtk.FILL | gtk.EXPAND elif field['type'] in ('date', 'datetime', 'time'): + date_format = self.screen.context.get('date_format', '%x') if field['type'] == 'date': - format_ = date_format() + entry = Dates(date_format) elif field['type'] in ('datetime', 'time'): - format_ = PYSONDecoder({}).decode(field['format']) - if field['type'] == 'datetime': - format_ = date_format() + ' ' + format_ - entry = Dates(format_) - entry.from_.connect('activate', date_activate) - entry.to.connect('activate', date_activate) + time_format = PYSONDecoder({}).decode(field['format']) + if field['type'] == 'time': + entry = Times(time_format) + elif field['type'] == 'datetime': + entry = DateTimes(date_format, time_format) + entry.connect_activate(lambda *a: search()) else: entry = gtk.Entry() entry.connect('activate', lambda *a: search()) diff --git a/tryton/translate.py b/tryton/translate.py index 008062ab9d..c43913843f 100644 --- a/tryton/translate.py +++ b/tryton/translate.py @@ -150,8 +150,6 @@ _LOCALE2WIN32 = { 'zh_TW': 'Chinese_Taiwan', } -_DATE_FORMAT = '%m/%d/%Y' - def setlang(lang=None, locale_dict=None): "Set language" @@ -191,8 +189,6 @@ def setlang(lang=None, locale_dict=None): conv = locale.localeconv() for field in locale_dict.keys(): if field == 'date': - global _DATE_FORMAT - _DATE_FORMAT = str(locale_dict[field]).replace('%-', '%') continue conv[field] = locale_dict[field] locale.localeconv = lambda: conv @@ -203,11 +199,3 @@ def set_language_direction(direction): gtk.widget_set_default_direction(gtk.TEXT_DIR_RTL) else: gtk.widget_set_default_direction(gtk.TEXT_DIR_LTR) - - -def date_format(): - ''' - Return the locale date format - ''' - global _DATE_FORMAT - return _DATE_FORMAT