1
0
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:
Virgil Dupras
2013-06-22 21:32:23 -04:00
parent 95623f9b47
commit 94a469205a
62 changed files with 6553 additions and 0 deletions

0
hscommon/gui/__init__.py Normal file
View File

58
hscommon/gui/base.py Normal file
View 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
View 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]

View 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
View 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)

View 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
View 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)