Improved hscommon docs

This commit is contained in:
Virgil Dupras 2013-11-30 16:13:12 -05:00
parent b6bc5de79c
commit 7116674663
7 changed files with 353 additions and 7 deletions

View File

@ -0,0 +1,25 @@
hscommon.gui.column
============================
.. automodule:: hscommon.gui.column
.. autosummary::
Columns
Column
ColumnsView
PrefAccessInterface
.. autoclass:: Columns
:members:
:private-members:
.. autoclass:: Column
:members:
:private-members:
.. autoclass:: ColumnsView
:members:
.. autoclass:: PrefAccessInterface
:members:

View File

@ -0,0 +1,18 @@
hscommon.gui.progress_window
============================
.. automodule:: hscommon.gui.progress_window
.. autosummary::
ProgressWindow
ProgressWindowView
.. autoclass:: ProgressWindow
:members:
:private-members:
.. autoclass:: ProgressWindowView
:members:
:private-members:

View File

@ -0,0 +1,18 @@
hscommon.gui.tree
=================
.. automodule:: hscommon.gui.tree
.. autosummary::
Tree
Node
.. autoclass:: Tree
:members:
:private-members:
.. autoclass:: Node
:members:
:private-members:

View File

@ -14,3 +14,6 @@ hscommon
gui/text_field
gui/selectable_list
gui/table
gui/tree
gui/column
gui/progress_window

View File

