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:
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)
|
||||
|
||||
Reference in New Issue
Block a user