mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-03-10 05:34:36 +00:00
Improved hscommon docs
This commit is contained in:
parent
b6bc5de79c
commit
7116674663
25
help/en/developer/hscommon/gui/column.rst
Normal file
25
help/en/developer/hscommon/gui/column.rst
Normal 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:
|
18
help/en/developer/hscommon/gui/progress_window.rst
Normal file
18
help/en/developer/hscommon/gui/progress_window.rst
Normal 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:
|
||||||
|
|
18
help/en/developer/hscommon/gui/tree.rst
Normal file
18
help/en/developer/hscommon/gui/tree.rst
Normal 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:
|
||||||
|
|
@ -14,3 +14,6 @@ hscommon
|
|||||||
gui/text_field
|
gui/text_field
|
||||||
gui/selectable_list
|
gui/selectable_list
|
||||||
gui/table
|
gui/table
|
||||||
|
gui/tree
|
||||||
|
gui/column
|
||||||
|
gui/progress_window
|
||||||
|
@ -11,19 +11,92 @@ import copy
|
|||||||
from .base import GUIObject
|
from .base import GUIObject
|
||||||
|
|
||||||
class Column:
|
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):
|
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
|
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
|
self.logical_index = 0
|
||||||
|
#: Index of the column in the ordered set of columns.
|
||||||
self.ordered_index = 0
|
self.ordered_index = 0
|
||||||
|
#: Width of the column.
|
||||||
self.width = 0
|
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
|
self.default_width = 0
|
||||||
|
#: Display name (title) of the column.
|
||||||
self.display = display
|
self.display = display
|
||||||
|
#: Whether the column is visible.
|
||||||
self.visible = 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
|
self.default_visible = visible
|
||||||
|
#: Whether the column can have :attr:`visible` set to false.
|
||||||
self.optional = optional
|
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):
|
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):
|
def __init__(self, table, prefaccess=None, savename=None):
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
self.table = table
|
self.table = table
|
||||||
@ -59,40 +132,71 @@ class Columns(GUIObject):
|
|||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def column_by_index(self, index):
|
def column_by_index(self, index):
|
||||||
|
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.
|
||||||
|
"""
|
||||||
return self.column_list[index]
|
return self.column_list[index]
|
||||||
|
|
||||||
def column_by_name(self, name):
|
def column_by_name(self, name):
|
||||||
|
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``.
|
||||||
|
"""
|
||||||
return self.coldata[name]
|
return self.coldata[name]
|
||||||
|
|
||||||
def columns_count(self):
|
def columns_count(self):
|
||||||
|
"""Returns the number of columns in our set.
|
||||||
|
"""
|
||||||
return len(self.column_list)
|
return len(self.column_list)
|
||||||
|
|
||||||
def column_display(self, colname):
|
def column_display(self, colname):
|
||||||
|
"""Returns display name for column named ``colname``, or ``''`` if there's none.
|
||||||
|
"""
|
||||||
return self._get_colname_attr(colname, 'display', '')
|
return self._get_colname_attr(colname, 'display', '')
|
||||||
|
|
||||||
def column_is_visible(self, colname):
|
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)
|
return self._get_colname_attr(colname, 'visible', True)
|
||||||
|
|
||||||
def column_width(self, colname):
|
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)
|
return self._get_colname_attr(colname, 'width', 0)
|
||||||
|
|
||||||
def columns_to_right(self, colname):
|
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]
|
column = self.coldata[colname]
|
||||||
index = column.ordered_index
|
index = column.ordered_index
|
||||||
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
||||||
|
|
||||||
def menu_items(self):
|
def menu_items(self):
|
||||||
# Returns a list of (display_name, marked) items for each optional column in the current
|
"""Returns a list of items convenient for quick visibility menu generation.
|
||||||
# view (marked means that it's visible).
|
|
||||||
|
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()]
|
return [(c.display, c.visible) for c in self._optional_columns()]
|
||||||
|
|
||||||
def move_column(self, colname, index):
|
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 = self.colnames
|
||||||
colnames.remove(colname)
|
colnames.remove(colname)
|
||||||
colnames.insert(index, colname)
|
colnames.insert(index, colname)
|
||||||
self.set_column_order(colnames)
|
self.set_column_order(colnames)
|
||||||
|
|
||||||
def reset_to_defaults(self):
|
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])
|
self.set_column_order([col.name for col in self.column_list])
|
||||||
for col in self._optional_columns():
|
for col in self._optional_columns():
|
||||||
col.visible = col.default_visible
|
col.visible = col.default_visible
|
||||||
@ -100,9 +204,13 @@ class Columns(GUIObject):
|
|||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def resize_column(self, colname, newwidth):
|
def resize_column(self, colname, newwidth):
|
||||||
|
"""Set column ``colname``'s width to ``newwidth``.
|
||||||
|
"""
|
||||||
self._set_colname_attr(colname, 'width', newwidth)
|
self._set_colname_attr(colname, 'width', newwidth)
|
||||||
|
|
||||||
def restore_columns(self):
|
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.prefaccess and self.savename and self.coldata):
|
||||||
if (not 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
|
# 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()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def save_columns(self):
|
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):
|
if not (self.prefaccess and self.savename and self.coldata):
|
||||||
return
|
return
|
||||||
for col in self.column_list:
|
for col in self.column_list:
|
||||||
@ -131,20 +241,35 @@ class Columns(GUIObject):
|
|||||||
self.prefaccess.set_default(pref_name, coldata)
|
self.prefaccess.set_default(pref_name, coldata)
|
||||||
|
|
||||||
def set_column_order(self, colnames):
|
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)
|
colnames = (name for name in colnames if name in self.coldata)
|
||||||
for i, colname in enumerate(colnames):
|
for i, colname in enumerate(colnames):
|
||||||
col = self.coldata[colname]
|
col = self.coldata[colname]
|
||||||
col.ordered_index = i
|
col.ordered_index = i
|
||||||
|
|
||||||
def set_column_visible(self, colname, visible):
|
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.table.save_edits() # the table on the GUI side will stop editing when the columns change
|
||||||
self._set_colname_attr(colname, 'visible', visible)
|
self._set_colname_attr(colname, 'visible', visible)
|
||||||
self.view.set_column_visible(colname, visible)
|
self.view.set_column_visible(colname, visible)
|
||||||
|
|
||||||
def set_default_width(self, colname, width):
|
def set_default_width(self, colname, width):
|
||||||
|
"""Set the default width or column ``colname``.
|
||||||
|
"""
|
||||||
self._set_colname_attr(colname, 'default_width', width)
|
self._set_colname_attr(colname, 'default_width', width)
|
||||||
|
|
||||||
def toggle_menu_item(self, index):
|
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]
|
col = self._optional_columns()[index]
|
||||||
self.set_column_visible(col.name, not col.visible)
|
self.set_column_visible(col.name, not col.visible)
|
||||||
return col.visible
|
return col.visible
|
||||||
@ -152,9 +277,13 @@ class Columns(GUIObject):
|
|||||||
#--- Properties
|
#--- Properties
|
||||||
@property
|
@property
|
||||||
def ordered_columns(self):
|
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)]
|
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colnames(self):
|
def colnames(self):
|
||||||
|
"""List of column names in visible order.
|
||||||
|
"""
|
||||||
return [col.name for col in self.ordered_columns]
|
return [col.name for col in self.ordered_columns]
|
||||||
|
|
||||||
|
@ -10,17 +10,68 @@ from jobprogress.performer import ThreadedJobPerformer
|
|||||||
from .base import GUIObject
|
from .base import GUIObject
|
||||||
from .text_field import TextField
|
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):
|
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):
|
def __init__(self, finish_func):
|
||||||
# finish_func(jobid) is the function that is called when a job is completed.
|
# finish_func(jobid) is the function that is called when a job is completed.
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
ThreadedJobPerformer.__init__(self)
|
ThreadedJobPerformer.__init__(self)
|
||||||
self._finish_func = finish_func
|
self._finish_func = finish_func
|
||||||
|
#: :class:`.TextField`. It contains that title you gave the job on :meth:`run`.
|
||||||
self.jobdesc_textfield = TextField()
|
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.progressdesc_textfield = TextField()
|
||||||
self.jobid = None
|
self.jobid = None
|
||||||
|
|
||||||
def cancel(self):
|
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
|
# 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
|
# 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.
|
# we verify that the job is still running.
|
||||||
@ -28,8 +79,15 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
self.job_cancelled = True
|
self.job_cancelled = True
|
||||||
|
|
||||||
def pulse(self):
|
def pulse(self):
|
||||||
# Call this regularly from the GUI main run loop.
|
"""Update progress reports in the GUI.
|
||||||
# the values might change before setValue happens
|
|
||||||
|
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_progress = self.last_progress
|
||||||
last_desc = self.last_desc
|
last_desc = self.last_desc
|
||||||
if not self._job_running or last_progress is None:
|
if not self._job_running or last_progress is None:
|
||||||
@ -45,6 +103,16 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
self.view.set_progress(last_progress)
|
self.view.set_progress(last_progress)
|
||||||
|
|
||||||
def run(self, jobid, title, target, args=()):
|
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
|
# target is a function with its first argument being a Job. It can then be followed by other
|
||||||
# arguments which are passed as `args`.
|
# arguments which are passed as `args`.
|
||||||
self.jobid = jobid
|
self.jobid = jobid
|
||||||
|
@ -9,6 +9,16 @@ from collections import MutableSequence
|
|||||||
from .base import GUIObject
|
from .base import GUIObject
|
||||||
|
|
||||||
class Node(MutableSequence):
|
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):
|
def __init__(self, name):
|
||||||
self._name = name
|
self._name = name
|
||||||
self._parent = None
|
self._parent = None
|
||||||
@ -43,15 +53,26 @@ class Node(MutableSequence):
|
|||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Clears the node of all its children.
|
||||||
|
"""
|
||||||
del self[:]
|
del self[:]
|
||||||
|
|
||||||
def find(self, predicate, include_self=True):
|
def find(self, predicate, include_self=True):
|
||||||
|
"""Return the first child to match ``predicate``.
|
||||||
|
|
||||||
|
See :meth:`findall`.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return next(self.findall(predicate, include_self=include_self))
|
return next(self.findall(predicate, include_self=include_self))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def findall(self, predicate, include_self=True):
|
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):
|
if include_self and predicate(self):
|
||||||
yield self
|
yield self
|
||||||
for child in self:
|
for child in self:
|
||||||
@ -59,6 +80,10 @@ class Node(MutableSequence):
|
|||||||
yield found
|
yield found
|
||||||
|
|
||||||
def get_node(self, index_path):
|
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
|
result = self
|
||||||
if index_path:
|
if index_path:
|
||||||
for index in index_path:
|
for index in index_path:
|
||||||
@ -66,24 +91,42 @@ class Node(MutableSequence):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get_path(self, target_node):
|
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:
|
if target_node is None:
|
||||||
return None
|
return None
|
||||||
return target_node.path
|
return target_node.path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children_count(self):
|
def children_count(self):
|
||||||
|
"""Same as ``len(self)``.
|
||||||
|
"""
|
||||||
return len(self)
|
return len(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
"""Name for the node, supplied on init.
|
||||||
|
"""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
|
"""Parent of the node.
|
||||||
|
|
||||||
|
If ``None``, we have a root node.
|
||||||
|
"""
|
||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
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._path is None:
|
||||||
if self._parent is None:
|
if self._parent is None:
|
||||||
self._path = []
|
self._path = []
|
||||||
@ -93,6 +136,10 @@ class Node(MutableSequence):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def root(self):
|
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:
|
if self._parent is None:
|
||||||
return self
|
return self
|
||||||
else:
|
else:
|
||||||
@ -100,28 +147,47 @@ class Node(MutableSequence):
|
|||||||
|
|
||||||
|
|
||||||
class Tree(Node, GUIObject):
|
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):
|
def __init__(self):
|
||||||
Node.__init__(self, '')
|
Node.__init__(self, '')
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
|
#: Where we store selected nodes (as a list of :class:`Node`)
|
||||||
self._selected_nodes = []
|
self._selected_nodes = []
|
||||||
|
|
||||||
#--- Virtual
|
#--- Virtual
|
||||||
def _select_nodes(self, nodes):
|
def _select_nodes(self, nodes):
|
||||||
# all selection changes go through this method, so you can override this if you want to
|
"""(Virtual) Customize node selection behavior.
|
||||||
# customize the tree's behavior.
|
|
||||||
|
By default, simply set :attr:`_selected_nodes`.
|
||||||
|
"""
|
||||||
self._selected_nodes = nodes
|
self._selected_nodes = nodes
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def _view_updated(self):
|
def _view_updated(self):
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
#--- Public
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self._selected_nodes = []
|
self._selected_nodes = []
|
||||||
Node.clear(self)
|
Node.clear(self)
|
||||||
|
|
||||||
|
#--- Public
|
||||||
@property
|
@property
|
||||||
def selected_node(self):
|
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
|
return self._selected_nodes[0] if self._selected_nodes else None
|
||||||
|
|
||||||
@selected_node.setter
|
@selected_node.setter
|
||||||
@ -133,6 +199,13 @@ class Tree(Node, GUIObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_nodes(self):
|
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
|
return self._selected_nodes
|
||||||
|
|
||||||
@selected_nodes.setter
|
@selected_nodes.setter
|
||||||
@ -141,6 +214,12 @@ class Tree(Node, GUIObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_path(self):
|
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)
|
return self.get_path(self.selected_node)
|
||||||
|
|
||||||
@selected_path.setter
|
@selected_path.setter
|
||||||
@ -152,6 +231,12 @@ class Tree(Node, GUIObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_paths(self):
|
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))
|
return list(map(self.get_path, self._selected_nodes))
|
||||||
|
|
||||||
@selected_paths.setter
|
@selected_paths.setter
|
||||||
|
Loading…
x
Reference in New Issue
Block a user