changeset 66d5d0e48684 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset;node=66d5d0e48684
description:
        Add list-form view

        issue8185
        review255091002
diffstat:

 CHANGELOG                                            |    1 +
 tryton/gui/window/view_form/model/group.py           |   12 +-
 tryton/gui/window/view_form/screen/screen.py         |   16 +-
 tryton/gui/window/view_form/view/__init__.py         |   34 ++-
 tryton/gui/window/view_form/view/calendar_.py        |    4 +-
 tryton/gui/window/view_form/view/form.py             |    4 +-
 tryton/gui/window/view_form/view/form_gtk/textbox.py |   10 +-
 tryton/gui/window/view_form/view/graph.py            |    4 +-
 tryton/gui/window/view_form/view/list.py             |    4 +-
 tryton/gui/window/view_form/view/list_form.py        |  147 +++++++++++++++++++
 10 files changed, 205 insertions(+), 31 deletions(-)

diffs (426 lines):

diff -r cb0a2d8c4a99 -r 66d5d0e48684 CHANGELOG
--- a/CHANGELOG Mon Apr 08 10:14:39 2019 +0200
+++ b/CHANGELOG Mon Apr 08 15:06:36 2019 +0200
@@ -1,3 +1,4 @@
+* Add list-form view
 * Remove support for Python 3.4
 * Replace ColorSelectionDialog by ColorChooserDialog
 * Support HTML widget
diff -r cb0a2d8c4a99 -r 66d5d0e48684 tryton/gui/window/view_form/model/group.py
--- a/tryton/gui/window/view_form/model/group.py        Mon Apr 08 10:14:39 
2019 +0200
+++ b/tryton/gui/window/view_form/model/group.py        Mon Apr 08 15:06:36 
2019 +0200
@@ -100,7 +100,7 @@
         super(Group, self).insert(pos, record)
         self.__id2record[record.id] = record
         if not self.lock_signal:
-            self.signal('group-list-changed', ('record-added', record))
+            self.signal('group-list-changed', ('record-added', record, pos))
 
     def append(self, record):
         assert record.group is self
@@ -110,7 +110,8 @@
         super(Group, self).append(record)
         self.__id2record[record.id] = record
         if not self.lock_signal:
-            self.signal('group-list-changed', ('record-added', record))
+            self.signal('group-list-changed', (
+                    'record-added', record, self.__len__() - 1))
 
     def _remove(self, record):
         idx = self.index(record)
@@ -120,17 +121,20 @@
                     self.__getitem__(idx + 1)
             else:
                 self.__getitem__(idx - 1).next[id(self)] = None
-        self.signal('group-list-changed', ('record-removed', record))
+        self.signal('group-list-changed', ('record-removed', record, idx))
         super(Group, self).remove(record)
         del self.__id2record[record.id]
 
     def clear(self):
         # Use reversed order to minimize the cursor reposition as the cursor
         # has more chances to be on top of the list.
+        length = self.__len__()
         for record in reversed(self[:]):
-            self.signal('group-list-changed', ('record-removed', record))
+            self.signal(
+                'group-list-changed', ('record-removed', record, length - 1))
             record.destroy()
             self.pop()
+            length -= 1
         self.__id2record = {}
         self.record_removed, self.record_deleted = [], []
 
diff -r cb0a2d8c4a99 -r 66d5d0e48684 
tryton/gui/window/view_form/screen/screen.py
--- a/tryton/gui/window/view_form/screen/screen.py      Mon Apr 08 10:14:39 
2019 +0200
+++ b/tryton/gui/window/view_form/screen/screen.py      Mon Apr 08 15:06:36 
2019 +0200
@@ -569,8 +569,8 @@
         self.group.add_fields(fields)
         for field in fields:
             self.group.fields[field].views.add(view_id)
-        view = View.parse(self, xml_dom, view.get('field_childs'))
-        view.view_id = view_id
+        view = View.parse(
+            self, view_id, view['type'], xml_dom, view.get('field_childs'))
         self.views.append(view)
 
         return view
@@ -658,7 +658,7 @@
         current_view = self.current_view
         if not current_view:
             return
