mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-03-10 05:34:36 +00:00
Improved hscommon.gui docs
Added docs for Table and Row in hscommon.gui.table.
This commit is contained in:
parent
da06ef8cad
commit
3734bd6f6c
13
help/en/developer/hscommon/gui/table.rst
Normal file
13
help/en/developer/hscommon/gui/table.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
hscommon.gui.table
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. automodule:: hscommon.gui.table
|
||||||
|
|
||||||
|
.. autoclass:: Table
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
.. autoclass:: Row
|
||||||
|
:members:
|
||||||
|
:private-members:
|
||||||
|
|
@ -13,3 +13,4 @@ hscommon
|
|||||||
gui/base
|
gui/base
|
||||||
gui/text_field
|
gui/text_field
|
||||||
gui/selectable_list
|
gui/selectable_list
|
||||||
|
gui/table
|
||||||
|
@ -11,15 +11,57 @@ from collections import MutableSequence, namedtuple
|
|||||||
from .base import GUIObject
|
from .base import GUIObject
|
||||||
from .selectable_list import Selectable
|
from .selectable_list import Selectable
|
||||||
|
|
||||||
# We used to directly subclass list, but it caused problems at some point with deepcopy
|
|
||||||
|
|
||||||
# Adding and removing footer here and there might seem (and is) hackish, but it's much simpler than
|
# We used to directly subclass list, but it caused problems at some point with deepcopy
|
||||||
# the alternative, which is to override magic methods and adjust the results. When we do that, there
|
|
||||||
# the slice stuff that we have to implement and it gets quite complex.
|
|
||||||
# Moreover, the most frequent operation on a table is __getitem__, and making checks to know whether
|
|
||||||
# the key is a header or footer at each call would make that operation, which is the most used,
|
|
||||||
# slower.
|
|
||||||
class Table(MutableSequence, Selectable):
|
class Table(MutableSequence, Selectable):
|
||||||
|
"""Sortable and selectable sequence of :class:`Row`.
|
||||||
|
|
||||||
|
In fact, the Table is very similar to :class:`~hscommon.gui.selectable_list.SelectableList` in
|
||||||
|
practice and differs mostly in principle. Their difference lies in the nature of their items
|
||||||
|
theiy manage. With the Table, rows usually have many properties, presented in columns, and they
|
||||||
|
have to subclass :class:`Row`.
|
||||||
|
|
||||||
|
Usually used with :class:`~hscommon.gui.column.Column`.
|
||||||
|
|
||||||
|
Subclasses :class:`~hscommon.gui.selectable_list.Selectable`.
|
||||||
|
|
||||||
|
.. attribute:: header
|
||||||
|
.. attribute:: footer
|
||||||
|
|
||||||
|
When set to something else than ``None``, represent rows that will always be kept in first
|
||||||
|
and/or last position, regardless of sorting. ``len()`` and indexing will include them, which
|
||||||
|
means that if there's a header, ``table[0]`` returns it and if there's a footer,
|
||||||
|
``table[-1]`` returns it. To make things short, all list-like functions work with header and
|
||||||
|
footer "on". But things get fuzzy for ``append()`` and ``insert()`` because these will
|
||||||
|
ensure that no "normal" row gets inserted before the header or after the footer.
|
||||||
|
|
||||||
|
Adding and removing footer here and there might seem (and is) hackish, but it's much simpler
|
||||||
|
than the alternative (when, of course, you need such a feature), which is to override magic
|
||||||
|
methods and adjust the results. When we do that, there the slice stuff that we have to
|
||||||
|
implement and it gets quite complex. Moreover, the most frequent operation on a table is
|
||||||
|
``__getitem__``, and making checks to know whether the key is a header or footer at each
|
||||||
|
call would make that operation, which is the most used, slower.
|
||||||
|
|
||||||
|
.. attribute:: row_count
|
||||||
|
|
||||||
|
Number or rows in the table (without counting header and footer).
|
||||||
|
|
||||||
|
.. attribute:: rows
|
||||||
|
|
||||||
|
List of rows in the table, excluding header and footer.
|
||||||
|
|
||||||
|
.. attribute:: selected_row
|
||||||
|
|
||||||
|
:class:`Row`. *get/set*. Selected row, based on
|
||||||
|
:attr:`~hscommon.gui.selectable_list.Selectable.selected_index`. When setting this
|
||||||
|
attribute, we look up the index of the row and set the selected index from there. If the
|
||||||
|
row isn't in the list, selection isn't changed.
|
||||||
|
|
||||||
|
.. attribute:: selected_rows
|
||||||
|
|
||||||
|
List of :class:`Row`. *read-only*. List of selected rows based on
|
||||||
|
:attr:`~hscommon.gui.selectable_list.Selectable.selected_indexes`.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Selectable.__init__(self)
|
Selectable.__init__(self)
|
||||||
self._rows = []
|
self._rows = []
|
||||||
@ -44,12 +86,21 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._rows.__setitem__(key, value)
|
self._rows.__setitem__(key, value)
|
||||||
|
|
||||||
def append(self, item):
|
def append(self, item):
|
||||||
|
"""Appends ``item`` at the end of the table.
|
||||||
|
|
||||||
|
If there's a footer, the item is inserted before it.
|
||||||
|
"""
|
||||||
if self._footer is not None:
|
if self._footer is not None:
|
||||||
self._rows.insert(-1, item)
|
self._rows.insert(-1, item)
|
||||||
else:
|
else:
|
||||||
self._rows.append(item)
|
self._rows.append(item)
|
||||||
|
|
||||||
def insert(self, index, item):
|
def insert(self, index, item):
|
||||||
|
"""Inserts ``item`` at ``index`` in the table.
|
||||||
|
|
||||||
|
If there's a header, will make sure we don't insert before it, and if there's a footer, will
|
||||||
|
make sure that we don't insert after it.
|
||||||
|
"""
|
||||||
if (self._header is not None) and (index == 0):
|
if (self._header is not None) and (index == 0):
|
||||||
index = 1
|
index = 1
|
||||||
if (self._footer is not None) and (index >= len(self)):
|
if (self._footer is not None) and (index >= len(self)):
|
||||||
@ -57,6 +108,10 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._rows.insert(index, item)
|
self._rows.insert(index, item)
|
||||||
|
|
||||||
def remove(self, row):
|
def remove(self, row):
|
||||||
|
"""Removes ``row`` from table.
|
||||||
|
|
||||||
|
If ``row`` is a header or footer, that header or footer will be set to ``None``.
|
||||||
|
"""
|
||||||
if row is self._header:
|
if row is self._header:
|
||||||
self._header = None
|
self._header = None
|
||||||
if row is self._footer:
|
if row is self._footer:
|
||||||
@ -65,6 +120,14 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._check_selection_range()
|
self._check_selection_range()
|
||||||
|
|
||||||
def sort_by(self, column_name, desc=False):
|
def sort_by(self, column_name, desc=False):
|
||||||
|
"""Sort table by ``column_name``.
|
||||||
|
|
||||||
|
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
||||||
|
|
||||||
|
If ``desc`` is ``True``, sort order is reversed.
|
||||||
|
|
||||||
|
If present, header and footer will always be first and last, respectively.
|
||||||
|
"""
|
||||||
if self._header is not None:
|
if self._header is not None:
|
||||||
self._rows.pop(0)
|
self._rows.pop(0)
|
||||||
if self._footer is not None:
|
if self._footer is not None:
|
||||||
@ -236,6 +299,28 @@ class GUITable(Table, GUIObject):
|
|||||||
|
|
||||||
|
|
||||||
class Row:
|
class Row:
|
||||||
|
"""Represents a row in a :class:`Table`.
|
||||||
|
|
||||||
|
It holds multiple values to be represented through columns. It's its role to prepare data
|
||||||
|
fetched from model instances into ready-to-present-in-a-table fashion. You will do this in
|
||||||
|
:meth:`load`.
|
||||||
|
|
||||||
|
When you do this, you'll put the result into arbitrary attributes, which will later be fetched
|
||||||
|
by your table for presentation to the user.
|
||||||
|
|
||||||
|
You can organize your attributes in whatever way you want, but there's a convention you can
|
||||||
|
follow if you want to minimize subclassing and use default behavior:
|
||||||
|
|
||||||
|
1. Attribute name = column name. If your attribute is ``foobar``, whenever we refer to
|
||||||
|
``column_name``, you refer to that attribute with the column name ``foobar``.
|
||||||
|
2. Public attributes are for *formatted* value, that is, user readable strings.
|
||||||
|
3. Underscore prefix is the unformatted (computable) value. For example, you could have
|
||||||
|
``_foobar`` at ``42`` and ``foobar`` at ``"42 seconds"`` (what you present to the user).
|
||||||
|
4. Unformatted values are used for sorting.
|
||||||
|
5. If your column name is a python keyword, add an underscore suffix (``from_``).
|
||||||
|
|
||||||
|
Of course, this is only default behavior. This can be overriden.
|
||||||
|
"""
|
||||||
def __init__(self, table):
|
def __init__(self, table):
|
||||||
super(Row, self).__init__()
|
super(Row, self).__init__()
|
||||||
self.table = table
|
self.table = table
|
||||||
@ -248,19 +333,38 @@ class Row:
|
|||||||
|
|
||||||
#--- Virtual
|
#--- Virtual
|
||||||
def can_edit(self):
|
def can_edit(self):
|
||||||
|
"""(Virtual) Whether the whole row can be edited.
|
||||||
|
|
||||||
|
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
||||||
|
:meth:`can_edit_cell`.
|
||||||
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
"""(Virtual/Required) Loads up values from the model to be presented in the table.
|
||||||
|
|
||||||
|
Usually, our model instances contain values that are not quite ready for display. If you
|
||||||
|
have number formatting, display calculations and other whatnots to perform, you do it here
|
||||||
|
and then you put the result in an arbitrary attribute of the row.
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
"""(Virtual/Required) Saves user edits into your model.
|
||||||
|
|
||||||
|
If your table is editable, this is called when the user commits his changes. Usually, these
|
||||||
|
are typed up stuff, or selected indexes. You have to do proper parsing and reference
|
||||||
|
linking, and save that stuff into your model.
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def sort_key_for_column(self, column_name):
|
def sort_key_for_column(self, column_name):
|
||||||
# Most of the time, the adequate sort key for a column is the column name with '_' prepended
|
"""(Virtual) Return the value that is to be used to sort by column ``column_name``.
|
||||||
# to it. This member usually corresponds to the unformated version of the column. If it's
|
|
||||||
# not there, we try the column_name without underscores
|
By default, looks for an attribute with the same name as ``column_name``, but with an
|
||||||
# Of course, override for exceptions.
|
underscore prefix ("unformatted value"). If there's none, tries without the underscore. If
|
||||||
|
there's none, raises ``AttributeError``.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return getattr(self, '_' + column_name)
|
return getattr(self, '_' + column_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -268,6 +372,18 @@ class Row:
|
|||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def can_edit_cell(self, column_name):
|
def can_edit_cell(self, column_name):
|
||||||
|
"""Returns whether cell for column ``column_name`` can be edited.
|
||||||
|
|
||||||
|
By the default, the check is done in many steps:
|
||||||
|
|
||||||
|
1. We check whether the whole row can be edited with :meth:`can_edit`. If it can't, the cell
|
||||||
|
can't either.
|
||||||
|
2. If the column doesn't exist as an attribute, we can't edit.
|
||||||
|
3. If we have an attribute ``can_edit_<column_name>``, return that.
|
||||||
|
4. Check if our attribute is a property. If it's not, it's not editable.
|
||||||
|
5. If our attribute is in fact a property, check whether the property is "settable" (has a
|
||||||
|
``fset`` method). The cell is editable only if the property is "settable".
|
||||||
|
"""
|
||||||
if not self.can_edit():
|
if not self.can_edit():
|
||||||
return False
|
return False
|
||||||
# '_' is in case column is a python keyword
|
# '_' is in case column is a python keyword
|
||||||
@ -286,11 +402,21 @@ class Row:
|
|||||||
return bool(getattr(prop, 'fset', None))
|
return bool(getattr(prop, 'fset', None))
|
||||||
|
|
||||||
def get_cell_value(self, attrname):
|
def get_cell_value(self, attrname):
|
||||||
|
"""Get cell value for ``attrname``.
|
||||||
|
|
||||||
|
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
||||||
|
alternative value storage mechanisms.
|
||||||
|
"""
|
||||||
if attrname == 'from':
|
if attrname == 'from':
|
||||||
attrname = 'from_'
|
attrname = 'from_'
|
||||||
return getattr(self, attrname)
|
return getattr(self, attrname)
|
||||||
|
|
||||||
def set_cell_value(self, attrname, value):
|
def set_cell_value(self, attrname, value):
|
||||||
|
"""Set cell value to ``value`` for ``attrname``.
|
||||||
|
|
||||||
|
By default, does a simple ``setattr()``, but it is used to allow subclasses to have
|
||||||
|
alternative value storage mechanisms.
|
||||||
|
"""
|
||||||
if attrname == 'from':
|
if attrname == 'from':
|
||||||
attrname = 'from_'
|
attrname = 'from_'
|
||||||
setattr(self, attrname, value)
|
setattr(self, attrname, value)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user