From da06ef8cad0507f42d70e5d530df2a3988235c98 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Sun, 24 Nov 2013 13:53:52 -0500 Subject: [PATCH] Improved hscommon.gui docs --- help/en/developer/hscommon/gui/base.rst | 8 ++ .../hscommon/gui/selectable_list.rst | 19 ++++ help/en/developer/hscommon/gui/text_field.rst | 11 ++ help/en/developer/hscommon/index.rst | 3 + hscommon/gui/base.py | 47 +++++--- hscommon/gui/selectable_list.py | 100 +++++++++++++++--- hscommon/gui/text_field.py | 53 +++++++++- 7 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 help/en/developer/hscommon/gui/base.rst create mode 100644 help/en/developer/hscommon/gui/selectable_list.rst create mode 100644 help/en/developer/hscommon/gui/text_field.rst diff --git a/help/en/developer/hscommon/gui/base.rst b/help/en/developer/hscommon/gui/base.rst new file mode 100644 index 00000000..0119f68c --- /dev/null +++ b/help/en/developer/hscommon/gui/base.rst @@ -0,0 +1,8 @@ +hscommon.gui.base +================= + +.. automodule:: hscommon.gui.base + + .. autoclass:: GUIObject + :members: + :private-members: diff --git a/help/en/developer/hscommon/gui/selectable_list.rst b/help/en/developer/hscommon/gui/selectable_list.rst new file mode 100644 index 00000000..b8a8d2bf --- /dev/null +++ b/help/en/developer/hscommon/gui/selectable_list.rst @@ -0,0 +1,19 @@ +hscommon.gui.selectable_list +============================ + +.. automodule:: hscommon.gui.selectable_list + + .. autoclass:: Selectable + :members: + :private-members: + + .. autoclass:: SelectableList + :members: + :private-members: + + .. autoclass:: GUISelectableList + :members: + :private-members: + + .. autoclass:: GUISelectableListView + :members: diff --git a/help/en/developer/hscommon/gui/text_field.rst b/help/en/developer/hscommon/gui/text_field.rst new file mode 100644 index 00000000..6625ce68 --- /dev/null +++ b/help/en/developer/hscommon/gui/text_field.rst @@ -0,0 +1,11 @@ +hscommon.gui.text_field +======================= + +.. automodule:: hscommon.gui.text_field + + .. autoclass:: TextField + :members: + :private-members: + + .. autoclass:: TextFieldView + :members: diff --git a/help/en/developer/hscommon/index.rst b/help/en/developer/hscommon/index.rst index c70b8295..01fb5664 100644 --- a/help/en/developer/hscommon/index.rst +++ b/help/en/developer/hscommon/index.rst @@ -10,3 +10,6 @@ hscommon notify path util + gui/base + gui/text_field + gui/selectable_list diff --git a/hscommon/gui/base.py b/hscommon/gui/base.py index 31b94db9..b258e3c9 100644 --- a/hscommon/gui/base.py +++ b/hscommon/gui/base.py @@ -13,23 +13,42 @@ class NoopGUI: def __getattr__(self, func_name): return noop -# A GUIObject is a cross-toolkit "model" representation of a GUI layer object, for example, a table. -# It acts as a cross-toolkit interface to multiple what we call here a "view". That view is a -# toolkit-specific controller to the actual view (an NSTableView, a QTableView, etc.). -# In our GUIObject, we need a reference to that toolkit-specific controller because some actions, -# have effects on it (for example, prompting it to refresh its data). The GUIObject is typically -# instantiated before its "view", that is why we set it as None on init. However, the GUI -# layer is supposed to set the view as soon as its toolkit-specific controller is instantiated. - -# When you subclass GUIObject, you will likely want to update its view on instantiation. That -# is why we call self.view.refresh() in _view_updated(). If you need another type of action on -# view instantiation, just override the method. class GUIObject: + """Cross-toolkit "model" representation of a GUI layer object. + + A ``GUIObject`` is a cross-toolkit "model" representation of a GUI layer object, for example, a + table. It acts as a cross-toolkit interface to multiple what we call here a :attr:`view`. That + view is a toolkit-specific controller to the actual view (an ``NSTableView``, a ``QTableView``, + etc.). In our GUIObject, we need a reference to that toolkit-specific controller because some + actions, have effects on it (for example, prompting it to refresh its data). The ``GUIObject`` + is typically instantiated before its :attr:`view`, that is why we set it as ``None`` on init. + However, the GUI layer is supposed to set the view as soon as its toolkit-specific controller is + instantiated. + + When you subclass ``GUIObject``, you will likely want to update its view on instantiation. That + is why we call ``self.view.refresh()`` in :meth:`_view_updated`. If you need another type of + action on view instantiation, just override the method. + + .. attribute:: view + + A reference to our toolkit-specific view controller. This view stats as ``None`` and has to + be set "manually". There's two times at which we set the view property: On initialization, + where we set the view that we'll use for our lifetime, and just before the view is + deallocated. We need to unset our view at that time to avoid calls to a deallocated instance + (which means a crash). + + To unset our view, we simple assign it to ``None``. + """ def __init__(self): self._view = None def _view_updated(self): - pass #virtual + """(Virtual) Called after :attr:`view` has been set. + + Doing nothing by default, this method is called after :attr:`view` has been set (it isn't + called when it's unset, however). Use this for initialization code that requires a view + (which is often the whole of the initialization code). + """ def has_view(self): return (self._view is not None) and (not isinstance(self._view, NoopGUI)) @@ -40,10 +59,6 @@ class GUIObject: @view.setter def view(self, value): - # There's two times at which we set the view property: On initialization, where we set the - # view that we'll use for your lifetime, and just before the view is deallocated. We need - # to unset our view at that time to avoid calls to a deallocated instance (which means a - # crash). if self._view is None: # Initial view assignment if value is None: diff --git a/hscommon/gui/selectable_list.py b/hscommon/gui/selectable_list.py index 9bf457f0..eaec0b8f 100644 --- a/hscommon/gui/selectable_list.py +++ b/hscommon/gui/selectable_list.py @@ -11,6 +11,22 @@ from collections import Sequence, MutableSequence from .base import GUIObject class Selectable(Sequence): + """Mix-in for a ``Sequence`` that manages its selection status. + + When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection + is held as a list of ``int`` indexes. Multiple selection is supported. + + .. attribute:: selected_index + + *int*. *get/set*. Thin wrapper around :attr:`selected_indexes`. Points to the first selected + index or ``None`` if it's empty. Using this property only makes sense if your selectable + sequence supports single selection only. + + .. attribute:: selected_indexes + + *list*. *get/set*. List of selected indexes. When setting the value, automatically removes + out-of-bounds indexes. The list is kept sorted. + """ def __init__(self): self._selected_indexes = [] @@ -26,16 +42,30 @@ class Selectable(Sequence): #--- Virtual def _update_selection(self): - # Takes the table's selection and does appropriates updates on the view and/or model, when - # appropriate. Common sense would dictate that when the selection doesn't change, we don't - # update anything (and thus don't call _update_selection() at all), but there are cases - # where it's false. For example, if our list updates its items but doesn't change its - # selection, we probably want to update the model's selection. A redesign of how this whole - # thing works is probably in order, but not now, there's too much breakage at once involved. - pass + """(Virtual) Updates the model's selection appropriately. + + Called after selection has been updated. Takes the table's selection and does appropriates + updates on the view and/or model. Common sense would dictate that when the selection doesn't + change, we don't update anything (and thus don't call ``_update_selection()`` at all), but + there are cases where it's false. For example, if our list updates its items but doesn't + change its selection, we probably want to update the model's selection. + + By default, does nothing. + + Important note: This is only called on :meth:`select`, not on changes to + :attr:`selected_indexes`. + """ + # A redesign of how this whole thing works is probably in order, but not now, there's too + # much breakage at once involved. #--- Public def select(self, indexes): + """Update selection to ``indexes``. + + :meth:`_update_selection` is called afterwards. + + :param list indexes: List of ``int`` that is to become the new selection. + """ if isinstance(indexes, int): indexes = [indexes] self.selected_indexes = indexes @@ -62,6 +92,10 @@ class Selectable(Sequence): class SelectableList(MutableSequence, Selectable): + """A list that can manage selection of its items. + + Subclasses :class:`Selectable`. Behaves like a ``list``. + """ def __init__(self, items=None): Selectable.__init__(self) if items: @@ -100,10 +134,14 @@ class SelectableList(MutableSequence, Selectable): #--- Virtual def _on_change(self): - pass + """(Virtual) Called whenever the contents of the list changes. + + By default, does nothing. + """ #--- Public def search_by_prefix(self, prefix): + # XXX Why the heck is this method here? prefix = prefix.lower() for index, s in enumerate(self): if s.lower().startswith(prefix): @@ -111,21 +149,57 @@ class SelectableList(MutableSequence, Selectable): return -1 -class GUISelectableList(SelectableList, GUIObject): - #--- View interface - # refresh() - # update_selection() - # +class GUISelectableListView: + """Expected interface for :class:`GUISelectableList`'s view. + *Not actually used in the code. For documentation purposes only.* + + Our view, some kind of list view or combobox, is expected to sync with the list's contents by + appropriately behave to all callbacks in this interface. + """ + def refresh(self): + """Refreshes the contents of the list widget. + + Ensures that the contents of the list widget is synced with the model. + """ + + def update_selection(self): + """Update selection status. + + Ensures that the list widget's selection is in sync with the model. + """ + +class GUISelectableList(SelectableList, GUIObject): + """Cross-toolkit list view. + + Represents a UI element presenting the user with a selectable list of items. + + Subclasses :class:`SelectableList` and :class:`~hscommon.gui.base.GUIObject`. Expected view: + :class:`GUISelectableListView`. + + :param iterable items: If specified, items to fill the list with initially. + """ def __init__(self, items=None): SelectableList.__init__(self, items) GUIObject.__init__(self) def _view_updated(self): + """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. + + Overrides :meth:`~hscommon.gui.base.GUIObject._view_updated`. + """ self.view.refresh() def _update_selection(self): + """Refreshes the view selection with :meth:`GUISelectableListView.update_selection`. + + Overrides :meth:`Selectable._update_selection`. + """ self.view.update_selection() def _on_change(self): + """Refreshes the view contents with :meth:`GUISelectableListView.refresh`. + + Overrides :meth:`SelectableList._on_change`. + """ self.view.refresh() diff --git a/hscommon/gui/text_field.py b/hscommon/gui/text_field.py index ab636bd2..d1935c1f 100644 --- a/hscommon/gui/text_field.py +++ b/hscommon/gui/text_field.py @@ -8,7 +8,44 @@ from .base import GUIObject from ..util import nonone +class TextFieldView: + """Expected interface for :class:`TextField`'s view. + + *Not actually used in the code. For documentation purposes only.* + + Our view is expected to sync with :attr:`TextField.text` "both ways", that is, update the + model's text when the user types something, but also update the text field when :meth:`refresh` + is called. + """ + def refresh(self): + """Refreshes the contents of the input widget. + + Ensures that the contents of the input widget is actually :attr:`TextField.text`. + """ + class TextField(GUIObject): + """Cross-toolkit text field. + + Represents a UI element allowing the user to input a text value. Its main attribute is + :attr:`text` which acts as the store of the said value. + + When our model value isn't a string, we have a built-in parsing/formatting mechanism allowing + us to directly retrieve/set our non-string value through :attr:`value`. + + Subclasses :class:`hscommon.gui.base.GUIObject`. Expected view: :class:`TextFieldView`. + + .. attribute:: text + + *str*. The text that is currently displayed in the widget. This property can be set. When it + is, :meth:`refresh` is called and the view is synced with our value. Always in sync with + :attr:`value`. + + .. attribute:: value + + The "parsed" representation of :attr:`text`. By default, it's a mirror of :attr:`text`, but + a subclass can override :meth:`_parse` and :meth:`_format` to have anything else. Always in + sync with :attr:`text`. + """ def __init__(self): GUIObject.__init__(self) self._text = '' @@ -16,13 +53,25 @@ class TextField(GUIObject): #--- Virtual def _parse(self, text): + """(Virtual) Parses ``text`` to put into :attr:`value`. + + Returns the parsed version of ``text``. Called whenever :attr:`text` changes. + """ return text def _format(self, value): + """(Virtual) Formats ``value`` to put into :attr:`text`. + + Returns the formatted version of ``value``. Called whenever :attr:`value` changes. + """ return value def _update(self, newvalue): - pass + """(Virtual) Called whenever we have a new value. + + Whenever our text/value store changes to a new value (different from the old one), this + method is called. By default, it does nothing but you can override it if you want. + """ #--- Override def _view_updated(self): @@ -30,6 +79,8 @@ class TextField(GUIObject): #--- Public def refresh(self): + """Triggers a view :meth:`~TextFieldView.refresh`. + """ self.view.refresh() @property