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)