-        elif current_view.view_type in ('tree', 'form'):
+        elif current_view.view_type in ('tree', 'form', 'list-form'):
             current_view.set_cursor(new=new, reset_view=reset_view)
 
     def get(self):
@@ -946,6 +946,11 @@
                 record = next
                 break
             self.current_record = record
+        elif (view.view_type == 'list-form' and len(self.group)
+                and self.current_record in self.group):
+            idx = self.group.index(self.current_record)
+            if 0 <= idx < len(self.group) - 1:
+                self.current_record = self.group[idx + 1]
         elif view.view_type == 'calendar':
             record = self.current_record
             goocalendar = view.widgets.get('goocalendar')
@@ -1032,6 +1037,11 @@
                             if prev_id >= 0:
                                 self.current_record = events[prev_id].record
                             break
+        elif (view.view_type == 'list-form' and len(self.group)
+                and self.current_record in self.group):
+            idx = self.group.index(self.current_record)
+            if 0 < idx <= len(self.group) - 1:
+                self.current_record = self.group[idx - 1]
         else:
             self.current_record = self.group[-1] if len(self.group) else None
         self.set_cursor(reset_view=False)
diff -r cb0a2d8c4a99 -r 66d5d0e48684 
tryton/gui/window/view_form/view/__init__.py
--- a/tryton/gui/window/view_form/view/__init__.py      Mon Apr 08 10:14:39 
2019 +0200
+++ b/tryton/gui/window/view_form/view/__init__.py      Mon Apr 08 15:06:36 
2019 +0200
@@ -20,17 +20,19 @@
     scroll = None
     xml_parser = None
 
-    def __init__(self, screen, xml):
+    def __init__(self, view_id, screen, xml):
+        self.view_id = view_id
         self.screen = screen
         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)
+        if self.xml_parser:
+            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
@@ -58,22 +60,24 @@
         raise NotImplementedError
 
     @staticmethod
-    def parse(screen, xml, children_field):
+    def parse(screen, view_id, view_type, xml, children_field):
         from .list import ViewTree
         from .form import ViewForm
         from .graph import ViewGraph
         from .calendar_ import ViewCalendar
+        from .list_form import ViewListForm
 
         root, = xml.childNodes
-        tagname = root.tagName
-        if tagname == 'tree':
-            return ViewTree(screen, root, children_field)
-        elif tagname == 'form':
-            return ViewForm(screen, root)
-        elif tagname == 'graph':
-            return ViewGraph(screen, root)
-        elif tagname == 'calendar':
-            return ViewCalendar(screen, root)
+        if view_type == 'tree':
+            return ViewTree(view_id, screen, root, children_field)
+        elif view_type == 'form':
+            return ViewForm(view_id, screen, root)
+        elif view_type == 'graph':
+            return ViewGraph(view_id, screen, root)
+        elif view_type == 'calendar':
+            return ViewCalendar(view_id, screen, root)
+        elif view_type == 'list-form':
+            return ViewListForm(view_id, screen, root)
 
 
 class XMLViewParser:
diff -r cb0a2d8c4a99 -r 66d5d0e48684 
tryton/gui/window/view_form/view/calendar_.py
--- a/tryton/gui/window/view_form/view/calendar_.py     Mon Apr 08 10:14:39 
2019 +0200
+++ b/tryton/gui/window/view_form/view/calendar_.py     Mon Apr 08 15:06:36 
2019 +0200
@@ -55,7 +55,7 @@
     view_type = 'calendar'
     xml_parser = CalendarXMLViewParser
 
-    def __init__(self, screen, xml):
+    def __init__(self, view_id, screen, xml):
         self.widget = Gtk.VBox()
 
         if not Calendar_:
@@ -66,7 +66,7 @@
         self.widget.pack_end(
             scrolledWindow, expand=True, fill=True, padding=0)
 
-        super().__init__(screen, xml)
+        super().__init__(view_id, screen, xml)
 
         goocalendar = self.widgets['goocalendar']
         toolbar = self.widgets['toolbar']
diff -r cb0a2d8c4a99 -r 66d5d0e48684 tryton/gui/window/view_form/view/form.py
--- a/tryton/gui/window/view_form/view/form.py  Mon Apr 08 10:14:39 2019 +0200
+++ b/tryton/gui/window/view_form/view/form.py  Mon Apr 08 15:06:36 2019 +0200
@@ -393,7 +393,7 @@
     view_type = 'form'
     xml_parser = FormXMLViewParser
 
-    def __init__(self, screen, xml):
+    def __init__(self, view_id, screen, xml):
         self.notebooks = []
         self.expandables = []
 
@@ -413,7 +413,7 @@
         self.widget = vbox
         self.viewport = vp
 
-        super().__init__(screen, xml)
+        super().__init__(view_id, screen, xml)
 
     def get_fields(self):
         return list(self.widgets.keys())
diff -r cb0a2d8c4a99 -r 66d5d0e48684 
tryton/gui/window/view_form/view/form_gtk/textbox.py
--- a/tryton/gui/window/view_form/view/form_gtk/textbox.py      Mon Apr 08 
10:14:39 2019 +0200
+++ b/tryton/gui/window/view_form/view/form_gtk/textbox.py      Mon Apr 08 
15:06:36 2019 +0200
@@ -2,7 +2,7 @@
 # this repository contains the full copyright notices and license terms.
 import logging
 
-from gi.repository import Gtk
+from gi.repository import Gtk, Gdk
 try:
     from gi.repository import GtkSpell
 except ImportError:
@@ -32,6 +32,10 @@
         self.textview.connect('focus-out-event',
             lambda x, y: self._focus_out())
         self.textview.connect('key-press-event', self.send_modified)
+        # The click is grabbed by ListBox widget in this case user can never
+        # set the input with a click
+        self.textview.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
+        self.textview.connect_after('button-press-event', self._button_press)
         self.scrolledwindow.add(self.textview)
         self.scrolledwindow.show_all()
 
@@ -56,6 +60,10 @@
         textview.set_accepts_tab(False)
         return textview
 
+    def _button_press(self, textview, event):
+        textview.grab_focus()
+        return True
+
     def translate_widget(self):
         box = Gtk.VBox()
         scrolledwindow = Gtk.ScrolledWindow()
diff -r cb0a2d8c4a99 -r 66d5d0e48684 tryton/gui/window/view_form/view/graph.py
--- a/tryton/gui/window/view_form/view/graph.py Mon Apr 08 10:14:39 2019 +0200
+++ b/tryton/gui/window/view_form/view/graph.py Mon Apr 08 15:06:36 2019 +0200
@@ -69,9 +69,9 @@
     editable = False
     xml_parser = GraphXMLViewParser
 
-    def __init__(self, screen, xml):
+    def __init__(self, view_id, screen, xml):
         self.widget = event = Gtk.EventBox()
-        super().__init__(screen, xml)
+        super().__init__(view_id, screen, xml)
         event.connect('button-press-event', self.button_press)
 
     def __getitem__(self, name):
diff -r cb0a2d8c4a99 -r 66d5d0e48684 tryton/gui/window/view_form/view/list.py
--- a/tryton/gui/window/view_form/view/list.py  Mon Apr 08 10:14:39 2019 +0200
+++ b/tryton/gui/window/view_form/view/list.py  Mon Apr 08 15:06:36 2019 +0200
@@ -429,7 +429,7 @@
     view_type = 'tree'
     xml_parser = TreeXMLViewParser
 
-    def __init__(self, screen, xml, children_field):
+    def __init__(self, view_id, screen, xml, children_field):
         self.children_field = children_field
         self.sum_widgets = []
         self.sum_box = Gtk.HBox()
@@ -443,7 +443,7 @@
             self.treeview = TreeView(self)
             grid_lines = Gtk.TreeViewGridLines.VERTICAL
 
-        super().__init__(screen, xml)
+        super().__init__(view_id, screen, xml)
 
         self.mnemonic_widget = self.treeview
 
