Improved hscommon.gui docs

This commit is contained in:
Virgil Dupras 2013-11-24 13:53:52 -05:00
parent 0b00171655
commit da06ef8cad
7 changed files with 211 additions and 30 deletions

View File

@ -0,0 +1,8 @@
hscommon.gui.base
=================
.. automodule:: hscommon.gui.base
.. autoclass:: GUIObject
:members:
:private-members:

View File

@ -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:

View File

@ -0,0 +1,11 @@
hscommon.gui.text_field
=======================
.. automodule:: hscommon.gui.text_field
.. autoclass:: TextField
:members:
:private-members:
.. autoclass:: TextFieldView
:members:

View File

@ -10,3 +10,6 @@ hscommon
notify
path
util
gui/base
gui/text_field
gui/selectable_list

View File

@ -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:

View File

@ -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()

View File

@ -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