From 237826bb43e8312a75c50bc03e231491ef9b008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 11 Mar 2019 12:02:58 +0100 Subject: [PATCH] Use separate XML parser in Views issue8131 review54561002 --- CHANGELOG | 1 + tryton/gui/window/view_form/view/__init__.py | 64 ++- tryton/gui/window/view_form/view/calendar_.py | 64 +-- .../view_form/view/calendar_gtk/calendar_.py | 8 +- tryton/gui/window/view_form/view/form.py | 384 +++++++-------- tryton/gui/window/view_form/view/graph.py | 93 ++-- tryton/gui/window/view_form/view/list.py | 436 ++++++++---------- 7 files changed, 527 insertions(+), 523 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 068e76e10a..9ee4bc2036 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Use separate XML parser in Views * Use between numbers in filter box * Rename email and calendar file * Use markup for styling diff --git a/tryton/gui/window/view_form/view/__init__.py b/tryton/gui/window/view_form/view/__init__.py index 89cb9f15a6..580e07a686 100644 --- a/tryton/gui/window/view_form/view/__init__.py +++ b/tryton/gui/window/view_form/view/__init__.py @@ -1,7 +1,13 @@ # 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 +from collections import defaultdict + from tryton.common import node_attributes +_ = gettext.gettext +list_ = list # list builtins is overridden by import .list + class View(object): view_type = None @@ -12,13 +18,20 @@ class View(object): editable = None children_field = None scroll = None + xml_parser = None def __init__(self, screen, xml): self.screen = screen - self.fields = {} + self.widgets = defaultdict(list_) + self.state_widgets = [] self.attributes = node_attributes(xml) screen.set_on_write(self.attributes.get('on_write')) + self.xml_parser( + self, self.screen.exclude_field, + {k: f.attrs for k, f in self.screen.group.fields.items()} + ).parse(xml) + def set_value(self): raise NotImplementedError @@ -61,3 +74,52 @@ class View(object): return ViewGraph(screen, root) elif tagname == 'calendar': return ViewCalendar(screen, root) + + +class XMLViewParser: + + def __init__(self, view, exclude_field, field_attrs): + self.view = view + self.exclude_field = exclude_field + self.field_attrs = field_attrs + + def _node_attributes(self, node): + node_attrs = node_attributes(node) + if 'name' in node_attrs: + field = self.field_attrs.get(node_attrs['name'], {}) + else: + field = {} + + for name in ['readonly', 'homogeneous']: + if name in node_attrs: + node_attrs[name] = bool(int(node_attrs[name])) + for name in [ + 'yexpand', 'yfill', 'xexpand', 'xfill', 'colspan', 'position']: + if name in node_attrs: + node_attrs[name] = int(node_attrs[name]) + for name in ['xalign', 'yalign']: + if name in node_attrs: + node_attrs[name] = float(node_attrs[name]) + + if field: + node_attrs.setdefault('widget', field['type']) + if node.tagName == 'label' and 'string' not in node_attrs: + node_attrs['string'] = field['string'] + _(':') + if node.tagName == 'field' and 'help' not in node_attrs: + node_attrs['help'] = field['help'] + for name in [ + 'relation', 'domain', 'selection', 'string', 'states', + 'relation_field', 'views', 'invisible', 'add_remove', + 'sort', 'context', 'size', 'filename', 'autocomplete', + 'translate', 'create', 'delete', 'selection_change_with', + 'schema_model']: + if name in field: + node_attrs.setdefault(name, field[name]) + return node_attrs + + def parse(self, node): + node_attrs = self._node_attributes(node) + if node.nodeType != node.ELEMENT_NODE: + return + parser = getattr(self, '_parse_%s' % node.tagName) + parser(node, node_attrs) diff --git a/tryton/gui/window/view_form/view/calendar_.py b/tryton/gui/window/view_form/view/calendar_.py index af349b771b..0853c0e6e0 100644 --- a/tryton/gui/window/view_form/view/calendar_.py +++ b/tryton/gui/window/view_form/view/calendar_.py @@ -7,8 +7,7 @@ import gettext from gi.repository import Gtk -from tryton.common import node_attributes -from . import View +from . import View, XMLViewParser try: from .calendar_gtk.calendar_ import Calendar_ from .calendar_gtk.toolbar import Toolbar @@ -29,29 +28,48 @@ def goocalendar_required(func): return wrapper +class CalendarXMLViewParser(XMLViewParser): + + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self.calendar_fields = [] + + def _parse_calendar(self, node, attributes): + for child in node.childNodes: + self.parse(child) + goocalendar = Calendar_( + self.view.attributes, self.view, self.calendar_fields) + toolbar = Toolbar(goocalendar) + self.view.scroll.add(goocalendar) + self.view.widget.pack_start( + toolbar, expand=False, fill=False, padding=0) + self.view.widgets['goocalendar'] = goocalendar + self.view.widgets['toolbar'] = toolbar + + def _parse_field(self, node, attributes): + self.calendar_fields.append(attributes) + + class ViewCalendar(View): + editable = False + view_type = 'calendar' + xml_parser = CalendarXMLViewParser def __init__(self, screen, xml): - super(ViewCalendar, self).__init__(screen, xml) - self.view_type = 'calendar' - self.editable = False - self.widgets = {} - self.widget = self.parse(xml) + self.widget = Gtk.VBox() - def parse(self, node): - vbox = Gtk.VBox() if not Calendar_: - return vbox - fields = [] - for node in node.childNodes: - if node.nodeType != node.ELEMENT_NODE: - continue - if node.tagName == 'field': - fields.append(node_attributes(node)) - goocalendar = Calendar_(attrs=self.attributes, screen=self.screen, - fields=fields) - toolbar = Toolbar(goocalendar) - self.widgets['toolbar'] = toolbar + self.widgets = {} + return + + self.scroll = scrolledWindow = Gtk.ScrolledWindow() + self.widget.pack_end( + scrolledWindow, expand=True, fill=True, padding=0) + + super().__init__(screen, xml) + + goocalendar = self.widgets['goocalendar'] + toolbar = self.widgets['toolbar'] goocalendar.connect('view-changed', self.on_view_changed, toolbar) goocalendar.connect('page-changed', self.on_page_changed, toolbar) goocalendar.connect('event-pressed', self.on_event_pressed) @@ -59,12 +77,6 @@ class ViewCalendar(View): goocalendar.connect('event-released', self.on_event_released) goocalendar.connect('day-pressed', self.on_day_pressed) goocalendar.connect('day-activated', self.on_day_activated) - self.widgets['goocalendar'] = goocalendar - self.scroll = scrolledWindow = Gtk.ScrolledWindow() - scrolledWindow.add(goocalendar) - vbox.pack_start(toolbar, expand=False, fill=False, padding=0) - vbox.pack_start(scrolledWindow, expand=True, fill=True, padding=0) - return vbox def on_page_changed(self, goocalendar, day, toolbar): toolbar.update_displayed_date() diff --git a/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py b/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py index 471513e380..b1968f4e62 100644 --- a/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +++ b/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py @@ -9,11 +9,11 @@ from .dates_period import DatesPeriod class Calendar_(goocalendar.Calendar): 'Calendar' - def __init__(self, attrs, screen, fields, event_store=None): + def __init__(self, attrs, view, fields, event_store=None): super(Calendar_, self).__init__( event_store, attrs.get('mode', 'month')) self.attrs = attrs - self.screen = screen + self.view_calendar = view self.fields = fields self.event_store = event_store self.current_domain_period = self.get_displayed_period() @@ -76,8 +76,8 @@ class Calendar_(goocalendar.Calendar): def display(self, group): dtstart = self.attrs['dtstart'] dtend = self.attrs.get('dtend') - if self.screen.current_record: - record = self.screen.current_record + if self.view_calendar.record: + record = self.view_calendar.record date = record[dtstart].get(record) if date: # select the day of the current record self.select(date) diff --git a/tryton/gui/window/view_form/view/form.py b/tryton/gui/window/view_form/view/form.py index 31c4cfe8ce..b82ee641aa 100644 --- a/tryton/gui/window/view_form/view/form.py +++ b/tryton/gui/window/view_form/view/form.py @@ -2,11 +2,10 @@ # this repository contains the full copyright notices and license terms. import operator import gettext -from collections import defaultdict from gi.repository import Gtk -from . import View +from . import View, XMLViewParser from tryton.common.focus import (get_invisible_ancestor, find_focused_child, next_focus_widget, find_focusable_child, find_first_focus_widget) from tryton.common import Tooltips, node_attributes, IconFactory, get_align @@ -163,102 +162,123 @@ class HContainer(Container): self.table.pack_start(widget, expand=expand, fill=fill, padding=1) -class ViewForm(View): - editable = True +class FormXMLViewParser(XMLViewParser): - def __init__(self, screen, xml): - super(ViewForm, self).__init__(screen, xml) - self.view_type = 'form' - self.widgets = defaultdict(list) - self.state_widgets = [] - self.notebooks = [] - self.expandables = [] + WIDGETS = { + 'biginteger': Integer, + 'binary': Binary, + 'boolean': CheckBox, + 'callto': CallTo, + 'char': Char, + 'date': Date, + 'datetime': DateTime, + 'dict': DictWidget, + 'email': Email, + 'float': Float, + 'image': Image2, + 'integer': Integer, + 'many2many': Many2Many, + 'many2one': Many2One, + 'multiselection': MultiSelection, + 'numeric': Float, + 'one2many': One2Many, + 'one2one': One2One, + 'password': Password, + 'progressbar': ProgressBar, + 'pyson': PYSON, + 'reference': Reference, + 'richtext': RichTextBox, + 'selection': Selection, + 'sip': SIP, + 'text': TextBox, + 'time': Time, + 'timedelta': TimeDelta, + 'timestamp': DateTime, + 'url': URL, + } - container = self.parse(xml) + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self._containers = [] + self._mnemonics = {} - vbox = Gtk.VBox() - vp = Gtk.Viewport() - vp.set_shadow_type(Gtk.ShadowType.NONE) - vp.add(container.table) - self.scroll = scroll = Gtk.ScrolledWindow() - scroll.add(vp) - scroll.set_policy( - Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scroll.set_placement(Gtk.CornerType.TOP_LEFT) - viewport = Gtk.Viewport() - viewport.set_shadow_type(Gtk.ShadowType.ETCHED_IN) - viewport.add(scroll) - vbox.pack_start(viewport, expand=True, fill=True, padding=0) + @property + def container(self): + if self._containers: + return self._containers[-1] + return None - self.widget = vbox - self._viewport = vp + def _parse_form(self, node, attributes): + container_attributes = node_attributes(node) + container = Container.constructor( + int(container_attributes.get('col', 4)), + container_attributes.get('homogeneous', False)) + self.view.viewport.add(container.table) + self.parse_child(node, container) + assert not self._containers - def parse(self, node, container=None): - if not container: - node_attrs = node_attributes(node) - container = Container.constructor( - int(node_attrs.get('col', 4)), - node_attrs.get('homogeneous', False)) - mnemonics = {} - for node in node.childNodes: - if node.nodeType != node.ELEMENT_NODE: - continue - node_attrs = node_attributes(node) - for b_field in ('readonly', 'homogeneous'): - if b_field in node_attrs: - node_attrs[b_field] = bool(int(node_attrs[b_field])) - for i_field in ('yexpand', 'yfill', 'xexpand', 'xfill', 'colspan', - 'position'): - if i_field in node_attrs: - node_attrs[i_field] = int(node_attrs[i_field]) + def parse_child(self, node, container=None): + if container: + self._containers.append(container) + for child in node.childNodes: + self.parse(child) + if container: + self._containers.pop() - parser = getattr(self, '_parse_%s' % node.tagName) - widget = parser(node, container, node_attrs) - if not widget: - continue - name = node_attrs.get('name') - if node.tagName == 'label' and name: - mnemonics[name] = widget - if node.tagName == 'field': - if name in mnemonics and widget.mnemonic_widget: - label = mnemonics.pop(name) - label.set_label(set_underline(label.get_label())) - label.set_use_underline(True) - label.set_mnemonic_widget(widget.mnemonic_widget) - return container + def _parse_field(self, node, attributes): + name = attributes['name'] + if name and name == self.exclude_field: + self.container.add(None, attributes) + return - def _parse_image(self, node, container, attributes): + widget = self.WIDGETS[attributes['widget']](self.view, attributes) + self.view.widgets[name].append(widget) + + if widget.expand: + attributes.setdefault('yexpand', True) + attributes.setdefault('yfill', True) + + if attributes.get('height') or attributes.get('width'): + widget.widget.set_size_request( + int(attributes.get('width', -1)), + int(attributes.get('height', -1))) + + self.container.add(Alignment(widget.widget, attributes), attributes) + + if name in self._mnemonics and widget.mnemonic_widget: + label = self._mnemonics.pop(name) + label.set_label(set_underline(label.get_label())) + label.set_use_underline(True) + label.set_mnemonic_widget(widget.mnemonic_widget) + + def _parse_button(self, node, attributes): + button = Button(attributes) + button.connect('clicked', self.view.button_clicked) + self.view.state_widgets.append(button) + self.container.add(button, attributes) + + def _parse_image(self, node, attributes): image = Image(attrs=attributes) - self.state_widgets.append(image) - container.add(image, attributes) + self.view.state_widgets.append(image) + self.container.add(image, attributes) - def _parse_separator(self, node, container, attributes): - if 'name' in attributes: - field = self.group.fields[attributes['name']] - for attr in ('states', 'string'): - if attr not in attributes and attr in field.attrs: - attributes[attr] = field.attrs[attr] + def _parse_separator(self, node, attributes): vbox = VBox(attrs=attributes) if attributes.get('string'): label = Label(label=attributes['string'], attrs=attributes) label.set_halign(get_align(attributes.get('xalign', 0.0))) label.set_valign(get_align(attributes.get('yalign', 0.5))) vbox.pack_start(label, expand=True, fill=True, padding=0) - self.state_widgets.append(label) + self.view.state_widgets.append(label) vbox.pack_start(Gtk.HSeparator(), expand=True, fill=True, padding=0) - self.state_widgets.append(vbox) - container.add(vbox, attributes) + self.view.state_widgets.append(vbox) + self.container.add(vbox, attributes) - def _parse_label(self, node, container, attributes): - if 'name' in attributes: - field = self.group.fields[attributes['name']] - if attributes['name'] == self.screen.exclude_field: - container.add(None, attributes) - return - if 'states' not in attributes and 'states' in field.attrs: - attributes['states'] = field.attrs['states'] - if 'string' not in attributes: - attributes['string'] = field.attrs['string'] + _(':') + def _parse_label(self, node, attributes): + name = attributes.get('name') + if name and name == self.exclude_field: + self.container.add(None, attributes) + return if CONFIG['client.modepda']: attributes['xalign'] = 0.0 @@ -267,20 +287,15 @@ class ViewForm(View): label.set_valign(get_align(attributes.get('yalign', 0.5))) label.set_angle(int(attributes.get('angle', 0))) attributes.setdefault('xexpand', 0) - self.state_widgets.append(label) - container.add(label, attributes) - return label + self.view.state_widgets.append(label) + self.container.add(label, attributes) + if name: + self._mnemonics[name] = label - def _parse_newline(self, node, container, attributes): - container.add_row() + def _parse_newline(self, node, attributes): + self.container.add_row() - def _parse_button(self, node, container, attributes): - button = Button(attributes) - button.connect('clicked', self.button_clicked) - self.state_widgets.append(button) - container.add(button, attributes) - - def _parse_notebook(self, node, container, attributes): + def _parse_notebook(self, node, attributes): attributes.setdefault('yexpand', True) attributes.setdefault('yfill', True) attributes.setdefault('colspan', 4) @@ -295,28 +310,23 @@ class ViewForm(View): # Force to display the first time it switches on a page # This avoids glitch in position of widgets def switch(notebook, page, page_num): - if not self.widget: + if not self.view.widget: # Not yet finish to parse return notebook.grab_focus() - self.display() + self.view.display() notebook.disconnect(handler_id) handler_id = notebook.connect('switch-page', switch) - self.state_widgets.append(notebook) + self.view.state_widgets.append(notebook) - self.notebooks.append(notebook) - container.add(notebook, attributes) - self.parse(node, notebook) + self.view.notebooks.append(notebook) + self.container.add(notebook, attributes) + self.parse_child(node, notebook) - def _parse_page(self, node, notebook, attributes): + def _parse_page(self, node, attributes): tab_box = Gtk.HBox(spacing=3) - if 'name' in attributes: - field = self.group.fields[attributes['name']] - if attributes['name'] == self.screen.exclude_field: - return - for attr in ('states', 'string'): - if attr not in attributes and attr in field.attrs: - attributes[attr] = field.attrs[attr] + if 'name' in attributes and attributes['name'] == self.exclude_field: + return label = Gtk.Label(label=set_underline(attributes['string'])) label.set_use_underline(True) @@ -335,133 +345,91 @@ class ViewForm(View): Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolledwindow.add(viewport) scrolledwindow.show_all() - self.state_widgets.append(scrolledwindow) - notebook.append_page(scrolledwindow, tab_box) - container = self.parse(node) + self.view.state_widgets.append(scrolledwindow) + self.container.append_page(scrolledwindow, tab_box) + container = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, container) viewport.add(container.table) - def _parse_field(self, node, container, attributes): - name = attributes['name'] - field = self.group.fields[name] + def _parse_group(self, node, attributes): + group = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, group) - if (name not in self.group.fields - or name == self.screen.exclude_field): - container.add(None, attributes) + if 'name' in attributes and attributes['name'] == self.exclude_field: + self.container.add(None, attributes) return - if 'widget' not in attributes: - attributes['widget'] = field.attrs['type'] - - for i_field in ('width', 'height'): - if i_field in attributes: - attributes[i_field] = int(attributes[i_field]) - - for attr in ('relation', 'domain', 'selection', - 'relation_field', 'string', 'help', 'views', - 'add_remove', 'sort', 'context', 'size', 'filename', - 'autocomplete', 'translate', 'create', 'delete', - 'selection_change_with', 'schema_model'): - if attr in field.attrs and attr not in attributes: - attributes[attr] = field.attrs[attr] - - Widget = self.get_widget(attributes['widget']) - widget = Widget(self, attributes) - self.widgets[name].append(widget) - - if Widget.expand: - attributes.setdefault('yexpand', True) - attributes.setdefault('yfill', True) - - if attributes.get('height') or attributes.get('width'): - widget.widget.set_size_request( - int(attributes.get('width', -1)), - int(attributes.get('height', -1))) - container.add(Alignment(widget.widget, attributes), attributes) - return widget - - def _parse_group(self, node, container, attributes): - group = self.parse(node) - if 'name' in attributes: - field = self.group.fields[attributes['name']] - if attributes['name'] == self.screen.exclude_field: - container.add(None, attributes) - return - for attr in ('states', 'string'): - if attr not in attributes and attr in field.attrs: - attributes[attr] = field.attrs[attr] - can_expand = attributes.get('expandable') if can_expand: widget = Expander(label=attributes.get('string'), attrs=attributes) widget.add(group.table) widget.set_expanded(can_expand == '1') - self.expandables.append(widget) + self.view.expandables.append(widget) else: widget = Frame(label=attributes.get('string'), attrs=attributes) widget.add(group.table) - self.state_widgets.append(widget) - container.add(widget, attributes) + self.view.state_widgets.append(widget) + self.container.add(widget, attributes) - def _parse_paned(self, node, container, attributes, Paned): + def _parse_hpaned(self, node, attributes): + self._parse_paned(node, attributes, Gtk.HPaned) + + def _parse_vpaned(self, node, attributes): + self._parse_paned(node, attributes, Gtk.VPaned) + + def _parse_paned(self, node, attributes, Paned): attributes.setdefault('yexpand', True) attributes.setdefault('yfill', True) paned = Paned() if 'position' in attributes: paned.set_position(attributes['position']) - container.add(paned, attributes) - self.parse(node, paned) + self.container.add(paned, attributes) + self.parse_child(node, paned) - def _parse_hpaned(self, node, container, attributes): - self._parse_paned(node, container, attributes, Gtk.HPaned) - - def _parse_vpaned(self, node, container, attributes): - self._parse_paned(node, container, attributes, Gtk.VPaned) - - def _parse_child(self, node, paned, attributes): - container = self.parse(node) + def _parse_child(self, node, attributes): + paned = self.container + container = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, container) if not paned.get_child1(): pack = paned.pack1 else: pack = paned.pack2 pack(container.table, resize=True, shrink=True) - WIDGETS = { - 'date': Date, - 'datetime': DateTime, - 'time': Time, - 'timestamp': DateTime, - 'float': Float, - 'numeric': Float, - 'integer': Integer, - 'biginteger': Integer, - 'selection': Selection, - 'char': Char, - 'password': Password, - 'timedelta': TimeDelta, - 'boolean': CheckBox, - 'reference': Reference, - 'binary': Binary, - 'text': TextBox, - 'one2many': One2Many, - 'many2many': Many2Many, - 'many2one': Many2One, - 'email': Email, - 'url': URL, - 'callto': CallTo, - 'sip': SIP, - 'image': Image2, - 'progressbar': ProgressBar, - 'one2one': One2One, - 'richtext': RichTextBox, - 'dict': DictWidget, - 'multiselection': MultiSelection, - 'pyson': PYSON, - } - @classmethod - def get_widget(cls, name): - return cls.WIDGETS[name] +class ViewForm(View): + editable = True + view_type = 'form' + xml_parser = FormXMLViewParser + + def __init__(self, screen, xml): + self.notebooks = [] + self.expandables = [] + + vbox = Gtk.VBox() + vp = Gtk.Viewport() + vp.set_shadow_type(Gtk.ShadowType.NONE) + self.scroll = scroll = Gtk.ScrolledWindow() + scroll.add(vp) + scroll.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.set_placement(Gtk.CornerType.TOP_LEFT) + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + viewport.add(scroll) + vbox.pack_start(viewport, expand=True, fill=True, padding=0) + + self.widget = vbox + self.viewport = vp + + super().__init__(screen, xml) def get_fields(self): return list(self.widgets.keys()) @@ -554,7 +522,7 @@ class ViewForm(View): focus_widget = find_focusable_child(self.widgets[ self.attributes['cursor']][0].widget) else: - child = find_focusable_child(self._viewport) + child = find_focusable_child(self.viewport) if child: child.grab_focus() record = self.record @@ -568,7 +536,7 @@ class ViewForm(View): invalid_widgets.append(invalid_widget) if invalid_widgets: focus_widget = find_first_focus_widget( - self._viewport, invalid_widgets) + self.viewport, invalid_widgets) if focus_widget: for notebook in self.notebooks: for i in range(notebook.get_n_pages()): diff --git a/tryton/gui/window/view_form/view/graph.py b/tryton/gui/window/view_form/view/graph.py index a40e90ba37..cf4c6a671a 100644 --- a/tryton/gui/window/view_form/view/graph.py +++ b/tryton/gui/window/view_form/view/graph.py @@ -6,7 +6,7 @@ import gettext from gi.repository import Gtk -from . import View +from . import View, XMLViewParser from .graph_gtk.bar import VerticalBar, HorizontalBar from .graph_gtk.line import Line from .graph_gtk.pie import Pie @@ -19,57 +19,60 @@ from tryton.gui import Main _ = gettext.gettext -class ViewGraph(View): - - def __init__(self, screen, xml): - super(ViewGraph, self).__init__(screen, xml) - self.view_type = 'graph' - self.widgets = {} - self.widget = self.parse(xml) - - def parse(self, node): - xfield = None - yfields = [] - - for node in node.childNodes: - if node.nodeType != node.ELEMENT_NODE: - continue - if node.tagName == 'x': - for child in node.childNodes: - if not child.nodeType == child.ELEMENT_NODE: - continue - xfield = node_attributes(child) - field = self.screen.group.fields[xfield['name']] - if not xfield.get('string'): - xfield['string'] = field.attrs['string'] - elif node.tagName == 'y': - for child in node.childNodes: - if not child.nodeType == child.ELEMENT_NODE: - continue - yattrs = node_attributes(child) - if not yattrs.get('string') and yattrs['name'] != '#': - field = self.screen.group.fields[yattrs['name']] - yattrs['string'] = field.attrs['string'] - yfields.append(yattrs) - - Widget = self.get_widget(self.attributes.get('type', 'vbar')) - widget = Widget(self, xfield, yfields) - self.widgets['root'] = widget - event = Gtk.EventBox() - event.add(widget) - event.connect('button-press-event', self.button_press) - return event +class GraphXMLViewParser(XMLViewParser): WIDGETS = { - 'vbar': VerticalBar, 'hbar': HorizontalBar, 'line': Line, 'pie': Pie, + 'vbar': VerticalBar, } - @classmethod - def get_widget(cls, name): - return cls.WIDGETS[name] + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self._xfield = None + self._yfields = [] + + def _node_attributes(self, node): + node_attrs = node_attributes(node) + if 'name' in node_attrs: + if not node_attrs.get('string') and node_attrs['name'] != '#': + field = self.field_attrs[node_attrs['name']] + node_attrs['string'] = field['string'] + return node_attrs + + def _parse_graph(self, node, attributes): + for child in node.childNodes: + self.parse(child) + + Widget = self.WIDGETS.get(attributes.get('type', 'vbar')) + widget = Widget( + self.view, self._xfield, self._yfields) + self.view.widget.add(widget) + self.view.widgets['root'] = widget + + def _parse_x(self, node, attributes): + for child in node.childNodes: + if not child.nodeType == child.ELEMENT_NODE: + continue + self._xfield = self._node_attributes(child) + + def _parse_y(self, node, attributes): + for child in node.childNodes: + if not child.nodeType == child.ELEMENT_NODE: + continue + self._yfields.append(self._node_attributes(child)) + + +class ViewGraph(View): + view_type = 'graph' + editable = False + xml_parser = GraphXMLViewParser + + def __init__(self, screen, xml): + self.widget = event = Gtk.EventBox() + super().__init__(screen, xml) + event.connect('button-press-event', self.button_press) def __getitem__(self, name): return None diff --git a/tryton/gui/window/view_form/view/list.py b/tryton/gui/window/view_form/view/list.py index 643781e16c..f7571a52ba 100644 --- a/tryton/gui/window/view_form/view/list.py +++ b/tryton/gui/window/view_form/view/list.py @@ -5,7 +5,6 @@ import json import locale import gettext from functools import wraps -from collections import defaultdict from gi.repository import Gdk, GLib, GObject, Gtk from pygtkcompat.generictreemodel import GenericTreeModel @@ -18,7 +17,7 @@ from tryton.common import RPCExecute, RPCException, node_attributes, Tooltips from tryton.common import domain_inversion, simplify, unique_value from tryton.pyson import PYSONDecoder import tryton.common as common -from . import View +from . import View, XMLViewParser from .list_gtk.editabletree import EditableTreeView, TreeView from .list_gtk.widget import (Affix, Char, Text, Int, Boolean, URL, Date, Time, Float, TimeDelta, Binary, M2O, O2O, O2M, M2M, Selection, Reference, @@ -250,26 +249,215 @@ class AdaptModelGroup(GenericTreeModel): return record.parent +class TreeXMLViewParser(XMLViewParser): + + WIDGETS = { + 'biginteger': Int, + 'binary': Binary, + 'boolean': Boolean, + 'callto': URL, + 'char': Char, + 'date': Date, + 'email': URL, + 'float': Float, + 'image': Image, + 'integer': Int, + 'many2many': M2M, + 'many2one': M2O, + 'numeric': Float, + 'one2many': O2M, + 'one2one': O2O, + 'progressbar': ProgressBar, + 'reference': Reference, + 'selection': Selection, + 'sip': URL, + 'text': Text, + 'time': Time, + 'timedelta': TimeDelta, + 'url': URL, + } + + def _parse_tree(self, node, attributes): + for child in node.childNodes: + self.parse(child) + + def _parse_field(self, node, attributes): + name = attributes['name'] + widget = self.WIDGETS[attributes['widget']](self.view, attributes) + self.view.widgets[name].append(widget) + + column = Gtk.TreeViewColumn(attributes['string']) + column._type = 'field' + column.name = name + + prefixes = [] + suffixes = [] + if attributes['widget'] in ['url', 'email', 'callto', 'sip']: + prefixes.append( + Affix(self.view, attributes, protocol=attributes['widget'])) + if 'icon' in attributes: + prefixes.append(Affix(self.view, attributes)) + + for affix in node.childNodes: + affix_attrs = node_attributes(affix) + if 'name' not in affix_attrs: + affix_attrs['name'] = attributes['name'] + if affix.tagName == 'prefix': + list_ = prefixes + else: + list_ = suffixes + list_.append(Affix(self, affix_attrs)) + + for prefix in prefixes: + column.pack_start(prefix.renderer, expand=prefix.expand) + column.set_cell_data_func(prefix.renderer, prefix.setter) + column.pack_start(widget.renderer, expand=True) + column.set_cell_data_func(widget.renderer, widget.setter) + for suffix in suffixes: + column.pack_start(suffix.renderer, expand=suffix.expand) + column.set_cell_data_func(suffix.renderer, suffix.setter) + + self._set_column_widget(column, attributes, align=widget.align) + self._set_column_width(column, attributes) + + if (not self.view.attributes.get('sequence') + and not self.view.children_field + and self.field_attrs[name].get('sortable', True)): + column.connect('clicked', self.view.sort_model) + + self.view.treeview.append_column(column) + + if 'sum' in attributes: + text = attributes['sum'] + _(':') + label, sum_ = Gtk.Label(label=text), Gtk.Label() + + hbox = Gtk.HBox() + hbox.pack_start(label, expand=True, fill=False, padding=2) + hbox.pack_start(sum_, expand=True, fill=False, padding=2) + hbox.show_all() + self.view.sum_box.pack_start( + hbox, expand=False, fill=False, padding=0) + + self.view.sum_widgets.append((attributes['name'], sum_)) + + def _parse_button(self, node, attributes): + button = Button(self.view, attributes) + self.view.state_widgets.append(button) + + column = Gtk.TreeViewColumn( + attributes.get('string', ''), button.renderer) + column._type = 'button' + column.name = None + column.set_cell_data_func(button.renderer, button.setter) + + self._set_column_widget(column, attributes, arrow=False) + self._set_column_width(column, attributes) + + decoder = PYSONDecoder(self.view.screen.context) + column.set_visible( + not decoder.decode(attributes.get('tree_invisible', '0'))) + + self.view.treeview.append_column(column) + + def _set_column_widget(self, column, attributes, arrow=True, align=0.5): + hbox = Gtk.HBox(homogeneous=False, spacing=2) + label = Gtk.Label(label=attributes['string']) + field = self.field_attrs.get(attributes['name'], {}) + if field and self.view.editable: + required = field.get('required') + readonly = field.get('readonly') + common.apply_label_attributes(label, readonly, required) + label.show() + help = attributes.get('help') + if help: + tooltips = Tooltips() + tooltips.set_tip(label, help) + tooltips.enable() + if arrow: + arrow_widget = Gtk.Arrow( + arrow_type=Gtk.ArrowType.NONE, shadow_type=Gtk.ShadowType.NONE) + arrow_widget.show() + column.arrow = arrow_widget + hbox.pack_start(label, expand=True, fill=True, padding=0) + if arrow: + hbox.pack_start(arrow_widget, expand=False, fill=False, padding=0) + column.set_clickable(True) + hbox.show() + column.set_widget(hbox) + column.set_alignment(align) + + def _set_column_width(self, column, attributes): + default_width = { + 'integer': 60, + 'biginteger': 60, + 'float': 80, + 'numeric': 80, + 'timedelta': 100, + 'date': 100, + 'datetime': 100, + 'time': 100, + 'selection': 90, + 'char': 100, + 'one2many': 50, + 'many2many': 50, + 'boolean': 20, + 'binary': 200, + } + + screen = self.view.screen + width = screen.tree_column_width[screen.model_name].get(column.name) + field_attrs = self.field_attrs.get(attributes['name'], {}) + if not width: + if 'width' in attributes: + width = int(attributes['width']) + elif field_attrs: + width = default_width.get(field_attrs['type'], 100) + else: + width = 80 + column.width = width + if width > 0: + column.set_fixed_width(width) + column.set_min_width(1) + + expand = attributes.get('expand', False) + column.set_expand(expand) + column.set_resizable(True) + if attributes.get('widget') != 'text': + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + + class ViewTree(View): + view_type = 'tree' + xml_parser = TreeXMLViewParser def __init__(self, screen, xml, children_field): - super(ViewTree, self).__init__(screen, xml) - self.view_type = 'tree' - self.widgets = defaultdict(list) - self.state_widgets = [] self.children_field = children_field self.sum_widgets = [] self.sum_box = Gtk.HBox() self.reload = False - if self.attributes.get('editable') and not screen.readonly: - self.treeview = EditableTreeView(self.attributes['editable'], self) + self.treeview = None + editable = xml.getAttribute('editable') + if editable and not screen.readonly: + self.treeview = EditableTreeView(editable, self) grid_lines = Gtk.TreeViewGridLines.BOTH else: self.treeview = TreeView(self) grid_lines = Gtk.TreeViewGridLines.VERTICAL + + super().__init__(screen, xml) + self.mnemonic_widget = self.treeview - self.parse(xml) + # Add last column if necessary + for column in self.treeview.get_columns(): + if column.get_expand(): + break + else: + column = Gtk.TreeViewColumn() + column._type = 'fill' + column.name = None + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + self.treeview.append_column(column) self.treeview.set_property('rules-hint', True) self.treeview.set_property('enable-grid-lines', grid_lines) @@ -307,231 +495,12 @@ class ViewTree(View): self.display() - def parse(self, xml): - for node in xml.childNodes: - if node.nodeType != node.ELEMENT_NODE: - continue - if node.tagName == 'field': - self._parse_field(node) - elif node.tagName == 'button': - self._parse_button(node) - - self.add_last_column() - - def _parse_field(self, node): - group = self.group - node_attrs = node_attributes(node) - name = node_attrs['name'] - field = group.fields[name] - for b_field in ('readonly', 'expand'): - if b_field in node_attrs: - node_attrs[b_field] = bool(int(node_attrs[b_field])) - for i_field in ('width', 'height'): - if i_field in node_attrs: - node_attrs[i_field] = int(node_attrs[i_field]) - if 'widget' not in node_attrs: - node_attrs['widget'] = field.attrs['type'] - - for attr in ('relation', 'domain', 'selection', - 'relation_field', 'string', 'views', 'invisible', - 'add_remove', 'sort', 'context', 'size', 'filename', - 'autocomplete', 'translate', 'create', 'delete', - 'selection_change_with', 'schema_model'): - if (attr in field.attrs - and attr not in node_attrs): - node_attrs[attr] = field.attrs[attr] - - Widget = self.get_widget(node_attrs['widget']) - widget = Widget(self, node_attrs) - self.widgets[name].append(widget) - - column = Gtk.TreeViewColumn(node_attrs['string']) - column._type = 'field' - column.name = name - - prefixes = [] - suffixes = list(widget.suffixes) - if node_attrs['widget'] in ('url', 'email', 'callto', 'sip'): - prefixes.append(Affix(self, node_attrs, - protocol=node_attrs['widget'])) - if 'icon' in node_attrs: - prefixes.append(Affix(self, node_attrs)) - for affix in node.childNodes: - affix_attrs = node_attributes(affix) - if 'name' not in affix_attrs: - affix_attrs['name'] = name - if affix.tagName == 'prefix': - list_ = prefixes - else: - list_ = suffixes - list_.append(Affix(self, affix_attrs)) - prefixes.extend(widget.prefixes) - - for prefix in prefixes: - column.pack_start(prefix.renderer, expand=prefix.expand) - column.set_cell_data_func(prefix.renderer, - prefix.setter) - - column.pack_start(widget.renderer, expand=True) - column.set_cell_data_func(widget.renderer, widget.setter) - - for suffix in suffixes: - column.pack_start(suffix.renderer, expand=suffix.expand) - column.set_cell_data_func(suffix.renderer, - suffix.setter) - - self.set_column_widget(column, field, node_attrs, align=widget.align) - self.set_column_width(column, field, node_attrs) - - if (not self.attributes.get('sequence') - and not self.children_field - and field.attrs.get('sortable', True)): - column.connect('clicked', self.sort_model) - - self.treeview.append_column(column) - - self.add_sum(node_attrs) - - def _parse_button(self, node): - node_attrs = node_attributes(node) - widget = Button(self, node_attrs) - self.state_widgets.append(widget) - - column = Gtk.TreeViewColumn(node_attrs.get('string', ''), - widget.renderer) - column._type = 'button' - column.name = None - column.set_cell_data_func(widget.renderer, widget.setter) - - self.set_column_widget(column, None, node_attrs, arrow=False) - self.set_column_width(column, None, node_attrs) - - decoder = PYSONDecoder(self.screen.context) - column.set_visible( - not decoder.decode(node_attrs.get('tree_invisible', '0'))) - - self.treeview.append_column(column) - - WIDGETS = { - 'char': Char, - 'many2one': M2O, - 'date': Date, - 'one2many': O2M, - 'many2many': M2M, - 'selection': Selection, - 'float': Float, - 'numeric': Float, - 'timedelta': TimeDelta, - 'integer': Int, - 'biginteger': Int, - 'time': Time, - 'boolean': Boolean, - 'text': Text, - 'url': URL, - 'email': URL, - 'callto': URL, - 'sip': URL, - 'progressbar': ProgressBar, - 'reference': Reference, - 'one2one': O2O, - 'binary': Binary, - 'image': Image, - } - - @classmethod - def get_widget(cls, name): - return cls.WIDGETS[name] - - def set_column_widget(self, column, field, attributes, - arrow=True, align=0.5): - hbox = Gtk.HBox(homogeneous=False, spacing=2) - label = Gtk.Label(label=attributes['string']) - if field and self.editable: - required = field.attrs.get('required') - readonly = field.attrs.get('readonly') - common.apply_label_attributes(label, readonly, required) - label.show() - help = None - if field and field.attrs.get('help'): - help = field.attrs['help'] - elif attributes.get('help'): - help = attributes['help'] - if help: - tooltips = Tooltips() - tooltips.set_tip(label, help) - tooltips.enable() - if arrow: - arrow_widget = Gtk.Arrow( - arrow_type=Gtk.ArrowType.NONE, shadow_type=Gtk.ShadowType.NONE) - arrow_widget.show() - column.arrow = arrow_widget - hbox.pack_start(label, expand=True, fill=True, padding=0) - if arrow: - hbox.pack_start( - arrow_widget, expand=False, fill=False, padding=0) - column.set_clickable(True) - hbox.show() - column.set_widget(hbox) - column.set_alignment(align) - - def set_column_width(self, column, field, attributes): - default_width = { - 'integer': 60, - 'biginteger': 60, - 'float': 80, - 'numeric': 80, - 'timedelta': 100, - 'date': 100, - 'datetime': 100, - 'time': 100, - 'selection': 90, - 'char': 100, - 'one2many': 50, - 'many2many': 50, - 'boolean': 20, - 'binary': 200, - } - - width = self.screen.tree_column_width[self.screen.model_name].get( - column.name) - if not width: - if 'width' in attributes: - width = int(attributes['width']) - elif field: - width = default_width.get(field.attrs['type'], 100) - else: - width = 80 - column.width = width - if width > 0: - column.set_fixed_width(width) - column.set_min_width(1) - - expand = attributes.get('expand', False) - column.set_expand(expand) - column.set_resizable(True) - if attributes.get('widget') != 'text': - column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) - def get_column_widget(self, column): 'Return the widget of the column' idx = [c for c in self.treeview.get_columns() if c.name == column.name].index(column) return self.widgets[column.name][idx] - def add_sum(self, attributes): - if 'sum' not in attributes: - return - text = attributes['sum'] + _(':') - label, sum_ = Gtk.Label(label=text), Gtk.Label() - - hbox = Gtk.HBox() - hbox.pack_start(label, expand=True, fill=False, padding=2) - hbox.pack_start(sum_, expand=True, fill=False, padding=2) - hbox.show_all() - self.sum_box.pack_start(hbox, expand=False, fill=False, padding=0) - - self.sum_widgets.append((attributes['name'], sum_)) - def sort_model(self, column): for col in self.treeview.get_columns(): if col != column and getattr(col, 'arrow', None): @@ -577,17 +546,6 @@ class ViewTree(View): else: arrow.set(direction, Gtk.ShadowType.IN) - def add_last_column(self): - for column in self.treeview.get_columns(): - if column.get_expand(): - break - else: - column = Gtk.TreeViewColumn() - column._type = 'fill' - column.name = None - column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) - self.treeview.append_column(column) - def set_drag_and_drop(self): dnd = False if self.children_field: