1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2024-11-14 03:29:02 +00:00
dupeguru/hscommon/gui/column.py

285 lines
12 KiB
Python
Raw Normal View History

2019-09-10 00:54:28 +00:00
# Created By: Virgil Dupras
# Created On: 2010-07-25
# 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-10 00:54:28 +00:00
# http://www.gnu.org/licenses/gpl-3.0.html
import copy
2022-05-11 05:50:34 +00:00
from typing import Any, List, Tuple, Union
2019-09-10 00:54:28 +00:00
2022-05-09 06:40:08 +00:00
from hscommon.gui.base import GUIObject
2022-05-11 05:50:34 +00:00
from hscommon.gui.table import GUITable
2019-09-10 00:54:28 +00:00
2019-09-10 00:54:28 +00:00
class Column:
"""Holds column attributes such as its name, width, visibility, etc.
2019-09-10 00:54:28 +00:00
These attributes are then used to correctly configure the column on the "view" side.
"""
2022-05-11 05:50:34 +00:00
def __init__(self, name: str, display: str = "", visible: bool = True, optional: bool = False) -> None:
2019-09-10 00:54:28 +00:00
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
#: as :meth:`Columns.column_by_name`.
self.name = name
#: Immutable index of the column. Doesn't change even when columns are re-ordered. Used in
#: :meth:`Columns.column_by_index`.
self.logical_index = 0
#: Index of the column in the ordered set of columns.
self.ordered_index = 0
#: Width of the column.
self.width = 0
#: Default width of the column. This value usually depends on the platform and is set on
#: columns initialisation. It will be used if column restoration doesn't contain any
#: "remembered" widths.
self.default_width = 0
#: Display name (title) of the column.
self.display = display
#: Whether the column is visible.
self.visible = visible
#: Whether the column is visible by default. It will be used if column restoration doesn't
#: contain any "remembered" widths.
self.default_visible = visible
#: Whether the column can have :attr:`visible` set to false.
self.optional = optional
2019-09-10 00:54:28 +00:00
class ColumnsView:
"""Expected interface for :class:`Columns`'s view.
2019-09-10 00:54:28 +00:00
*Not actually used in the code. For documentation purposes only.*
2019-09-10 00:54:28 +00:00
Our view, the columns controller of a table or outline, is expected to properly respond to
callbacks.
"""
2022-05-11 05:50:34 +00:00
def restore_columns(self) -> None:
2019-09-10 00:54:28 +00:00
"""Update all columns according to the model.
2019-09-10 00:54:28 +00:00
When this is called, our view has to update the columns title, order and visibility of all
columns.
"""
2022-05-11 05:50:34 +00:00
def set_column_visible(self, colname: str, visible: bool) -> None:
2019-09-10 00:54:28 +00:00
"""Update visibility of column ``colname``.
2019-09-10 00:54:28 +00:00
Called when the user toggles the visibility of a column, we must update the column
``colname``'s visibility status to ``visible``.
"""
2019-09-10 00:54:28 +00:00
class PrefAccessInterface:
"""Expected interface for :class:`Columns`'s prefaccess.
2019-09-10 00:54:28 +00:00
*Not actually used in the code. For documentation purposes only.*
"""
2022-05-11 05:50:34 +00:00
def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any:
2019-09-10 00:54:28 +00:00
"""Retrieve the value for ``key`` in the currently running app's preference store.
2019-09-10 00:54:28 +00:00
If the key doesn't exist, return ``fallback_value``.
"""
2022-05-11 05:50:34 +00:00
def set_default(self, key: str, value: Any) -> None:
"""Set the value ``value`` for ``key`` in the currently running app's preference store."""
2019-09-10 00:54:28 +00:00
class Columns(GUIObject):
"""Cross-toolkit GUI-enabled column set for tables or outlines.
2019-09-10 00:54:28 +00:00
Manages a column set's order, visibility and width. We also manage the persistence of these
attributes so that we can restore them on the next run.
2019-09-10 00:54:28 +00:00
Subclasses :class:`.GUIObject`. Expected view: :class:`ColumnsView`.
2019-09-10 00:54:28 +00:00
:param table: The table the columns belong to. It's from there that we retrieve our column
configuration and it must have a ``COLUMNS`` attribute which is a list of
:class:`Column`. We also call :meth:`~.GUITable.save_edits` on it from time to
time. Technically, this argument can also be a tree, but there's probably some
sorting in the code to do to support this option cleanly.
:param prefaccess: An object giving access to user preferences for the currently running app.
We use this to make column attributes persistent. Must follow
:class:`PrefAccessInterface`.
:param str savename: The name under which column preferences will be saved. This name is in fact
a prefix. Preferences are saved under more than one name, but they will all
have that same prefix.
"""
2022-05-11 05:50:34 +00:00
def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None):
2019-09-10 00:54:28 +00:00
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.
2022-05-11 05:50:34 +00:00
self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS))
2019-09-10 00:54:28 +00:00
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
2022-05-11 05:50:34 +00:00
def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:
2019-09-10 00:54:28 +00:00
try:
return getattr(self.coldata[colname], attrname)
except KeyError:
return default
2022-05-11 05:50:34 +00:00
def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:
2019-09-10 00:54:28 +00:00
try:
col = self.coldata[colname]
setattr(col, attrname, value)
except KeyError:
pass
2022-05-11 05:50:34 +00:00
def _optional_columns(self) -> List[Column]:
2019-09-10 00:54:28 +00:00
return [c for c in self.column_list if c.optional]
# --- Override
2022-05-11 05:50:34 +00:00
def _view_updated(self) -> None:
2019-09-10 00:54:28 +00:00
self.restore_columns()
# --- Public
2022-05-11 05:50:34 +00:00
def column_by_index(self, index: int):
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
2019-09-10 00:54:28 +00:00
return self.column_list[index]
2022-05-11 05:50:34 +00:00
def column_by_name(self, name: str):
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
2019-09-10 00:54:28 +00:00
return self.coldata[name]
2022-05-11 05:50:34 +00:00
def columns_count(self) -> int:
"""Returns the number of columns in our set."""
2019-09-10 00:54:28 +00:00
return len(self.column_list)
2022-05-11 05:50:34 +00:00
def column_display(self, colname: str) -> str:
"""Returns display name for column named ``colname``, or ``''`` if there's none."""
return self._get_colname_attr(colname, "display", "")
2022-05-11 05:50:34 +00:00
def column_is_visible(self, colname: str) -> bool:
"""Returns visibility for column named ``colname``, or ``True`` if there's none."""
return self._get_colname_attr(colname, "visible", True)
2022-05-11 05:50:34 +00:00
def column_width(self, colname: str) -> int:
"""Returns width for column named ``colname``, or ``0`` if there's none."""
return self._get_colname_attr(colname, "width", 0)
2022-05-11 05:50:34 +00:00
def columns_to_right(self, colname: str) -> List[str]:
2019-09-10 00:54:28 +00:00
"""Returns the list of all columns to the right of ``colname``.
2019-09-10 00:54:28 +00:00
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
civilization.
"""
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)]
2022-05-11 05:50:34 +00:00
def menu_items(self) -> List[Tuple[str, bool]]:
2019-09-10 00:54:28 +00:00
"""Returns a list of items convenient for quick visibility menu generation.
2019-09-10 00:54:28 +00:00
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
current view (``is_marked`` means that it's visible).
2019-09-10 00:54:28 +00:00
You can use this to generate a menu to let the user toggle the visibility of an optional
column. That is why we only show optional column, because the visibility of mandatory
columns can't be toggled.
"""
return [(c.display, c.visible) for c in self._optional_columns()]
2022-05-11 05:50:34 +00:00
def move_column(self, colname: str, index: int) -> None:
2019-09-10 00:54:28 +00:00
"""Moves column ``colname`` to ``index``.
2019-09-10 00:54:28 +00:00
The column will be placed just in front of the column currently having that index, or to the
end of the list if there's none.
"""
colnames = self.colnames
colnames.remove(colname)
colnames.insert(index, colname)
self.set_column_order(colnames)
2022-05-11 05:50:34 +00:00
def reset_to_defaults(self) -> None:
"""Reset all columns' width and visibility to their default values."""
2019-09-10 00:54:28 +00:00
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()
2022-05-11 05:50:34 +00:00
def resize_column(self, colname: str, newwidth: int) -> None:
"""Set column ``colname``'s width to ``newwidth``."""
self._set_colname_attr(colname, "width", newwidth)
2022-05-11 05:50:34 +00:00
def restore_columns(self) -> None:
"""Restore's column persistent attributes from the last :meth:`save_columns`."""
2019-09-10 00:54:28 +00:00
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:
2022-04-28 01:53:12 +00:00
pref_name = f"{self.savename}.Columns.{col.name}"
2019-09-10 00:54:28 +00:00
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"]
2019-09-10 00:54:28 +00:00
self.view.restore_columns()
2022-05-11 05:50:34 +00:00
def save_columns(self) -> None:
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
2019-09-10 00:54:28 +00:00
if not (self.prefaccess and self.savename and self.coldata):
return
for col in self.column_list:
2022-04-28 01:53:12 +00:00
pref_name = f"{self.savename}.Columns.{col.name}"
coldata = {"index": col.ordered_index, "width": col.width}
2019-09-10 00:54:28 +00:00
if col.optional:
coldata["visible"] = col.visible
2019-09-10 00:54:28 +00:00
self.prefaccess.set_default(pref_name, coldata)
2022-05-11 05:50:34 +00:00
# TODO annotate colnames
def set_column_order(self, colnames) -> None:
2019-09-10 00:54:28 +00:00
"""Change the columns order so it matches the order in ``colnames``.
2019-09-10 00:54:28 +00:00
:param colnames: A list of column names in the desired order.
"""
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
2022-05-11 05:50:34 +00:00
def set_column_visible(self, colname: str, visible: bool) -> None:
"""Set the visibility of column ``colname``."""
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, "visible", visible)
2019-09-10 00:54:28 +00:00
self.view.set_column_visible(colname, visible)
2022-05-11 05:50:34 +00:00
def set_default_width(self, colname: str, width: int) -> None:
"""Set the default width or column ``colname``."""
self._set_colname_attr(colname, "default_width", width)
2022-05-11 05:50:34 +00:00
def toggle_menu_item(self, index: int) -> bool:
2019-09-10 00:54:28 +00:00
"""Toggles the visibility of an optional column.
2019-09-10 00:54:28 +00:00
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
is the index of them menu item in *that* menu that the user has clicked on to toggle it.
2019-09-10 00:54:28 +00:00
Returns whether the column in question ends up being visible or not.
"""
col = self._optional_columns()[index]
self.set_column_visible(col.name, not col.visible)
return col.visible
# --- Properties
2019-09-10 00:54:28 +00:00
@property
2022-05-11 05:50:34 +00:00
def ordered_columns(self) -> List[Column]:
"""List of :class:`Column` in visible order."""
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
2019-09-10 00:54:28 +00:00
@property
2022-05-11 05:50:34 +00:00
def colnames(self) -> List[str]:
"""List of column names in visible order."""
2019-09-10 00:54:28 +00:00
return [col.name for col in self.ordered_columns]