mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-22 14:41:39 +00:00
Added hscommon repo as a subtree
This commit is contained in:
0
hscommon/gui/__init__.py
Normal file
0
hscommon/gui/__init__.py
Normal file
58
hscommon/gui/base.py
Normal file
58
hscommon/gui/base.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2011/09/09
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
pass
|
||||
|
||||
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:
|
||||
def __init__(self):
|
||||
self._view = None
|
||||
|
||||
def _view_updated(self):
|
||||
pass #virtual
|
||||
|
||||
def has_view(self):
|
||||
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
|
||||
|
||||
@property
|
||||
def view(self):
|
||||
return self._view
|
||||
|
||||
@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:
|
||||
return
|
||||
self._view = value
|
||||
self._view_updated()
|
||||
else:
|
||||
assert value is None
|
||||
# Instead of None, we put a NoopGUI() there to avoid rogue view callback raising an
|
||||
# exception.
|
||||
self._view = NoopGUI()
|
||||
|
||||
160
hscommon/gui/column.py
Normal file
160
hscommon/gui/column.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2010-07-25
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import copy
|
||||
|
||||
from .base import GUIObject
|
||||
|
||||
class Column:
|
||||
def __init__(self, name, display='', visible=True, optional=False):
|
||||
self.name = name
|
||||
self.logical_index = 0
|
||||
self.ordered_index = 0
|
||||
self.width = 0
|
||||
self.default_width = 0
|
||||
self.display = display
|
||||
self.visible = visible
|
||||
self.default_visible = visible
|
||||
self.optional = optional
|
||||
|
||||
|
||||
class Columns(GUIObject):
|
||||
def __init__(self, table, prefaccess=None, savename=None):
|
||||
GUIObject.__init__(self)
|
||||
self.table = table
|
||||
self.prefaccess = prefaccess
|
||||
self.savename = savename
|
||||
# We use copy here for test isolation. If we don't, changing a column affects all tests.
|
||||
self.column_list = list(map(copy.copy, table.COLUMNS))
|
||||
for i, column in enumerate(self.column_list):
|
||||
column.logical_index = i
|
||||
column.ordered_index = i
|
||||
self.coldata = {col.name: col for col in self.column_list}
|
||||
|
||||
#--- Private
|
||||
def _get_colname_attr(self, colname, attrname, default):
|
||||
try:
|
||||
return getattr(self.coldata[colname], attrname)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def _set_colname_attr(self, colname, attrname, value):
|
||||
try:
|
||||
col = self.coldata[colname]
|
||||
setattr(col, attrname, value)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _optional_columns(self):
|
||||
return [c for c in self.column_list if c.optional]
|
||||
|
||||
#--- Override
|
||||
def _view_updated(self):
|
||||
self.restore_columns()
|
||||
|
||||
#--- Public
|
||||
def column_by_index(self, index):
|
||||
return self.column_list[index]
|
||||
|
||||
def column_by_name(self, name):
|
||||
return self.coldata[name]
|
||||
|
||||
def columns_count(self):
|
||||
return len(self.column_list)
|
||||
|
||||
def column_display(self, colname):
|
||||
return self._get_colname_attr(colname, 'display', '')
|
||||
|
||||
def column_is_visible(self, colname):
|
||||
return self._get_colname_attr(colname, 'visible', True)
|
||||
|
||||
def column_width(self, colname):
|
||||
return self._get_colname_attr(colname, 'width', 0)
|
||||
|
||||
def columns_to_right(self, colname):
|
||||
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).
|
||||
return [(c.display, c.visible) for c in self._optional_columns()]
|
||||
|
||||
def move_column(self, colname, index):
|
||||
colnames = self.colnames
|
||||
colnames.remove(colname)
|
||||
colnames.insert(index, colname)
|
||||
self.set_column_order(colnames)
|
||||
|
||||
def reset_to_defaults(self):
|
||||
self.set_column_order([col.name for col in self.column_list])
|
||||
for col in self._optional_columns():
|
||||
col.visible = col.default_visible
|
||||
col.width = col.default_width
|
||||
self.view.restore_columns()
|
||||
|
||||
def resize_column(self, colname, newwidth):
|
||||
self._set_colname_attr(colname, 'width', newwidth)
|
||||
|
||||
def restore_columns(self):
|
||||
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
|
||||
# "restore" its default column attributes.
|
||||
self.view.restore_columns()
|
||||
return
|
||||
for col in self.column_list:
|
||||
pref_name = '{}.Columns.{}'.format(self.savename, col.name)
|
||||
coldata = self.prefaccess.get_default(pref_name, fallback_value={})
|
||||
if 'index' in coldata:
|
||||
col.ordered_index = coldata['index']
|
||||
if 'width' in coldata:
|
||||
col.width = coldata['width']
|
||||
if col.optional and 'visible' in coldata:
|
||||
col.visible = coldata['visible']
|
||||
self.view.restore_columns()
|
||||
|
||||
def save_columns(self):
|
||||
if not (self.prefaccess and self.savename and self.coldata):
|
||||
return
|
||||
for col in self.column_list:
|
||||
pref_name = '{}.Columns.{}'.format(self.savename, col.name)
|
||||
coldata = {'index': col.ordered_index, 'width': col.width}
|
||||
if col.optional:
|
||||
coldata['visible'] = col.visible
|
||||
self.prefaccess.set_default(pref_name, coldata)
|
||||
|
||||
def set_column_order(self, colnames):
|
||||
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):
|
||||
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):
|
||||
self._set_colname_attr(colname, 'default_width', width)
|
||||
|
||||
def toggle_menu_item(self, index):
|
||||
col = self._optional_columns()[index]
|
||||
self.set_column_visible(col.name, not col.visible)
|
||||
return col.visible
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def ordered_columns(self):
|
||||
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
|
||||
|
||||
@property
|
||||
def colnames(self):
|
||||
return [col.name for col in self.ordered_columns]
|
||||
|
||||
131
hscommon/gui/selectable_list.py
Normal file
131
hscommon/gui/selectable_list.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2011-09-06
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from collections import Sequence, MutableSequence
|
||||
|
||||
from .base import GUIObject
|
||||
|
||||
class Selectable(Sequence):
|
||||
def __init__(self):
|
||||
self._selected_indexes = []
|
||||
|
||||
#--- Private
|
||||
def _check_selection_range(self):
|
||||
if not self:
|
||||
self._selected_indexes = []
|
||||
if not self._selected_indexes:
|
||||
return
|
||||
self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]
|
||||
if not self._selected_indexes:
|
||||
self._selected_indexes = [len(self) - 1]
|
||||
|
||||
#--- 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
|
||||
|
||||
#--- Public
|
||||
def select(self, indexes):
|
||||
if isinstance(indexes, int):
|
||||
indexes = [indexes]
|
||||
self.selected_indexes = indexes
|
||||
self._update_selection()
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def selected_index(self):
|
||||
return self._selected_indexes[0] if self._selected_indexes else None
|
||||
|
||||
@selected_index.setter
|
||||
def selected_index(self, value):
|
||||
self.selected_indexes = [value]
|
||||
|
||||
@property
|
||||
def selected_indexes(self):
|
||||
return self._selected_indexes
|
||||
|
||||
@selected_indexes.setter
|
||||
def selected_indexes(self, value):
|
||||
self._selected_indexes = value
|
||||
self._selected_indexes.sort()
|
||||
self._check_selection_range()
|
||||
|
||||
|
||||
class SelectableList(MutableSequence, Selectable):
|
||||
def __init__(self, items=None):
|
||||
Selectable.__init__(self)
|
||||
if items:
|
||||
self._items = list(items)
|
||||
else:
|
||||
self._items = []
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._items.__delitem__(key)
|
||||
self._check_selection_range()
|
||||
self._on_change()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._items.__getitem__(key)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._items.__setitem__(key, value)
|
||||
self._on_change()
|
||||
|
||||
#--- Override
|
||||
def append(self, item):
|
||||
self._items.append(item)
|
||||
self._on_change()
|
||||
|
||||
def insert(self, index, item):
|
||||
self._items.insert(index, item)
|
||||
self._on_change()
|
||||
|
||||
def remove(self, row):
|
||||
self._items.remove(row)
|
||||
self._check_selection_range()
|
||||
self._on_change()
|
||||
|
||||
#--- Virtual
|
||||
def _on_change(self):
|
||||
pass
|
||||
|
||||
#--- Public
|
||||
def search_by_prefix(self, prefix):
|
||||
prefix = prefix.lower()
|
||||
for index, s in enumerate(self):
|
||||
if s.lower().startswith(prefix):
|
||||
return index
|
||||
return -1
|
||||
|
||||
|
||||
class GUISelectableList(SelectableList, GUIObject):
|
||||
#--- View interface
|
||||
# refresh()
|
||||
# update_selection()
|
||||
#
|
||||
|
||||
def __init__(self, items=None):
|
||||
SelectableList.__init__(self, items)
|
||||
GUIObject.__init__(self)
|
||||
|
||||
def _view_updated(self):
|
||||
self.view.refresh()
|
||||
|
||||
def _update_selection(self):
|
||||
self.view.update_selection()
|
||||
|
||||
def _on_change(self):
|
||||
self.view.refresh()
|
||||
297
hscommon/gui/table.py
Normal file
297
hscommon/gui/table.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# Created By: Eric Mc Sween
|
||||
# Created On: 2008-05-29
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from collections import MutableSequence, namedtuple
|
||||
|
||||
from .base import GUIObject
|
||||
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
|
||||
# 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):
|
||||
def __init__(self):
|
||||
Selectable.__init__(self)
|
||||
self._rows = []
|
||||
self._header = None
|
||||
self._footer = None
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._rows.__delitem__(key)
|
||||
if self._header is not None and ((not self) or (self[0] is not self._header)):
|
||||
self._header = None
|
||||
if self._footer is not None and ((not self) or (self[-1] is not self._footer)):
|
||||
self._footer = None
|
||||
self._check_selection_range()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._rows.__getitem__(key)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._rows)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._rows.__setitem__(key, value)
|
||||
|
||||
def append(self, item):
|
||||
if self._footer is not None:
|
||||
self._rows.insert(-1, item)
|
||||
else:
|
||||
self._rows.append(item)
|
||||
|
||||
def insert(self, index, item):
|
||||
if (self._header is not None) and (index == 0):
|
||||
index = 1
|
||||
if (self._footer is not None) and (index >= len(self)):
|
||||
index = len(self) - 1
|
||||
self._rows.insert(index, item)
|
||||
|
||||
def remove(self, row):
|
||||
if row is self._header:
|
||||
self._header = None
|
||||
if row is self._footer:
|
||||
self._footer = None
|
||||
self._rows.remove(row)
|
||||
self._check_selection_range()
|
||||
|
||||
def sort_by(self, column_name, desc=False):
|
||||
if self._header is not None:
|
||||
self._rows.pop(0)
|
||||
if self._footer is not None:
|
||||
self._rows.pop()
|
||||
key = lambda row: row.sort_key_for_column(column_name)
|
||||
self._rows.sort(key=key, reverse=desc)
|
||||
if self._header is not None:
|
||||
self._rows.insert(0, self._header)
|
||||
if self._footer is not None:
|
||||
self._rows.append(self._footer)
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def footer(self):
|
||||
return self._footer
|
||||
|
||||
@footer.setter
|
||||
def footer(self, value):
|
||||
if self._footer is not None:
|
||||
self._rows.pop()
|
||||
if value is not None:
|
||||
self._rows.append(value)
|
||||
self._footer = value
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return self._header
|
||||
|
||||
@header.setter
|
||||
def header(self, value):
|
||||
if self._header is not None:
|
||||
self._rows.pop(0)
|
||||
if value is not None:
|
||||
self._rows.insert(0, value)
|
||||
self._header = value
|
||||
|
||||
@property
|
||||
def row_count(self):
|
||||
result = len(self)
|
||||
if self._footer is not None:
|
||||
result -= 1
|
||||
if self._header is not None:
|
||||
result -= 1
|
||||
return result
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
start = None
|
||||
end = None
|
||||
if self._footer is not None:
|
||||
end = -1
|
||||
if self._header is not None:
|
||||
start = 1
|
||||
return self[start:end]
|
||||
|
||||
@property
|
||||
def selected_row(self):
|
||||
return self[self.selected_index] if self.selected_index is not None else None
|
||||
|
||||
@selected_row.setter
|
||||
def selected_row(self, value):
|
||||
try:
|
||||
self.selected_index = self.index(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def selected_rows(self):
|
||||
return [self[index] for index in self.selected_indexes]
|
||||
|
||||
|
||||
SortDescriptor = namedtuple('SortDescriptor', 'column desc')
|
||||
class GUITable(Table, GUIObject):
|
||||
def __init__(self):
|
||||
GUIObject.__init__(self)
|
||||
Table.__init__(self)
|
||||
self.edited = None
|
||||
self._sort_descriptor = None
|
||||
|
||||
#--- Virtual
|
||||
def _do_add(self):
|
||||
# Creates a new row, adds it in the table and returns (row, insert_index)
|
||||
raise NotImplementedError()
|
||||
|
||||
def _do_delete(self):
|
||||
# Delete the selected rows
|
||||
pass
|
||||
|
||||
def _fill(self):
|
||||
# Called by refresh()
|
||||
# Fills the table with all the rows that this table is supposed to have.
|
||||
pass
|
||||
|
||||
def _is_edited_new(self):
|
||||
return False
|
||||
|
||||
def _restore_selection(self, previous_selection):
|
||||
if not self.selected_indexes:
|
||||
if previous_selection:
|
||||
self.select(previous_selection)
|
||||
else:
|
||||
self.select([len(self) - 1])
|
||||
|
||||
#--- Public
|
||||
def add(self):
|
||||
self.view.stop_editing()
|
||||
if self.edited is not None:
|
||||
self.save_edits()
|
||||
row, insert_index = self._do_add()
|
||||
self.insert(insert_index, row)
|
||||
self.select([insert_index])
|
||||
self.edited = row
|
||||
self.view.refresh()
|
||||
self.view.start_editing()
|
||||
|
||||
def can_edit_cell(self, column_name, row_index):
|
||||
# A row is, by default, editable as soon as it has an attr with the same name as `column`.
|
||||
# If can_edit() returns False, the row is not editable at all. You can set editability of
|
||||
# rows at the attribute level with can_edit_* properties
|
||||
row = self[row_index]
|
||||
return row.can_edit_cell(column_name)
|
||||
|
||||
def cancel_edits(self):
|
||||
if self.edited is None:
|
||||
return
|
||||
self.view.stop_editing()
|
||||
if self._is_edited_new():
|
||||
previous_selection = self.selected_indexes
|
||||
self.remove(self.edited)
|
||||
self._restore_selection(previous_selection)
|
||||
self._update_selection()
|
||||
else:
|
||||
self.edited.load()
|
||||
self.edited = None
|
||||
self.view.refresh()
|
||||
|
||||
def delete(self):
|
||||
self.view.stop_editing()
|
||||
if self.edited is not None:
|
||||
self.cancel_edits()
|
||||
return
|
||||
if self:
|
||||
self._do_delete()
|
||||
|
||||
def refresh(self, refresh_view=True):
|
||||
self.cancel_edits()
|
||||
previous_selection = self.selected_indexes
|
||||
del self[:]
|
||||
self._fill()
|
||||
sd = self._sort_descriptor
|
||||
if sd is not None:
|
||||
Table.sort_by(self, column_name=sd.column, desc=sd.desc)
|
||||
self._restore_selection(previous_selection)
|
||||
if refresh_view:
|
||||
self.view.refresh()
|
||||
|
||||
def save_edits(self):
|
||||
if self.edited is None:
|
||||
return
|
||||
row = self.edited
|
||||
self.edited = None
|
||||
row.save()
|
||||
|
||||
def sort_by(self, column_name, desc=False):
|
||||
Table.sort_by(self, column_name=column_name, desc=desc)
|
||||
self._sort_descriptor = SortDescriptor(column_name, desc)
|
||||
self._update_selection()
|
||||
self.view.refresh()
|
||||
|
||||
|
||||
class Row:
|
||||
def __init__(self, table):
|
||||
super(Row, self).__init__()
|
||||
self.table = table
|
||||
|
||||
def _edit(self):
|
||||
if self.table.edited is self:
|
||||
return
|
||||
assert self.table.edited is None
|
||||
self.table.edited = self
|
||||
|
||||
#--- Virtual
|
||||
def can_edit(self):
|
||||
return True
|
||||
|
||||
def load(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def save(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
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
|
||||
# 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
|
||||
# Of course, override for exceptions.
|
||||
try:
|
||||
return getattr(self, '_' + column_name)
|
||||
except AttributeError:
|
||||
return getattr(self, column_name)
|
||||
|
||||
#--- Public
|
||||
def can_edit_cell(self, column_name):
|
||||
if not self.can_edit():
|
||||
return False
|
||||
# '_' is in case column is a python keyword
|
||||
if not hasattr(self, column_name):
|
||||
if hasattr(self, column_name + '_'):
|
||||
column_name = column_name + '_'
|
||||
else:
|
||||
return False
|
||||
if hasattr(self, 'can_edit_' + column_name):
|
||||
return getattr(self, 'can_edit_' + column_name)
|
||||
# If the row has a settable property, we can edit the cell
|
||||
rowclass = self.__class__
|
||||
prop = getattr(rowclass, column_name, None)
|
||||
if prop is None:
|
||||
return False
|
||||
return bool(getattr(prop, 'fset', None))
|
||||
|
||||
def get_cell_value(self, attrname):
|
||||
if attrname == 'from':
|
||||
attrname = 'from_'
|
||||
return getattr(self, attrname)
|
||||
|
||||
def set_cell_value(self, attrname, value):
|
||||
if attrname == 'from':
|
||||
attrname = 'from_'
|
||||
setattr(self, attrname, value)
|
||||
|
||||
55
hscommon/gui/text_field.py
Normal file
55
hscommon/gui/text_field.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Created On: 2012/01/23
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from .base import GUIObject
|
||||
from ..util import nonone
|
||||
|
||||
class TextField(GUIObject):
|
||||
def __init__(self):
|
||||
GUIObject.__init__(self)
|
||||
self._text = ''
|
||||
self._value = None
|
||||
|
||||
#--- Virtual
|
||||
def _parse(self, text):
|
||||
return text
|
||||
|
||||
def _format(self, value):
|
||||
return value
|
||||
|
||||
def _update(self, newvalue):
|
||||
pass
|
||||
|
||||
#--- Override
|
||||
def _view_updated(self):
|
||||
self.view.refresh()
|
||||
|
||||
#--- Public
|
||||
def refresh(self):
|
||||
self.view.refresh()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
@text.setter
|
||||
def text(self, newtext):
|
||||
self.value = self._parse(nonone(newtext, ''))
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, newvalue):
|
||||
if newvalue == self._value:
|
||||
return
|
||||
self._value = newvalue
|
||||
self._text = self._format(newvalue)
|
||||
self._update(self._value)
|
||||
self.refresh()
|
||||
|
||||
166
hscommon/gui/tree.py
Normal file
166
hscommon/gui/tree.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from collections import MutableSequence
|
||||
|
||||
from .base import GUIObject
|
||||
|
||||
class Node(MutableSequence):
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._parent = None
|
||||
self._path = None
|
||||
self._children = []
|
||||
|
||||
def __repr__(self):
|
||||
return '<Node %r>' % self.name
|
||||
|
||||
#--- MutableSequence overrides
|
||||
def __delitem__(self, key):
|
||||
self._children.__delitem__(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._children.__getitem__(key)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._children)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._children.__setitem__(key, value)
|
||||
|
||||
def append(self, node):
|
||||
self._children.append(node)
|
||||
node._parent = self
|
||||
node._path = None
|
||||
|
||||
def insert(self, index, node):
|
||||
self._children.insert(index, node)
|
||||
node._parent = self
|
||||
node._path = None
|
||||
|
||||
#--- Public
|
||||
def clear(self):
|
||||
del self[:]
|
||||
|
||||
def find(self, predicate, include_self=True):
|
||||
try:
|
||||
return next(self.findall(predicate, include_self=include_self))
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
def findall(self, predicate, include_self=True):
|
||||
if include_self and predicate(self):
|
||||
yield self
|
||||
for child in self:
|
||||
for found in child.findall(predicate, include_self=True):
|
||||
yield found
|
||||
|
||||
def get_node(self, index_path):
|
||||
result = self
|
||||
if index_path:
|
||||
for index in index_path:
|
||||
result = result[index]
|
||||
return result
|
||||
|
||||
def get_path(self, target_node):
|
||||
if target_node is None:
|
||||
return None
|
||||
return target_node.path
|
||||
|
||||
@property
|
||||
def children_count(self):
|
||||
return len(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if self._path is None:
|
||||
if self._parent is None:
|
||||
self._path = []
|
||||
else:
|
||||
self._path = self._parent.path + [self._parent.index(self)]
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def root(self):
|
||||
if self._parent is None:
|
||||
return self
|
||||
else:
|
||||
return self._parent.root
|
||||
|
||||
|
||||
class Tree(Node, GUIObject):
|
||||
def __init__(self):
|
||||
Node.__init__(self, '')
|
||||
GUIObject.__init__(self)
|
||||
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.
|
||||
self._selected_nodes = nodes
|
||||
|
||||
#--- Override
|
||||
def _view_updated(self):
|
||||
self.view.refresh()
|
||||
|
||||
#--- Public
|
||||
def clear(self):
|
||||
self._selected_nodes = []
|
||||
Node.clear(self)
|
||||
|
||||
@property
|
||||
def selected_node(self):
|
||||
return self._selected_nodes[0] if self._selected_nodes else None
|
||||
|
||||
@selected_node.setter
|
||||
def selected_node(self, node):
|
||||
if node is not None:
|
||||
self._select_nodes([node])
|
||||
else:
|
||||
self._select_nodes([])
|
||||
|
||||
@property
|
||||
def selected_nodes(self):
|
||||
return self._selected_nodes
|
||||
|
||||
@selected_nodes.setter
|
||||
def selected_nodes(self, nodes):
|
||||
self._select_nodes(nodes)
|
||||
|
||||
@property
|
||||
def selected_path(self):
|
||||
return self.get_path(self.selected_node)
|
||||
|
||||
@selected_path.setter
|
||||
def selected_path(self, index_path):
|
||||
if index_path is not None:
|
||||
self.selected_paths = [index_path]
|
||||
else:
|
||||
self._select_nodes([])
|
||||
|
||||
@property
|
||||
def selected_paths(self):
|
||||
return list(map(self.get_path, self._selected_nodes))
|
||||
|
||||
@selected_paths.setter
|
||||
def selected_paths(self, index_paths):
|
||||
nodes = []
|
||||
for path in index_paths:
|
||||
try:
|
||||
nodes.append(self.get_node(path))
|
||||
except IndexError:
|
||||
pass
|
||||
self._select_nodes(nodes)
|
||||
|
||||
Reference in New Issue
Block a user