diff -r cb0a2d8c4a99 -r 66d5d0e48684 
tryton/gui/window/view_form/view/list_form.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tryton/gui/window/view_form/view/list_form.py     Mon Apr 08 15:06:36 
2019 +0200
@@ -0,0 +1,147 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+from gi.repository import GObject, Gio, Gtk
+
+from tryton.common import common
+from . import View
+from .form import ViewForm
+
+
+class ListBoxViewForm(ViewForm):
+
+    def __init__(self, view_id, screen, xml):
+        super().__init__(view_id, screen, xml)
+        self._record = None
+        self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
+
+    @property
+    def record(self):
+        return self._record
+
+    @record.setter
+    def record(self, value):
+        self._record = value
+
+
+class ListBoxItem(GObject.Object):
+
+    def __init__(self, record):
+        super().__init__()
+        self.record = record
+
+
+class ListBoxModel(GObject.Object, Gio.ListModel):
+
+    def __init__(self, group):
+        super().__init__()
+        self.group = group
+        self._records = {}
+
+    def do_get_item(self, position):
+        if position >= len(self.group):
+            return None
+        record = self.group[position]
+        if record.id not in self._records:
+            self._records[record.id] = ListBoxItem(record)
+        return self._records[record.id]
+
+    def do_get_item_type(self):
+        return ListBoxItem
+
+    def do_get_n_items(self):
+        return len(self.group)
+
+
+class ViewListForm(View):
+    editable = True
+    xml_parser = None
+
+    def __init__(self, view_id, screen, xml):
+        super().__init__(view_id, screen, xml)
+        self.view_type = 'list-form'
+
+        self.form_xml = xml
+        self.listbox = Gtk.ListBox.new()
+        self.listbox.connect('row-selected', self._row_selected)
+        self.listbox.props.activate_on_single_click = False
+        self.listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
+        self.widget = Gtk.ScrolledWindow.new()
+        self.widget.add_with_viewport(self.listbox)
+        self._model = None
+        self._view_forms = []
+
+    def display(self):
+        if self._model is None or self._model.group is not self.group:
+            self._view_forms = []
+            self._model = ListBoxModel(self.group)
+            self.listbox.bind_model(self._model, self._create_form)
+        for idx, view_form in enumerate(self._view_forms):
+            view_form.display()
+
+    def _create_form(self, item):
+        view_form = ListBoxViewForm(self.view_id, self.screen, self.form_xml)
+        view_form.record = item.record
+        view_form.widget.props.margin = 3
+        self._view_forms.append(view_form)
+        frame = Gtk.Frame.new()
+        frame.add(view_form.widget)
+        frame.show_all()
+        return frame
+
+    def set_value(self):
+        for view_form in self._view_forms:
+            view_form.set_value()
+
+    def get_fields(self):
+        if self._view_forms:
+            return self._view_forms[0].get_fields()
+        return []
+
+    def get_buttons(self):
+        if self._view_forms:
+            return self._view_forms[0].get_buttons()
+        return []
+
+    def destroy(self):
+        for view_form in self._view_forms:
+            view_form.destroy()
+        self.widget.destroy()
+
+    @property
+    def selected_records(self):
+        selected_rows = self.listbox.get_selected_rows()
+        return [self.group[r.get_index()] for r in selected_rows]
+
+    def group_list_changed(self, group, signal):
+        action = signal[0]
+        if action == 'record-added':
+            position = signal[2]
+            self._model.emit('items-changed', position, 0, 1)
+            self._view_forms.insert(position, self._view_forms.pop())
+        elif action == 'record-removed':
+            position = signal[2]
+            self._model.emit('items-changed', position, 1, 0)
+            self._view_forms.pop(position)
+
+    def set_cursor(self, new=False, reset_view=True):
+        for idx, form in enumerate(self._view_forms):
+            if form.record == self.record:
+                self._select_show_row(idx)
+                break
+
+    def _row_selected(self, listbox, row):
+        if not row:
+            return
+        self.record = self.group[row.get_index()]
+
+    @common.idle_add
+    def _select_show_row(self, index):
+        self.listbox.unselect_all()
+        row = self.listbox.get_row_at_index(index)
+        if not row:
+            return
+        self.listbox.select_row(row)
+        y_position = row.translate_coordinates(self.listbox, 0, 0)[1]
+        y_size = row.get_allocated_height()
+        vadjustment = self.widget.get_vadjustment()
+        vadjustment.clamp_page(y_position, y_position + y_size)

Reply via email to