From 77eb3c38eae8fbcee036ec08f98f31db7aec14d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 3 Mar 2019 12:36:36 +0100 Subject: [PATCH] Allow cells to have multiple renderers Each cell can define prefixes and suffixes renderer which will be added to the column using their expand property. We use this new feature to replace the CellRendererBinary by CellRendererClickablePixbuf's for each buttons. We implement also an editable cell for Reference fields by using a Selection and a Many2One cell. In order to be able to navigate between cells of the same column, we do not validate anymore the field when the cell is left for another of the same record (this is a similar behavior to the form). Also the next and previous columns are checked if they are editable if start_editing is set to ensure to stay in edition mode during the all navigation. issue8112 issue6918 review46761002 --- CHANGELOG | 3 + tryton/common/common.py | 11 +- tryton/gui/window/view_form/view/list.py | 10 +- .../view_form/view/list_gtk/editabletree.py | 153 +++-- .../window/view_form/view/list_gtk/widget.py | 529 ++++++++++++------ 5 files changed, 455 insertions(+), 251 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1ca03bb0c2..1f72cd6906 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Implement Reference cell with Selection and Many2One cells +* Replace CellRendererBinary with clickable pixbuf cells +* Allow cell to have multiple renderers * Fill 'to' with 'from' value on filter box * Add tab domain in URL * Use Popover for search filter diff --git a/tryton/common/common.py b/tryton/common/common.py index 8f5e918ba6..6374098471 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -20,7 +20,7 @@ try: from http import HTTPStatus except ImportError: from http import client as HTTPStatus -from functools import partial +from functools import partial, wraps from tryton.config import CONFIG from tryton.config import TRYTON_ICON, PIXMAPS_DIR import sys @@ -48,7 +48,7 @@ except ImportError: from threading import Lock import dateutil.tz -from gi.repository import Gtk +from gi.repository import Gtk, GLib from tryton import __version__ from tryton.exceptions import TrytonServerError, TrytonError @@ -1329,3 +1329,10 @@ def ellipsize(string, length): def date_format(format_): return format_ or rpc.CONTEXT.get('locale', {}).get('date', '%x') + + +def idle_add(func): + @wraps(func) + def wrapper(*args, **kwargs): + GLib.idle_add(func, *args, **kwargs) + return wrapper diff --git a/tryton/gui/window/view_form/view/list.py b/tryton/gui/window/view_form/view/list.py index ca18571623..3893cf8916 100644 --- a/tryton/gui/window/view_form/view/list.py +++ b/tryton/gui/window/view_form/view/list.py @@ -347,7 +347,7 @@ class ViewTree(View): column.name = name prefixes = [] - suffixes = [] + suffixes = list(widget.suffixes) if node_attrs['widget'] in ('url', 'email', 'callto', 'sip'): prefixes.append(Affix(self, node_attrs, protocol=node_attrs['widget'])) @@ -362,9 +362,10 @@ class ViewTree(View): else: list_ = suffixes list_.append(Affix(self, affix_attrs)) + prefixes.extend(widget.prefixes) for prefix in prefixes: - column.pack_start(prefix.renderer, expand=False) + column.pack_start(prefix.renderer, expand=prefix.expand) column.set_cell_data_func(prefix.renderer, prefix.setter) @@ -372,7 +373,7 @@ class ViewTree(View): column.set_cell_data_func(widget.renderer, widget.setter) for suffix in suffixes: - column.pack_start(suffix.renderer, expand=False) + column.pack_start(suffix.renderer, expand=suffix.expand) column.set_cell_data_func(suffix.renderer, suffix.setter) @@ -1141,7 +1142,8 @@ class ViewTree(View): path = self.record.get_index_path(model.group) if model.get_flags() & gtk.TREE_MODEL_LIST_ONLY: path = (path[0],) - focus_column = self.treeview.next_column(path, editable=new) + focus_column, focus_cell = self.treeview.next_column( + path, editable=new) if path[:-1]: self.treeview.expand_to_path(gtk.TreePath(path[:-1])) self.treeview.scroll_to_cell(path, focus_column, 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 22a79e00d0..adff22facc 100644 --- a/tryton/gui/window/view_form/view/list_gtk/editabletree.py +++ b/tryton/gui/window/view_form/view/list_gtk/editabletree.py @@ -3,7 +3,9 @@ import gtk import gettext import gobject -from itertools import islice, cycle +from itertools import islice, cycle, chain + +from gi.repository import Gtk from tryton.common import MODELACCESS from tryton.common.datetime_ import Date, Time @@ -11,6 +13,13 @@ from tryton.common.datetime_ import Date, Time _ = gettext.gettext +def focusable_cells(column, editable=True): + for cell in column.get_cells(): + mode = cell.props.mode + if not editable or mode & Gtk.CellRendererMode.EDITABLE: + yield cell + + class TreeView(gtk.TreeView): display_counter = 0 @@ -18,7 +27,14 @@ class TreeView(gtk.TreeView): super(TreeView, self).__init__() self.view = view - def next_column(self, path, column=None, editable=True, _sign=1): + def next_column( + self, path, column=None, cell=None, editable=True, _sign=1): + if cell: + cells = list(focusable_cells(column, editable)) + if len(cells) > 1: + idx = cells.index(cell) + _sign + if 0 <= idx < len(cells): + return (column, cells[idx]) columns = self.get_columns() if column is None: column = columns[-1 if _sign > 0 else 0] @@ -43,11 +59,17 @@ class TreeView(gtk.TreeView): else: readonly = False if not (invisible or readonly): - return column + cells = list(focusable_cells(column, editable)) + if cells: + cell = cells[0 if _sign > 0 else -1] + else: + continue + return (column, cell) + return (None, None) - def prev_column(self, path, column=None, editable=True): - return self.next_column(path, column=column, editable=editable, - _sign=-1) + def prev_column(self, path, column=None, cell=None, editable=True): + return self.next_column( + path, column=column, cell=cell, editable=editable, _sign=-1) class EditableTreeView(TreeView): @@ -60,7 +82,8 @@ class EditableTreeView(TreeView): super(EditableTreeView, self).__init__(view) self.editable = position - def on_quit_cell(self, current_record, column, value, callback=None): + def on_quit_cell( + self, current_record, column, renderer, value, callback=None): field = current_record[column.name] widget = self.view.get_column_widget(column) @@ -70,6 +93,12 @@ class EditableTreeView(TreeView): if callback: callback() return + if widget.renderer != renderer: + for widget in chain(widget.prefixes, widget.suffixes): + if widget.renderer == renderer: + break + else: + raise ValueError("Unknown renderer") widget.value_from_text(current_record, value, callback=callback) def on_open_remote(self, current_record, column, create, value, @@ -103,15 +132,19 @@ class EditableTreeView(TreeView): model.group.set_sequence(field=sequence) return res - def set_cursor(self, path, focus_column=None, start_editing=False): + def set_cursor( + self, path, focus_column=None, cell=None, start_editing=False): self.grab_focus() if focus_column: widget = self.view.get_column_widget(focus_column) if isinstance(widget.renderer, gtk.CellRendererToggle): start_editing = False self.scroll_to_cell(path, focus_column, use_align=False) - super(EditableTreeView, self).set_cursor(path, focus_column, - start_editing) + if cell: + self.set_cursor_on_cell(path, focus_column, cell, start_editing) + else: + super(EditableTreeView, self).set_cursor( + path, focus_column, start_editing) def set_value(self): path, column = self.get_cursor() @@ -120,10 +153,10 @@ class EditableTreeView(TreeView): for renderer in column.get_cell_renderers(): if renderer.props.editing: widget = self.view.get_column_widget(column) - self.on_editing_done(widget.editable) + self.on_editing_done(widget.editable, renderer) return True - def on_keypressed(self, entry, event): + def on_keypressed(self, entry, event, renderer): path, column = self.get_cursor() model = self.get_model() record = model.get_value(model.get_iter(path), 0) @@ -159,50 +192,47 @@ class EditableTreeView(TreeView): def callback(): entry.handler_unblock(entry.editing_done_id) - field = record[column.name] - # Must wait the edited entry came back in valid state - if field.validate(record): - if (keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Enter) - or (keyval == gtk.keysyms.Right and leaving)): - gobject.idle_add(self.set_cursor, path, - self.next_column(path, column), True) - elif (keyval == gtk.keysyms.ISO_Left_Tab - or (keyval == gtk.keysyms.Left and leaving)): - gobject.idle_add(self.set_cursor, path, - self.prev_column(path, column), True) - elif keyval in self.leaving_record_events: - fields = list(self.view.widgets.keys()) - if not record.validate(fields): - invalid_fields = record.invalid_fields - col = None - for col in self.get_columns(): - if col.name in invalid_fields: - break - gobject.idle_add(self.set_cursor, path, col, True) - return - if ((self.view.screen.pre_validate - and not record.pre_validate()) - or (not self.view.screen.parent - and not record.save())): - gobject.idle_add(self.set_cursor, path, column, - True) - return - entry.handler_block(entry.editing_done_id) - if keyval == gtk.keysyms.Up: - self._key_up(path, model, column) - elif keyval == gtk.keysyms.Down: - self._key_down(path, model, column) - elif keyval == gtk.keysyms.Return: - if self.editable == 'top': - new_path = self._key_up(path, model) - else: - new_path = self._key_down(path, model) - gobject.idle_add(self.set_cursor, new_path, - self.next_column(new_path), True) - entry.handler_unblock(entry.editing_done_id) + if (keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Enter) + or (keyval == gtk.keysyms.Right and leaving)): + gobject.idle_add(self.set_cursor, path, + *self.next_column(path, column, renderer), True) + elif (keyval == gtk.keysyms.ISO_Left_Tab + or (keyval == gtk.keysyms.Left and leaving)): + gobject.idle_add(self.set_cursor, path, + *self.prev_column(path, column, renderer), True) + elif keyval in self.leaving_record_events: + fields = list(self.view.widgets.keys()) + if not record.validate(fields): + invalid_fields = record.invalid_fields + col = None + for col in self.get_columns(): + if col.name in invalid_fields: + break + gobject.idle_add(self.set_cursor, path, col, True) + return + if ((self.view.screen.pre_validate + and not record.pre_validate()) + or (not self.view.screen.parent + and not record.save())): + gobject.idle_add(self.set_cursor, path, column, + True) + return + entry.handler_block(entry.editing_done_id) + if keyval == gtk.keysyms.Up: + self._key_up(path, model, column) + elif keyval == gtk.keysyms.Down: + self._key_down(path, model, column) + elif keyval == gtk.keysyms.Return: + if self.editable == 'top': + new_path = self._key_up(path, model) + else: + new_path = self._key_down(path, model) + gobject.idle_add(self.set_cursor, new_path, + *self.next_column(new_path), True) + entry.handler_unblock(entry.editing_done_id) else: gobject.idle_add(self.set_cursor, path, column, True) - self.on_quit_cell(record, column, txt, callback=callback) + self.on_quit_cell(record, column, renderer, txt, callback=callback) return True elif event.keyval in (gtk.keysyms.F3, gtk.keysyms.F2): if isinstance(entry, gtk.Entry): @@ -236,7 +266,7 @@ class EditableTreeView(TreeView): self.on_create_line() new_path = (path[0] + 1) % len(model) if not column: - column = self.next_column(new_path) + column, cell = self.next_column(new_path) self.set_cursor(new_path, column, True) self.scroll_to_cell(new_path) return new_path @@ -248,12 +278,12 @@ class EditableTreeView(TreeView): else: new_path = (path[0] - 1) % len(model) if not column: - column = self.next_column(new_path) + column, cell = self.next_column(new_path) self.set_cursor(new_path, column, True) self.scroll_to_cell(new_path) return new_path - def on_editing_done(self, entry): + def on_editing_done(self, entry, renderer): path, column = self.get_cursor() if not path: return True @@ -261,8 +291,9 @@ class EditableTreeView(TreeView): record = model.get_value(model.get_iter(path), 0) if isinstance(entry, (Date, Time)): entry.activate() - self.on_quit_cell(record, column, entry.props.value) - elif isinstance(entry, gtk.Entry): - self.on_quit_cell(record, column, entry.get_text()) + text = entry.props.value elif isinstance(entry, (gtk.ComboBoxEntry, gtk.ComboBox)): - self.on_quit_cell(record, column, entry.get_active_text()) + text = entry.get_active_text + else: + text = entry.get_text() + self.on_quit_cell(record, column, renderer, 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 d9fb1f9d05..5f4161b440 100644 --- a/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -6,9 +6,9 @@ import gtk import gettext import webbrowser -from functools import wraps, partial +from gi.repository import Gtk, GObject -import gobject +from functools import wraps, partial from tryton.gui.window.win_search import WinSearch from tryton.gui.window.win_form import WinForm @@ -22,7 +22,6 @@ from tryton.common.cellrenderertoggle import CellRendererToggle from tryton.common.cellrenderercombo import CellRendererCombo from tryton.common.cellrendererinteger import CellRendererInteger from tryton.common.cellrendererfloat import CellRendererFloat -from tryton.common.cellrendererbinary import CellRendererBinary from tryton.common.cellrendererclickablepixbuf import \ CellRendererClickablePixbuf from tryton.common import data2pixbuf @@ -36,15 +35,15 @@ _ = gettext.gettext def send_keys(renderer, editable, position, treeview): - editable.connect('key_press_event', treeview.on_keypressed) + editable.connect('key_press_event', treeview.on_keypressed, renderer) editable.editing_done_id = editable.connect('editing_done', - treeview.on_editing_done) + treeview.on_editing_done, renderer) if isinstance(editable, (gtk.ComboBoxEntry, gtk.ComboBox)): def changed(combobox): # "changed" signal is also triggered by text editing # so only trigger editing-done if a row is active if combobox.get_active_iter(): - treeview.on_editing_done(combobox) + treeview.on_editing_done(combobox, renderer) editable.connect('changed', changed) @@ -91,20 +90,20 @@ class CellCache(list): @classmethod def cache(cls, func): @wraps(func) - def wrapper(self, column, cell, store, iter): + def wrapper(self, column, cell, store, iter_): if not hasattr(self, 'display_counters'): self.display_counters = {} if not hasattr(self, 'cell_caches'): self.cell_caches = {} - record = store.get_value(iter, 0) + record = store.get_value(iter_, 0) counter = self.view.treeview.display_counter if (self.display_counters.get(record.id) != counter): if getattr(cell, 'decorated', None): - func(self, column, cell, store, iter) + func(self, column, cell, store, iter_) else: cache = cls() cache.decorate(cell) - func(self, column, cell, store, iter) + func(self, column, cell, store, iter_) cache.undecorate(cell) self.cell_caches[record.id] = cache self.display_counters[record.id] = counter @@ -114,10 +113,31 @@ class CellCache(list): class Cell(object): - pass + renderer = None + setter = None + expand = True + attrs = None + view = None + prefixes = [] + suffixes = [] + + def _get_record_field_from_path(self, path, store=None): + if not store: + store = self.view.treeview.get_model() + record = store.get_value(store.get_iter(path), 0) + field = record.group.fields[self.attrs['name']] + return record, field + + def _get_record_field_from_iter(self, iter_, store=None): + if not store: + store = self.view.treeview.get_model() + record = store.get_value(iter_, 0) + field = record[self.attrs['name']] + return record, field class Affix(Cell): + expand = False def __init__(self, view, attrs, protocol=None): super(Affix, self).__init__() @@ -138,7 +158,7 @@ class Affix(Cell): @realized @CellCache.cache def setter(self, column, cell, store, iter_): - record = store.get_value(iter_, 0) + record, field = self._get_record_field_from_iter(iter_, store) field = record[self.attrs['name']] field.state_set(record, states=('invisible',)) invisible = field.get_state_attrs(record).get('invisible', False) @@ -157,8 +177,7 @@ class Affix(Cell): cell.set_property('text', text) def clicked(self, renderer, path): - store = self.view.treeview.get_model() - record = store.get_value(store.get_iter(path), 0) + record, field = self._get_record_field_from_path(path) value = record[self.attrs['name']].get(record) if value: if self.protocol == 'email': @@ -181,10 +200,10 @@ class GenericText(Cell): renderer = CellRendererText self.renderer = renderer() self.renderer.connect('editing-started', self.editing_started) - if not isinstance(self.renderer, CellRendererBinary): - self.renderer.connect_after('editing-started', send_keys, - view.treeview) + self.renderer.connect_after( + 'editing-started', send_keys, view.treeview) self.renderer.set_property('yalign', 0) + self.renderer.set_property('xalign', self.align) self.view = view @property @@ -197,8 +216,8 @@ class GenericText(Cell): @realized @CellCache.cache - def setter(self, column, cell, store, iter): - record = store.get_value(iter, 0) + def setter(self, column, cell, store, iter_): + record = store.get_value(iter_, 0) text = self.get_textual_value(record) if isinstance(cell, CellRendererToggle): @@ -244,8 +263,6 @@ class GenericText(Cell): if isinstance(cell, CellRendererToggle): cell.set_property('activatable', False) - cell.set_property('xalign', self.align) - def open_remote(self, record, create, changed=False, text=None, callback=None): raise NotImplementedError @@ -268,12 +285,6 @@ class GenericText(Cell): editable.connect('remove-widget', remove) return False - def _get_record_field(self, path): - store = self.view.treeview.get_model() - record = store.get_value(store.get_iter(path), 0) - field = record.group.fields[self.attrs['name']] - return record, field - class Char(GenericText): @@ -321,9 +332,7 @@ class Boolean(GenericText): self.renderer.connect('toggled', self._sig_toggled) def _sig_toggled(self, renderer, path): - store = self.view.treeview.get_model() - record = store.get_value(store.get_iter(path), 0) - field = record[self.attrs['name']] + record, field = self._get_record_field_from_path(path) if not self.attrs.get('readonly', field.get_state_attrs(record).get('readonly', False)): value = record[self.attrs['name']].get_client(record) @@ -336,10 +345,9 @@ class URL(Char): @realized @CellCache.cache - def setter(self, column, cell, store, iter): - super(URL, self).setter(column, cell, store, iter) - record = store.get_value(iter, 0) - field = record[self.attrs['name']] + def setter(self, column, cell, store, iter_): + super(URL, self).setter(column, cell, store, iter_) + record, field = self._get_record_field_from_iter(iter_, store) field.state_set(record, states=('readonly',)) readonly = field.get_state_attrs(record).get('readonly', False) cell.set_property('visible', not readonly) @@ -353,11 +361,10 @@ class Date(GenericText): super(Date, self).__init__(view, attrs, renderer=renderer) @realized - def setter(self, column, cell, store, iter): - record = store.get_value(iter, 0) - field = record[self.attrs['name']] + def setter(self, column, cell, store, iter_): + record, field = self._get_record_field_from_iter(iter_, store) self.renderer.props.format = self.get_format(record, field) - super(Date, self).setter(column, cell, store, iter) + super(Date, self).setter(column, cell, store, iter_) def get_format(self, record, field): if field and record: @@ -412,35 +419,44 @@ class Float(Int): super(Float, self).__init__(view, attrs, renderer=renderer) @realized - def setter(self, column, cell, store, iter): - super(Float, self).setter(column, cell, store, iter) - record = store.get_value(iter, 0) - field = record[self.attrs['name']] + def setter(self, column, cell, store, iter_): + super(Float, self).setter(column, cell, store, iter_) + record, field = self._get_record_field_from_iter(iter_, store) digits = field.digits(record, factor=self.factor) cell.digits = digits class Binary(GenericText): - align = 0.5 + align = 1 def __init__(self, view, attrs, renderer=None): - self.filename = attrs.get('filename') if renderer is None: - renderer = partial(CellRendererBinary, bool(self.filename)) + renderer = CellRendererText super(Binary, self).__init__(view, attrs, renderer=renderer) - self.renderer.connect( - 'select', - lambda *args: gobject.idle_add(self.select_binary, *args)) - self.renderer.connect( - 'open', - lambda *args: gobject.idle_add(self.open_binary, *args)) - self.renderer.connect( - 'save', - lambda *args: gobject.idle_add(self.save_binary, *args)) - self.renderer.connect('clear', self.clear_binary) + self.renderer.set_property('editable', False) + self.renderer.set_property('xalign', self.align) + self.renderer_save = _BinarySave(self) + self.renderer_select = _BinarySelect(self) + if self.attrs.get('filename'): + self.renderer_open = _BinaryOpen(self) + else: + self.renderer_open = None + + @property + def prefixes(self): + return filter(None, [self.renderer_open]) + + @property + def suffixes(self): + return [self.renderer_save, self.renderer_select] def get_textual_value(self, record): - pass + field = record[self.attrs['name']] + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + return common.humanize(size) if size else '' def value_from_text(self, record, text, callback=None): if callback: @@ -448,14 +464,10 @@ class Binary(GenericText): @realized @CellCache.cache - def setter(self, column, cell, store, iter): - record = store.get_value(iter, 0) - field = record[self.attrs['name']] - if hasattr(field, 'get_size'): - size = field.get_size(record) - else: - size = len(field.get(record)) - cell.set_property('size', common.humanize(size) if size else '') + def setter(self, column, cell, store, iter_): + record, field = self._get_record_field_from_iter(iter_, store) + text = self.get_textual_value(record) + cell.set_property('text', text) states = ('invisible',) if getattr(self.view.treeview, 'editable', False): @@ -465,49 +477,6 @@ class Binary(GenericText): invisible = field.get_state_attrs(record).get('invisible', False) cell.set_property('visible', not invisible) - if getattr(self.view.treeview, 'editable', False): - readonly = self.attrs.get('readonly', - field.get_state_attrs(record).get('readonly', False)) - if invisible: - readonly = True - cell.set_property('editable', not readonly) - - def select_binary(self, renderer, path): - record, field = self._get_record_field(path) - filename = '' - filename = file_selection(_('Open...')) - if filename: - field.set_client(record, open(filename, 'rb').read()) - if self.filename: - filename_field = record.group.fields[self.filename] - filename_field.set_client(record, os.path.basename(filename)) - - def open_binary(self, renderer, path): - if not self.filename: - return - record, field = self._get_record_field(path) - filename_field = record.group.fields.get(self.filename) - filename = filename_field.get(record) - if not filename: - return - file_path = file_write(filename, self.get_data(record, field)) - root, type_ = os.path.splitext(filename) - if type_: - type_ = type_[1:] - file_open(file_path, type_) - - def save_binary(self, renderer, path): - filename = '' - record, field = self._get_record_field(path) - if self.filename: - filename_field = record.group.fields.get(self.filename) - filename = filename_field.get(record) - filename = file_selection(_('Save As...'), filename=filename, - action=gtk.FILE_CHOOSER_ACTION_SAVE) - if filename: - with open(filename, 'wb') as fp: - fp.write(self.get_data(record, field)) - def get_data(self, record, field): if hasattr(field, 'get_data'): data = field.get_data(record) @@ -517,12 +486,132 @@ class Binary(GenericText): data = data.encode('utf-8') return data - def clear_binary(self, renderer, path): - record, field = self._get_record_field(path) - if self.filename: - filename_field = record.group.fields[self.filename] - filename_field.set_client(record, None) - field.set_client(record, None) + +class _BinaryIcon(Cell): + expand = False + + def __init__(self, binary): + super().__init__() + self.binary = binary + self.renderer = CellRendererClickablePixbuf() + self.renderer.connect('clicked', self.clicked) + + @property + def attrs(self): + return self.binary.attrs + + @property + def view(self): + return self.binary.view + + +class _BinarySave(_BinaryIcon): + icon_name = 'tryton-save' + + def __init__(self, binary): + super().__init__(binary) + pixbuf = common.IconFactory.get_pixbuf( + self.icon_name, Gtk.IconSize.BUTTON) + self.renderer.set_property('pixbuf', pixbuf) + + @common.idle_add + def clicked(self, renderer, path): + filename = '' + record, field = self._get_record_field_from_path(path) + if self.attrs.get('filename'): + filename_field = record.group.fields.get(self.attrs['filename']) + filename = filename_field.get(record) + filename = file_selection(_('Save As...'), filename=filename, + action=gtk.FILE_CHOOSER_ACTION_SAVE) + if filename: + with open(filename, 'wb') as fp: + fp.write(self.binary.get_data(record, field)) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_): + record, field = self._get_record_field_from_iter(iter_, store) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + field.state_set(record, states=['invisible']) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible and size) + + +class _BinarySelect(_BinaryIcon): + def clicked(self, renderer, path): + record, field = self._get_record_field_from_path(path) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + if self.attrs.get('filename'): + filename_field = record.group.fields[self.attrs['filename']] + else: + filename_field = None + if size: + if filename_field: + filename_field.set_client(record, None) + field.set_client(record, None) + else: + def _select(): + filename = file_selection(_('Open...')) + if filename: + with open(filename, 'rb') as fp: + field.set_client(record, fp.read()) + if filename_field: + filename_field.set_client( + record, os.path.basename(filename)) + GObject.idle_add(_select) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_): + record, field = self._get_record_field_from_iter(iter_, store) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + if size: + icon = 'tryton-clear' + else: + icon = 'tryton-search' + pixbuf = common.IconFactory.get_pixbuf(icon, Gtk.IconSize.BUTTON) + cell.set_property('pixbuf', pixbuf) + field.state_set(record, states=['invisible', 'readonly']) + invisible = field.get_state_attrs(record).get('invisible', False) + readonly = self.attrs.get('readonly', + field.get_state_attrs(record).get('readonly', False)) + if readonly and size: + cell.set_property('visible', False) + else: + cell.set_property('visible', not invisible) + + +class _BinaryOpen(_BinarySave): + icon_name = 'tryton-open' + + def clicked(self, renderer, path): + record, field = self._get_record_field_from_path(path) + filename_field = record.group.fields.get(self.attrs.get('filename')) + filename = filename_field.get(record) + file_path = file_write(filename, self.binary.get_data(record, field)) + root, type_ = os.path.splitext(filename) + if type_: + type_ = type_[1:] + GObject.idle_add(file_open, file_path, type_) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_): + super().setter(column, cell, store, iter_) + record, field = self._get_record_field_from_iter(iter_, store) + filename_field = record.group.fields.get(self.attrs.get('filename')) + filename = filename_field.get(record) + if not filename: + cell.set_property('visible', False) class Image(GenericText): @@ -538,8 +627,7 @@ class Image(GenericText): @realized @CellCache.cache def setter(self, column, cell, store, iter_): - record = store.get_value(iter_, 0) - field = record[self.field_name] + record, field = self._get_record_field_from_iter(iter_, store) value = field.get_client(record) if isinstance(value, int): if value > CONFIG['image.max_size']: @@ -566,31 +654,50 @@ class M2O(GenericText): renderer = partial(CellRendererTextCompletion, self.set_completion) super(M2O, self).__init__(view, attrs, renderer=renderer) + def get_model(self, record, field): + return self.attrs['relation'] + + def has_target(self, value): + return value is not None + + def value_from_id(self, model, id_, str_=None): + if str_ is None: + str_ = '' + return id_, str_ + + def id_from_value(self, value): + return value + def value_from_text(self, record, text, callback=None): field = record.group.fields[self.attrs['name']] + model = self.get_model(record, field) if not text: - field.set_client(record, (None, '')) + field.set_client( + record, self.value_from_id(model, None, '')) if callback: callback() return - field = record[self.attrs['name']] - win = self.search_remote(record, field, text, callback=callback) - if len(win.screen.group) == 1: - win.response(None, gtk.RESPONSE_OK) - else: - win.show() + if model: + field = record[self.attrs['name']] + win = self.search_remote(record, field, text, callback=callback) + if len(win.screen.group) == 1: + win.response(None, gtk.RESPONSE_OK) + else: + win.show() def editing_started(self, cell, editable, path): super(M2O, self).editing_started(cell, editable, path) - record, field = self._get_record_field(path) + record, field = self._get_record_field_from_path(path) + model = self.get_model(record, field) def changed(editable): text = editable.get_text() - if field.get_client(record) != text: - field.set_client(record, (None, '')) + if self.get_textual_value(record) != text: + field.set_client( + record, self.value_from_id(model, None, '')) - if field.get(record): + if self.has_target(field.get(record)): icon1, tooltip1 = 'tryton-open', _("Open the record ") icon2, tooltip2 = 'tryton-clear', _("Clear the field ") else: @@ -609,10 +716,13 @@ class M2O(GenericText): def icon_press(editable, icon_pos, event): value = field.get(record) - if icon_pos == gtk.ENTRY_ICON_SECONDARY and value: - field.set_client(record, (None, '')) + if not model: + return + if icon_pos == gtk.ENTRY_ICON_SECONDARY and self.has_target(value): + field.set_client( + record, self.value_from_id(model, None, '')) editable.set_text('') - elif value: + elif self.has_target(value): self.open_remote(record, create=False, changed=False) else: self.open_remote( @@ -627,9 +737,9 @@ class M2O(GenericText): def open_remote(self, record, create=True, changed=False, text=None, callback=None): field = record.group.fields[self.attrs['name']] - relation = field.attrs['relation'] + model = self.get_model(record, field) - access = common.MODELACCESS[relation] + access = common.MODELACCESS[model] if (create and not (self.attrs.get('create', True) and access['create'])): return @@ -638,27 +748,26 @@ class M2O(GenericText): domain = field.domain_get(record) context = field.get_context(record) - if create: - obj_id = None - elif not changed: - obj_id = field.get(record) - else: + if not create and changed: self.search_remote(record, field, text, callback=callback).show() return + target_id = self.id_from_value(field.get(record)) - screen = Screen(relation, domain=domain, context=context, + screen = Screen(model, domain=domain, context=context, mode=['form'], view_ids=self.attrs.get('view_ids', '').split(','), exclude_field=field.attrs.get('relation_field')) def open_callback(result): if result: - value = (screen.current_record.id, + value = self.value_from_id( + model, + screen.current_record.id, screen.current_record.rec_name()) field.set_client(record, value, force_change=True) if callback: callback() - if obj_id: - screen.load([obj_id]) + if target_id and target_id >= 0: + screen.load([target_id]) WinForm(screen, open_callback, save_current=True, title=field.attrs.get('string')) else: @@ -666,21 +775,21 @@ class M2O(GenericText): title=field.attrs.get('string'), rec_name=text) def search_remote(self, record, field, text, callback=None): - relation = field.attrs['relation'] + model = self.get_model(record, field) domain = field.domain_get(record) context = field.get_search_context(record) order = field.get_search_order(record) - access = common.MODELACCESS[relation] + access = common.MODELACCESS[model] create_access = self.attrs.get('create', True) and access['create'] def search_callback(found): value = None if found: - value = found[0] - field.set_client(record, value) + value = self.value_from_id(model, *found[0]) + field.set_client(record, value) if callback: callback() - win = WinSearch(relation, search_callback, sel_multi=False, + win = WinSearch(model, search_callback, sel_multi=False, context=context, domain=domain, order=order, view_ids=self.attrs.get('view_ids', '').split(','), new=create_access, title=self.attrs.get('string')) @@ -688,14 +797,18 @@ class M2O(GenericText): return win def set_completion(self, entry, path): + record, field = self._get_record_field_from_path(path) if entry.get_completion(): entry.set_completion(None) - access = common.MODELACCESS[self.attrs['relation']] + model = self.get_model(record, field) + if not model: + return + access = common.MODELACCESS[model] completion = get_completion( search=access['read'], create=self.attrs.get('create', True) and access['create']) completion.connect('match-selected', self._completion_match_selected, - path) + path, model) completion.connect('action-activated', self._completion_action_activated, path) entry.set_completion(completion) @@ -703,18 +816,19 @@ class M2O(GenericText): entry.connect('changed', self._update_completion, path) def _key_press(self, entry, event, path): - record, field = self._get_record_field(path) - if (field.get(record) is not None + record, field = self._get_record_field_from_path(path) + if (self.has_target(field.get(record)) and event.keyval in (gtk.keysyms.Delete, gtk.keysyms.BackSpace)): entry.set_text('') - field.set_client(record, None) return False - def _completion_match_selected(self, completion, model, iter_, path): - record, field = self._get_record_field(path) + def _completion_match_selected( + self, completion, model, iter_, path, model_name): + record, field = self._get_record_field_from_path(path) rec_name, record_id = model.get(iter_, 0, 1) - field.set_client(record, (record_id, rec_name)) + field.set_client( + record, self.value_from_id(model_name, record_id, rec_name)) completion.get_entry().set_text(rec_name) completion_model = completion.get_model() @@ -723,14 +837,17 @@ class M2O(GenericText): return True def _update_completion(self, entry, path): - record, field = self._get_record_field(path) - if field.get(record) is not None: - return - model = field.attrs['relation'] + record, field = self._get_record_field_from_path(path) + value = field.get(record) + if self.has_target(value): + id_ = self.id_from_value(value) + if id_ is not None and id_ >= 0: + return + model = self.get_model(record, field) update_completion(entry, record, field, model) def _completion_action_activated(self, completion, index, path): - record, field = self._get_record_field(path) + record, field = self._get_record_field_from_path(path) entry = completion.get_entry() entry.handler_block(entry.editing_done_id) @@ -819,10 +936,13 @@ class Selection(GenericText, SelectionMixin, PopdownMixin): self.renderer.set_property('model', model) self.renderer.set_property('text-column', 0) + def get_value(self, record, field): + return field.get(record) + def get_textual_value(self, record): field = record[self.attrs['name']] self.update_selection(record, field) - value = field.get(record) + value = self.get_value(record, field) text = dict(self.selection).get(value, '') if value and not text: text = self.get_inactive_selection(value) @@ -834,9 +954,7 @@ class Selection(GenericText, SelectionMixin, PopdownMixin): def editing_started(self, cell, editable, path): super(Selection, self).editing_started(cell, editable, path) - store = self.view.treeview.get_model() - record = store.get_value(store.get_iter(path), 0) - field = record[self.attrs['name']] + record, field = self._get_record_field_from_path(path) def set_value(*a): return self.set_value(editable, record, field) @@ -847,7 +965,7 @@ class Selection(GenericText, SelectionMixin, PopdownMixin): self.update_selection(record, field) self.set_popdown(self.selection, editable) - value = field.get(record) + value = self.get_value(record, field) if not self.set_popdown_value(editable, value): self.get_inactive_selection(value) self.set_popdown_value(editable, value) @@ -861,28 +979,72 @@ class Selection(GenericText, SelectionMixin, PopdownMixin): return False -class Reference(GenericText, SelectionMixin): +class Reference(M2O): def __init__(self, view, attrs, renderer=None): super(Reference, self).__init__(view, attrs, renderer=renderer) - self.init_selection() + self.renderer_selection = _ReferenceSelection(view, attrs) + + @property + def prefixes(self): + return [self.renderer_selection] + + def get_model(self, record, field): + value = field.get_client(record) + if value: + model, value = value + return model + + def has_target(self, value): + if not value: + return False + model, value = value.split(',') + if not value: + value = None + else: + try: + value = int(value) + except ValueError: + value = None + return model and value >= 0 + + def value_from_id(self, model, id_, str_=None): + if str_ is None: + str_ = '' + return model, (id_, str_) + + def id_from_value(self, value): + _, value = value.split(',') + return int(value) def get_textual_value(self, record): - field = record[self.attrs['name']] - self.update_selection(record, field) - value = field.get_client(record) - if not value: - model, name = '', '' + value = super().get_textual_value(record) + if value: + model, value = value else: - model, name = value - if model: - return dict(self.selection).get(model, model) + ',' + name - else: - return name + value = '' + return value - def value_from_text(self, record, text, callback=None): - if callback: - callback() + +class _ReferenceSelection(Selection): + + def get_value(self, record, field): + value = field.get_client(record) + if value: + model, value = value + else: + model = None + return model + + def set_value(self, editable, record, field): + value = self.get_popdown_value(editable) + if value != self.get_value(record, field): + if value: + value = (value, (-1, '')) + else: + value = ('', '') + field.set_client(record, value) + return False class ProgressBar(object): @@ -909,9 +1071,8 @@ class ProgressBar(object): @realized @CellCache.cache - def setter(self, column, cell, store, iter): - record = store.get_value(iter, 0) - field = record[self.attrs['name']] + def setter(self, column, cell, store, iter_): + record, field = self._get_record_field_from_iter(iter_, store) field.state_set(record, states=('invisible',)) invisible = field.get_state_attrs(record).get('invisible', False) cell.set_property('visible', not invisible) @@ -936,7 +1097,7 @@ class ProgressBar(object): callback() -class Button(object): +class Button(Cell): def __init__(self, view, attrs): super(Button, self).__init__() @@ -949,8 +1110,8 @@ class Button(object): @realized @CellCache.cache - def setter(self, column, cell, store, iter): - record = store.get_value(iter, 0) + def setter(self, column, cell, store, iter_): + record = store.get_value(iter_, 0) states = record.expr_eval(self.attrs.get('states', {})) if record.group.readonly or record.readonly: states['readonly'] = True