dupeguru/hscommon/gui/selectable_list.py

215 lines
6.6 KiB
Python
Raw Normal View History

2019-09-09 19:54:28 -05:00
# Created By: Virgil Dupras
# Created On: 2011-09-06
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
2019-09-09 19:54:28 -05:00
# http://www.gnu.org/licenses/gpl-3.0.html
from collections.abc import Sequence, MutableSequence
2019-09-09 19:54:28 -05:00
2022-05-09 01:40:08 -05:00
from hscommon.gui.base import GUIObject
2019-09-09 19:54:28 -05:00
2019-09-09 19:54:28 -05:00
class Selectable(Sequence):
"""Mix-in for a ``Sequence`` that manages its selection status.
2019-09-09 19:54:28 -05:00
When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection
is held as a list of ``int`` indexes. Multiple selection is supported.
"""
2019-09-09 19:54:28 -05:00
def __init__(self):
self._selected_indexes = []
# --- Private
2019-09-09 19:54:28 -05:00
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)]
2019-09-09 19:54:28 -05:00
if not self._selected_indexes:
self._selected_indexes = [len(self) - 1]
# --- Virtual
2019-09-09 19:54:28 -05:00
def _update_selection(self):
"""(Virtual) Updates the model's selection appropriately.
2019-09-09 19:54:28 -05:00
Called after selection has been updated. Takes the table's selection and does appropriates
updates on the view and/or model. 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.
2019-09-09 19:54:28 -05:00
By default, does nothing.
2019-09-09 19:54:28 -05:00
Important note: This is only called on :meth:`select`, not on changes to
:attr:`selected_indexes`.
"""
# A redesign of how this whole thing works is probably in order, but not now, there's too
# much breakage at once involved.
# --- Public
2019-09-09 19:54:28 -05:00
def select(self, indexes):
"""Update selection to ``indexes``.
2019-09-09 19:54:28 -05:00
:meth:`_update_selection` is called afterwards.
2019-09-09 19:54:28 -05:00
:param list indexes: List of ``int`` that is to become the new selection.
"""
if isinstance(indexes, int):
indexes = [indexes]
self.selected_indexes = indexes
self._update_selection()
# --- Properties
2019-09-09 19:54:28 -05:00
@property
def selected_index(self):
"""Points to the first selected index.
*int*. *get/set*.
2019-09-09 19:54:28 -05:00
Thin wrapper around :attr:`selected_indexes`. ``None`` if selection is empty. Using this
property only makes sense if your selectable sequence supports single selection only.
"""
return self._selected_indexes[0] if self._selected_indexes else None
2019-09-09 19:54:28 -05:00
@selected_index.setter
def selected_index(self, value):
self.selected_indexes = [value]
2019-09-09 19:54:28 -05:00
@property
def selected_indexes(self):
"""List of selected indexes.
2019-09-09 19:54:28 -05:00
*list of int*. *get/set*.
2019-09-09 19:54:28 -05:00
When setting the value, automatically removes out-of-bounds indexes. The list is kept
sorted.
"""
return self._selected_indexes
2019-09-09 19:54:28 -05:00
@selected_indexes.setter
def selected_indexes(self, value):
self._selected_indexes = value
self._selected_indexes.sort()
self._check_selection_range()
class SelectableList(MutableSequence, Selectable):
"""A list that can manage selection of its items.
2019-09-09 19:54:28 -05:00
Subclasses :class:`Selectable`. Behaves like a ``list``.
"""
2019-09-09 19:54:28 -05:00
def __init__(self, items=None):
Selectable.__init__(self)
if items:
self._items = list(items)
else:
self._items = []
2019-09-09 19:54:28 -05:00
def __delitem__(self, key):
self._items.__delitem__(key)
self._check_selection_range()
self._on_change()
2019-09-09 19:54:28 -05:00
def __getitem__(self, key):
return self._items.__getitem__(key)
2019-09-09 19:54:28 -05:00
def __len__(self):
return len(self._items)
2019-09-09 19:54:28 -05:00
def __setitem__(self, key, value):
self._items.__setitem__(key, value)
self._on_change()
# --- Override
2019-09-09 19:54:28 -05:00
def append(self, item):
self._items.append(item)
self._on_change()
2019-09-09 19:54:28 -05:00
def insert(self, index, item):
self._items.insert(index, item)
self._on_change()
2019-09-09 19:54:28 -05:00
def remove(self, row):
self._items.remove(row)
self._check_selection_range()
self._on_change()
# --- Virtual
2019-09-09 19:54:28 -05:00
def _on_change(self):
"""(Virtual) Called whenever the contents of the list changes.
2019-09-09 19:54:28 -05:00
By default, does nothing.
"""
# --- Public
2019-09-09 19:54:28 -05:00
def search_by_prefix(self, prefix):
# XXX Why the heck is this method here?
prefix = prefix.lower()
for index, s in enumerate(self):
if s.lower().startswith(prefix):
return index
return -1
2019-09-09 19:54:28 -05:00
class GUISelectableListView:
"""Expected interface for :class:`GUISelectableList`'s view.
2019-09-09 19:54:28 -05:00
*Not actually used in the code. For documentation purposes only.*
2019-09-09 19:54:28 -05:00
Our view, some kind of list view or combobox, is expected to sync with the list's contents by
appropriately behave to all callbacks in this interface.
"""
2019-09-09 19:54:28 -05:00
def refresh(self):
"""Refreshes the contents of the list widget.
2019-09-09 19:54:28 -05:00
Ensures that the contents of the list widget is synced with the model.
"""
2019-09-09 19:54:28 -05:00
def update_selection(self):
"""Update selection status.
2019-09-09 19:54:28 -05:00
Ensures that the list widget's selection is in sync with the model.
"""
2019-09-09 19:54:28 -05:00
class GUISelectableList(SelectableList, GUIObject):
"""Cross-toolkit GUI-enabled list view.
2019-09-09 19:54:28 -05:00
Represents a UI element presenting the user with a selectable list of items.
2019-09-09 19:54:28 -05:00
Subclasses :class:`SelectableList` and :class:`.GUIObject`. Expected view:
:class:`GUISelectableListView`.
2019-09-09 19:54:28 -05:00
:param iterable items: If specified, items to fill the list with initially.
"""
2019-09-09 19:54:28 -05:00
def __init__(self, items=None):
SelectableList.__init__(self, items)
GUIObject.__init__(self)
2019-09-09 19:54:28 -05:00
def _view_updated(self):
"""Refreshes the view contents with :meth:`GUISelectableListView.refresh`.
2019-09-09 19:54:28 -05:00
Overrides :meth:`~hscommon.gui.base.GUIObject._view_updated`.
"""
self.view.refresh()
2019-09-09 19:54:28 -05:00
def _update_selection(self):
"""Refreshes the view selection with :meth:`GUISelectableListView.update_selection`.
2019-09-09 19:54:28 -05:00
Overrides :meth:`Selectable._update_selection`.
"""
self.view.update_selection()
2019-09-09 19:54:28 -05:00
def _on_change(self):
"""Refreshes the view contents with :meth:`GUISelectableListView.refresh`.
2019-09-09 19:54:28 -05:00
Overrides :meth:`SelectableList._on_change`.
"""
self.view.refresh()