mirror of
https://github.com/arsenetar/dupeguru.git
synced 2024-11-18 21:19:01 +00:00
298 lines
9.2 KiB
Python
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)
|
||
|
|