@ -11,19 +11,92 @@ import copy
from .base import GUIObject
class Column:
"""Holds column attributes such as its name, width, visibility, etc.
These attributes are then used to correctly configure the column on the "view" side.
"""
def __init__(self, name, display='', visible=True, optional=False):
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
#: as :meth:`Columns.column_by_name`.
self.name = name
#: Immutable index of the column. Doesn't change even when columns are re-ordered. Used in
#: :meth:`Columns.column_by_index`.
self.logical_index = 0
#: Index of the column in the ordered set of columns.
self.ordered_index = 0
#: Width of the column.
self.width = 0
#: Default width of the column. This value usually depends on the platform and is set on
#: columns initialisation. It will be used if column restoration doesn't contain any
#: "remembered" widths.
self.default_width = 0
#: Display name (title) of the column.
self.display = display
#: Whether the column is visible.
self.visible = visible
#: Whether the column is visible by default. It will be used if column restoration doesn't
#: contain any "remembered" widths.
self.default_visible = visible
#: Whether the column can have :attr:`visible` set to false.
self.optional = optional
class ColumnsView:
"""Expected interface for :class:`Columns`'s view.
*Not actually used in the code. For documentation purposes only.*
Our view, the columns controller of a table or outline, is expected to properly respond to
callbacks.
"""
def restore_columns(self):
"""Update all columns according to the model.
When this is called, our view has to update the columns title, order and visibility of all
columns.
"""
def set_column_visible(self, colname, visible):
"""Update visibility of column ``colname``.
Called when the user toggles the visibility of a column, we must update the column
``colname``'s visibility status to ``visible``.
"""
class PrefAccessInterface:
"""Expected interface for :class:`Columns`'s prefaccess.
*Not actually used in the code. For documentation purposes only.*
"""
def get_default(self, key, fallback_value):
"""Retrieve the value for ``key`` in the currently running app's preference store.
If the key doesn't exist, return ``fallback_value``.
"""
def set_default(self, key, value):
"""Set the value ``value`` for ``key`` in the currently running app's preference store.
"""
class Columns(GUIObject):
"""Cross-toolkit GUI-enabled column set for tables or outlines.
Manages a column set's order, visibility and width. We also manage the persistence of these
attributes so that we can restore them on the next run.
Subclasses :class:`.GUIObject`. Expected view: :class:`ColumnsView`.
:param table: The table the columns belong to. It's from there that we retrieve our column
configuration and it must have a ``COLUMNS`` attribute which is a list of
:class:`Column`. We also call :meth:`~.GUITable.save_edits` on it from time to
time. Technically, this argument can also be a tree, but there's probably some
sorting in the code to do to support this option cleanly.
:param prefaccess: An object giving access to user preferences for the currently running app.
We use this to make column attributes persistent. Must follow
:class:`PrefAccessInterface`.
:param str savename: The name under which column preferences will be saved. This name is in fact
a prefix. Preferences are saved under more than one name, but they will all
have that same prefix.
"""
def __init__(self, table, prefaccess=None, savename=None):
GUIObject.__init__(self)
self.table = table
@ -59,40 +132,71 @@ class Columns(GUIObject):
#--- Public
def column_by_index(self, index):
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.
"""
return self.column_list[index]
def column_by_name(self, name):
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``.
"""
return self.coldata[name]
def columns_count(self):
"""Returns the number of columns in our set.
"""
return len(self.column_list)
def column_display(self, colname):
"""Returns display name for column named ``colname``, or ``''`` if there's none.
"""
return self._get_colname_attr(colname, 'display', '')
def column_is_visible(self, colname):
"""Returns visibility for column named ``colname``, or ``True`` if there's none.
"""
return self._get_colname_attr(colname, 'visible', True)
def column_width(self, colname):
"""Returns width for column named ``colname``, or ``0`` if there's none.
"""
return self._get_colname_attr(colname, 'width', 0)
def columns_to_right(self, colname):
"""Returns the list of all columns to the right of ``colname``.
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
civilization.
"""
column = self.coldata[colname]
index = column.ordered_index
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
def menu_items(self):
# Returns a list of (display_name, marked) items for each optional column in the current
# view (marked means that it's visible).
"""Returns a list of items convenient for quick visibility menu generation.
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
current view (``is_marked`` means that it's visible).
You can use this to generate a menu to let the user toggle the visibility of an optional
column. That is why we only show optional column, because the visibility of mandatory
columns can't be toggled.
"""
return [(c.display, c.visible) for c in self._optional_columns()]
def move_column(self, colname, index):
"""Moves column ``colname`` to ``index``.
The column will be placed just in front of the column currently having that index, or to the
end of the list if there's none.
"""
colnames = self.colnames
colnames.remove(colname)
colnames.insert(index, colname)
self.set_column_order(colnames)
def reset_to_defaults(self):
"""Reset all columns' width and visibility to their default values.
"""
self.set_column_order([col.name for col in self.column_list])
for col in self._optional_columns():
col.visible = col.default_visible
@ -100,9 +204,13 @@ class Columns(GUIObject):
self.view.restore_columns()
def resize_column(self, colname, newwidth):
"""Set column ``colname``'s width to ``newwidth``.
"""
self._set_colname_attr(colname, 'width', newwidth)
def restore_columns(self):
"""Restore's column persistent attributes from the last :meth:`save_columns`.
"""
if not (self.prefaccess and self.savename and self.coldata):
if (not self.savename) and (self.coldata):
# This is a table that will not have its coldata saved/restored. we should
@ -121,6 +229,8 @@ class Columns(GUIObject):
self.view.restore_columns()
def save_columns(self):
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`.
"""
if not (self.prefaccess and self.savename and self.coldata):
return
for col in self.column_list:
@ -131,20 +241,35 @@ class Columns(GUIObject):
self.prefaccess.set_default(pref_name, coldata)
def set_column_order(self, colnames):
"""Change the columns order so it matches the order in ``colnames``.
:param colnames: A list of column names in the desired order.
"""
colnames = (name for name in colnames if name in self.coldata)
for i, colname in enumerate(colnames):
col = self.coldata[colname]
col.ordered_index = i
def set_column_visible(self, colname, visible):
"""Set the visibility of column ``colname``.
"""
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, 'visible', visible)
self.view.set_column_visible(colname, visible)
def set_default_width(self, colname, width):
"""Set the default width or column ``colname``.
"""
self._set_colname_attr(colname, 'default_width', width)
def toggle_menu_item(self, index):
"""Toggles the visibility of an optional column.
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
is the index of them menu item in *that* menu that the user has clicked on to toggle it.
Returns whether the column in question ends up being visible or not.
"""
col = self._optional_columns()[index]
self.set_column_visible(col.name, not col.visible)
return col.visible
@ -152,9 +277,13 @@ class Columns(GUIObject):
#--- Properties
@property
def ordered_columns(self):
"""List of :class:`Column` in visible order.
"""
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
@property
def colnames(self):
"""List of column names in visible order.
"""
return [col.name for col in self.ordered_columns]

View File

@ -10,17 +10,68 @@ from jobprogress.performer import ThreadedJobPerformer
from .base import GUIObject
from .text_field import TextField
class ProgressWindowView:
"""Expected interface for :class:`ProgressWindow`'s view.
*Not actually used in the code. For documentation purposes only.*
Our view, some kind window with a progress bar, two labels and a cancel button, is expected
to properly respond to its callbacks.
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
"""
def show(self):
"""Show the dialog.
"""
def close(self):
"""Close the dialog.
"""
def set_progress(self, progress):
"""Set the progress of the progress bar to ``progress``.
Not all jobs are equally responsive on their job progress report and it is recommended that
you put your progressbar in "indeterminate" mode as long as you haven't received the first
``set_progress()`` call to avoid letting the user think that the app is frozen.
:param int progress: a value between ``0`` and ``100``.
"""
class ProgressWindow(GUIObject, ThreadedJobPerformer):
"""Cross-toolkit GUI-enabled progress window.
This class allows you to run a long running, `job enabled`_ function in a separate thread and
allow the user to follow its progress with a progress dialog.
To use it, you start your long-running job with :meth:`run` and then have your UI layer
regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call
:meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related
functions from the main thread.
We subclass :class:`.GUIObject` and ``ThreadedJobPerformer`` (from the ``jobprogress`` library).
Expected view: :class:`ProgressWindowView`.
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
an arbitrary id passed to :meth:`run`.
.. _job enabled: https://pypi.python.org/pypi/jobprogress
"""
def __init__(self, finish_func):
# finish_func(jobid) is the function that is called when a job is completed.
GUIObject.__init__(self)
ThreadedJobPerformer.__init__(self)
self._finish_func = finish_func
#: :class:`.TextField`. It contains that title you gave the job on :meth:`run`.
self.jobdesc_textfield = TextField()
#: :class:`.TextField`. It contains the job textual update that the function might yield
#: during its course.
self.progressdesc_textfield = TextField()
self.jobid = None
def cancel(self):
"""Call for a user-initiated job cancellation.
"""
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
# make sure that this doesn't lead us to think that the user acually cancelled the task, so
# we verify that the job is still running.
@ -28,8 +79,15 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
self.job_cancelled = True
def pulse(self):
# Call this regularly from the GUI main run loop.
# the values might change before setValue happens
"""Update progress reports in the GUI.
Call this regularly from the GUI main run loop. The values might change before
:meth:`ProgressWindowView.set_progress` happens.
If the job is finished, ``pulse()`` will take care of closing the window and re-raising any
exception that might have been raised during the job (in the main thread this time). If
there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action.
"""
last_progress = self.last_progress
last_desc = self.last_desc
if not self._job_running or last_progress is None:
@ -45,6 +103,16 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
self.view.set_progress(last_progress)
def run(self, jobid, title, target, args=()):
"""Starts a threaded job.
The ``target`` function will be sent, as its first argument, a ``Job`` instance (from the
``jobprogress`` library) which it can use to report on its progress.
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
:param title: A title for the task you're starting.
:param target: The function that does your famous long running job.
:param args: additional arguments that you want to send to ``target``.
"""
# target is a function with its first argument being a Job. It can then be followed by other
# arguments which are passed as `args`.
self.jobid = jobid

View File

@ -9,6 +9,16 @@ from collections import MutableSequence
from .base import GUIObject
class Node(MutableSequence):
"""Pretty bland node implementation to be used in a :class:`Tree`.
It has a :attr:`parent`, behaves like a list, its content being its children. Link integrity
is somewhat enforced (adding a child to a node will set the child's :attr:`parent`, but that's
pretty much as far as we go, integrity-wise. Nodes don't tend to move around much in a GUI
tree). We don't even check for infinite node loops. Don't play around these grounds too much.
Nodes are designed to be subclassed and given meaningful attributes (those you'll want to
display in your tree view), but they all have a :attr:`name`, which is given on initialization.
"""
def __init__(self, name):
self._name = name
self._parent = None
@ -43,15 +53,26 @@ class Node(MutableSequence):
#--- Public
def clear(self):
"""Clears the node of all its children.
"""
del self[:]
def find(self, predicate, include_self=True):
"""Return the first child to match ``predicate``.
See :meth:`findall`.
"""
try:
return next(self.findall(predicate, include_self=include_self))
except StopIteration:
return None
def findall(self, predicate, include_self=True):
"""Yield all children matching ``predicate``.
:param predicate: ``f(node) --> bool``
:param include_self: Whether we can return ``self`` or we return only children.
"""
if include_self and predicate(self):
yield self
for child in self:
@ -59,6 +80,10 @@ class Node(MutableSequence):
yield found
def get_node(self, index_path):
"""Returns the node at ``index_path``.
:param index_path: a list of int indexes leading to our node. See :attr:`path`.
"""
result = self
if index_path:
for index in index_path:
@ -66,24 +91,42 @@ class Node(MutableSequence):
return result
def get_path(self, target_node):
"""Returns the :attr:`path` of ``target_node``.
If ``target_node`` is ``None``, returns ``None``.
"""
if target_node is None:
return None
return target_node.path
@property
def children_count(self):
"""Same as ``len(self)``.
"""
return len(self)
@property
def name(self):
"""Name for the node, supplied on init.
"""
return self._name
@property
def parent(self):
"""Parent of the node.
If ``None``, we have a root node.
"""
return self._parent
@property
def path(self):
"""A list of node indexes leading from the root node to ``self``.
The path of a node is always related to its :attr:`root`. It's the sequences of index that
we have to take to get to our node, starting from the root. For example, if
``node.path == [1, 2, 3, 4]``, it means that ``node.root[1][2][3][4] is node``.
"""
if self._path is None:
if self._parent is None:
self._path = []
@ -93,6 +136,10 @@ class Node(MutableSequence):
@property
def root(self):
"""Root node of current node.
To get it, we recursively follow our :attr:`parent` chain until we have ``None``.
"""
if self._parent is None:
return self
else:
@ -100,28 +147,47 @@ class Node(MutableSequence):
class Tree(Node, GUIObject):
"""Cross-toolkit GUI-enabled tree view.
This class is a bit too thin to be used as a tree view controller out of the box and HS apps
that subclasses it each add quite a bit of logic to it to make it workable. Making this more
usable out of the box is a work in progress.
This class is here (in addition to being a :class:`Node`) mostly to handle selection.
Subclasses :class:`Node` (it is the root node of all its children) and :class:`.GUIObject`.
"""
def __init__(self):
Node.__init__(self, '')
GUIObject.__init__(self)
#: Where we store selected nodes (as a list of :class:`Node`)
self._selected_nodes = []
#--- Virtual
def _select_nodes(self, nodes):
# all selection changes go through this method, so you can override this if you want to
# customize the tree's behavior.
"""(Virtual) Customize node selection behavior.
By default, simply set :attr:`_selected_nodes`.
"""
self._selected_nodes = nodes
#--- Override
def _view_updated(self):
self.view.refresh()
#--- Public
def clear(self):
self._selected_nodes = []
Node.clear(self)
#--- Public
@property
def selected_node(self):
"""Currently selected node.
*:class:`Node`*. *get/set*.
First of :attr:`selected_nodes`. ``None`` if empty.
"""
return self._selected_nodes[0] if self._selected_nodes else None
@selected_node.setter
@ -133,6 +199,13 @@ class Tree(Node, GUIObject):
@property
def selected_nodes(self):
"""List of selected nodes in the tree.
*List of :class:`Node`*. *get/set*.
We use nodes instead of indexes to store selection because it's simpler when it's time to
manage selection of multiple node levels.
"""
return self._selected_nodes
@selected_nodes.setter
@ -141,6 +214,12 @@ class Tree(Node, GUIObject):
@property
def selected_path(self):
"""Currently selected path.
*:attr:`Node.path`*. *get/set*.
First of :attr:`selected_paths`. ``None`` if empty.
"""
return self.get_path(self.selected_node)
@selected_path.setter
@ -152,6 +231,12 @@ class Tree(Node, GUIObject):
@property
def selected_paths(self):
"""List of selected paths in the tree.
*List of :attr:`Node.path`*. *get/set*
Computed from :attr:`selected_nodes`.
"""
return list(map(self.get_path, self._selected_nodes))
@selected_paths.setter