dupeguru/hscommon/gui/table.py

298 lines
9.2 KiB
Python

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