mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-03-12 03:31:37 +00:00
Compare commits
3 Commits
7865e4aeac
...
feature/gu
| Author | SHA1 | Date | |
|---|---|---|---|
|
ade5d7f8c1
|
|||
|
|
b9aabb8545 | ||
|
d5eeab4a17
|
@@ -555,9 +555,13 @@ class DupeGuru(Broadcaster):
|
||||
# a workaround to make the damn thing work.
|
||||
exepath, args = match.groups()
|
||||
path, exename = op.split(exepath)
|
||||
subprocess.Popen(exename + args, shell=True, cwd=path)
|
||||
p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output = p.stdout.read()
|
||||
logging.info("Custom command %s %s: %s", exename, args, output)
|
||||
else:
|
||||
subprocess.Popen(dupe_cmd, shell=True)
|
||||
p = subprocess.Popen(dupe_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output = p.stdout.read()
|
||||
logging.info("Custom command %s: %s", dupe_cmd, output)
|
||||
|
||||
def load(self):
|
||||
"""Load directory selection and ignore list from files in appdata.
|
||||
|
||||
@@ -36,11 +36,11 @@ class GUIObject:
|
||||
``multibind`` flag to ``True`` and the safeguard will be disabled.
|
||||
"""
|
||||
|
||||
def __init__(self, multibind=False):
|
||||
def __init__(self, multibind: bool = False) -> None:
|
||||
self._view = None
|
||||
self._multibind = multibind
|
||||
|
||||
def _view_updated(self):
|
||||
def _view_updated(self) -> None:
|
||||
"""(Virtual) Called after :attr:`view` has been set.
|
||||
|
||||
Doing nothing by default, this method is called after :attr:`view` has been set (it isn't
|
||||
@@ -48,7 +48,7 @@ class GUIObject:
|
||||
(which is often the whole of the initialization code).
|
||||
"""
|
||||
|
||||
def has_view(self):
|
||||
def has_view(self) -> bool:
|
||||
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
|
||||
|
||||
@property
|
||||
@@ -67,7 +67,7 @@ class GUIObject:
|
||||
return self._view
|
||||
|
||||
@view.setter
|
||||
def view(self, value):
|
||||
def view(self, value) -> None:
|
||||
if self._view is None and value is None:
|
||||
# Initial view assignment
|
||||
return
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import copy
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
from hscommon.gui.base import GUIObject
|
||||
from hscommon.gui.table import GUITable
|
||||
|
||||
|
||||
class Column:
|
||||
@@ -17,7 +19,7 @@ class Column:
|
||||
These attributes are then used to correctly configure the column on the "view" side.
|
||||
"""
|
||||
|
||||
def __init__(self, name, display="", visible=True, optional=False):
|
||||
def __init__(self, name: str, display: str = "", visible: bool = True, optional: bool = False) -> None:
|
||||
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
|
||||
#: as :meth:`Columns.column_by_name`.
|
||||
self.name = name
|
||||
@@ -52,14 +54,14 @@ class ColumnsView:
|
||||
callbacks.
|
||||
"""
|
||||
|
||||
def restore_columns(self):
|
||||
def restore_columns(self) -> None:
|
||||
"""Update all columns according to the model.
|
||||
|
||||
When this is called, our view has to update the columns title, order and visibility of all
|
||||
columns.
|
||||
"""
|
||||
|
||||
def set_column_visible(self, colname, visible):
|
||||
def set_column_visible(self, colname: str, visible: bool) -> None:
|
||||
"""Update visibility of column ``colname``.
|
||||
|
||||
Called when the user toggles the visibility of a column, we must update the column
|
||||
@@ -73,13 +75,13 @@ class PrefAccessInterface:
|
||||
*Not actually used in the code. For documentation purposes only.*
|
||||
"""
|
||||
|
||||
def get_default(self, key, fallback_value):
|
||||
def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any:
|
||||
"""Retrieve the value for ``key`` in the currently running app's preference store.
|
||||
|
||||
If the key doesn't exist, return ``fallback_value``.
|
||||
"""
|
||||
|
||||
def set_default(self, key, value):
|
||||
def set_default(self, key: str, value: Any) -> None:
|
||||
"""Set the value ``value`` for ``key`` in the currently running app's preference store."""
|
||||
|
||||
|
||||
@@ -104,65 +106,65 @@ class Columns(GUIObject):
|
||||
have that same prefix.
|
||||
"""
|
||||
|
||||
def __init__(self, table, prefaccess=None, savename=None):
|
||||
def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None):
|
||||
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.
|
||||
self.column_list = list(map(copy.copy, table.COLUMNS))
|
||||
self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS))
|
||||
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
|
||||
def _get_colname_attr(self, colname, attrname, default):
|
||||
def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:
|
||||
try:
|
||||
return getattr(self.coldata[colname], attrname)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def _set_colname_attr(self, colname, attrname, value):
|
||||
def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:
|
||||
try:
|
||||
col = self.coldata[colname]
|
||||
setattr(col, attrname, value)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _optional_columns(self):
|
||||
def _optional_columns(self) -> List[Column]:
|
||||
return [c for c in self.column_list if c.optional]
|
||||
|
||||
# --- Override
|
||||
def _view_updated(self):
|
||||
def _view_updated(self) -> None:
|
||||
self.restore_columns()
|
||||
|
||||
# --- Public
|
||||
def column_by_index(self, index):
|
||||
def column_by_index(self, index: int):
|
||||
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
|
||||
return self.column_list[index]
|
||||
|
||||
def column_by_name(self, name):
|
||||
def column_by_name(self, name: str):
|
||||
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
|
||||
return self.coldata[name]
|
||||
|
||||
def columns_count(self):
|
||||
def columns_count(self) -> int:
|
||||
"""Returns the number of columns in our set."""
|
||||
return len(self.column_list)
|
||||
|
||||
def column_display(self, colname):
|
||||
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", "")
|
||||
|
||||
def column_is_visible(self, colname):
|
||||
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)
|
||||
|
||||
def column_width(self, colname):
|
||||
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)
|
||||
|
||||
def columns_to_right(self, colname):
|
||||
def columns_to_right(self, colname: str) -> List[str]:
|
||||
"""Returns the list of all columns to the right of ``colname``.
|
||||
|
||||
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
|
||||
@@ -172,7 +174,7 @@ class Columns(GUIObject):
|
||||
index = column.ordered_index
|
||||
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
||||
|
||||
def menu_items(self):
|
||||
def menu_items(self) -> List[Tuple[str, bool]]:
|
||||
"""Returns a list of items convenient for quick visibility menu generation.
|
||||
|
||||
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
|
||||
@@ -184,7 +186,7 @@ class Columns(GUIObject):
|
||||
"""
|
||||
return [(c.display, c.visible) for c in self._optional_columns()]
|
||||
|
||||
def move_column(self, colname, index):
|
||||
def move_column(self, colname: str, index: int) -> None:
|
||||
"""Moves column ``colname`` to ``index``.
|
||||
|
||||
The column will be placed just in front of the column currently having that index, or to the
|
||||
@@ -195,7 +197,7 @@ class Columns(GUIObject):
|
||||
colnames.insert(index, colname)
|
||||
self.set_column_order(colnames)
|
||||
|
||||
def reset_to_defaults(self):
|
||||
def reset_to_defaults(self) -> None:
|
||||
"""Reset all columns' width and visibility to their default values."""
|
||||
self.set_column_order([col.name for col in self.column_list])
|
||||
for col in self._optional_columns():
|
||||
@@ -203,11 +205,11 @@ class Columns(GUIObject):
|
||||
col.width = col.default_width
|
||||
self.view.restore_columns()
|
||||
|
||||
def resize_column(self, colname, newwidth):
|
||||
def resize_column(self, colname: str, newwidth: int) -> None:
|
||||
"""Set column ``colname``'s width to ``newwidth``."""
|
||||
self._set_colname_attr(colname, "width", newwidth)
|
||||
|
||||
def restore_columns(self):
|
||||
def restore_columns(self) -> None:
|
||||
"""Restore's column persistent attributes from the last :meth:`save_columns`."""
|
||||
if not (self.prefaccess and self.savename and self.coldata):
|
||||
if (not self.savename) and (self.coldata):
|
||||
@@ -226,7 +228,7 @@ class Columns(GUIObject):
|
||||
col.visible = coldata["visible"]
|
||||
self.view.restore_columns()
|
||||
|
||||
def save_columns(self):
|
||||
def save_columns(self) -> None:
|
||||
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
|
||||
if not (self.prefaccess and self.savename and self.coldata):
|
||||
return
|
||||
@@ -237,7 +239,8 @@ class Columns(GUIObject):
|
||||
coldata["visible"] = col.visible
|
||||
self.prefaccess.set_default(pref_name, coldata)
|
||||
|
||||
def set_column_order(self, colnames):
|
||||
# TODO annotate colnames
|
||||
def set_column_order(self, colnames) -> None:
|
||||
"""Change the columns order so it matches the order in ``colnames``.
|
||||
|
||||
:param colnames: A list of column names in the desired order.
|
||||
@@ -247,17 +250,17 @@ class Columns(GUIObject):
|
||||
col = self.coldata[colname]
|
||||
col.ordered_index = i
|
||||
|
||||
def set_column_visible(self, colname, visible):
|
||||
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)
|
||||
self.view.set_column_visible(colname, visible)
|
||||
|
||||
def set_default_width(self, colname, width):
|
||||
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)
|
||||
|
||||
def toggle_menu_item(self, index):
|
||||
def toggle_menu_item(self, index: int) -> bool:
|
||||
"""Toggles the visibility of an optional column.
|
||||
|
||||
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
|
||||
@@ -271,11 +274,11 @@ class Columns(GUIObject):
|
||||
|
||||
# --- Properties
|
||||
@property
|
||||
def ordered_columns(self):
|
||||
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)]
|
||||
|
||||
@property
|
||||
def colnames(self):
|
||||
def colnames(self) -> List[str]:
|
||||
"""List of column names in visible order."""
|
||||
return [col.name for col in self.ordered_columns]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from typing import Callable, Tuple, Union
|
||||
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
||||
from hscommon.gui.base import GUIObject
|
||||
from hscommon.gui.text_field import TextField
|
||||
@@ -20,13 +21,13 @@ class ProgressWindowView:
|
||||
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
||||
"""
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
"""Show the dialog."""
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Close the dialog."""
|
||||
|
||||
def set_progress(self, progress):
|
||||
def set_progress(self, progress: int) -> None:
|
||||
"""Set the progress of the progress bar to ``progress``.
|
||||
|
||||
Not all jobs are equally responsive on their job progress report and it is recommended that
|
||||
@@ -60,7 +61,11 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
called as if the job terminated normally.
|
||||
"""
|
||||
|
||||
def __init__(self, finish_func, error_func=None):
|
||||
def __init__(
|
||||
self,
|
||||
finish_func: Callable[[Union[str, None]], None],
|
||||
error_func: Callable[[Union[str, None], Exception], bool] = None,
|
||||
) -> None:
|
||||
# finish_func(jobid) is the function that is called when a job is completed.
|
||||
GUIObject.__init__(self)
|
||||
ThreadedJobPerformer.__init__(self)
|
||||
@@ -71,9 +76,9 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
#: :class:`.TextField`. It contains the job textual update that the function might yield
|
||||
#: during its course.
|
||||
self.progressdesc_textfield = TextField()
|
||||
self.jobid = None
|
||||
self.jobid: Union[str, None] = None
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
"""Call for a user-initiated job cancellation."""
|
||||
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
|
||||
# make sure that this doesn't lead us to think that the user acually cancelled the task, so
|
||||
@@ -81,7 +86,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
if self._job_running:
|
||||
self.job_cancelled = True
|
||||
|
||||
def pulse(self):
|
||||
def pulse(self) -> None:
|
||||
"""Update progress reports in the GUI.
|
||||
|
||||
Call this regularly from the GUI main run loop. The values might change before
|
||||
@@ -111,7 +116,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
self.progressdesc_textfield.text = last_desc
|
||||
self.view.set_progress(last_progress)
|
||||
|
||||
def run(self, jobid, title, target, args=()):
|
||||
def run(self, jobid: str, title: str, target: Callable, args: Tuple = ()):
|
||||
"""Starts a threaded job.
|
||||
|
||||
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
from collections.abc import MutableSequence
|
||||
from collections import namedtuple
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
from hscommon.gui.base import GUIObject
|
||||
from hscommon.gui.selectable_list import Selectable
|
||||
@@ -27,12 +28,16 @@ class Table(MutableSequence, Selectable):
|
||||
Subclasses :class:`.Selectable`.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Selectable.__init__(self)
|
||||
self._rows = []
|
||||
self._header = None
|
||||
self._footer = None
|
||||
# Should be List[Column], but have circular import...
|
||||
COLUMNS: List = []
|
||||
|
||||
def __init__(self) -> None:
|
||||
Selectable.__init__(self)
|
||||
self._rows: List["Row"] = []
|
||||
self._header: Union["Row", None] = None
|
||||
self._footer: Union["Row", None] = None
|
||||
|
||||
# TODO type hint for key
|
||||
def __delitem__(self, key):
|
||||
self._rows.__delitem__(key)
|
||||
if self._header is not None and ((not self) or (self[0] is not self._header)):
|
||||
@@ -41,16 +46,18 @@ class Table(MutableSequence, Selectable):
|
||||
self._footer = None
|
||||
self._check_selection_range()
|
||||
|
||||
def __getitem__(self, key):
|
||||
# TODO type hint for key
|
||||
def __getitem__(self, key) -> Any:
|
||||
return self._rows.__getitem__(key)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._rows)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# TODO type hint for key
|
||||
def __setitem__(self, key, value: Any) -> None:
|
||||
self._rows.__setitem__(key, value)
|
||||
|
||||
def append(self, item):
|
||||
def append(self, item: "Row") -> None:
|
||||
"""Appends ``item`` at the end of the table.
|
||||
|
||||
If there's a footer, the item is inserted before it.
|
||||
@@ -60,7 +67,7 @@ class Table(MutableSequence, Selectable):
|
||||
else:
|
||||
self._rows.append(item)
|
||||
|
||||
def insert(self, index, item):
|
||||
def insert(self, index: int, item: "Row") -> None:
|
||||
"""Inserts ``item`` at ``index`` in the table.
|
||||
|
||||
If there's a header, will make sure we don't insert before it, and if there's a footer, will
|
||||
@@ -72,7 +79,7 @@ class Table(MutableSequence, Selectable):
|
||||
index = len(self) - 1
|
||||
self._rows.insert(index, item)
|
||||
|
||||
def remove(self, row):
|
||||
def remove(self, row: "Row") -> None:
|
||||
"""Removes ``row`` from table.
|
||||
|
||||
If ``row`` is a header or footer, that header or footer will be set to ``None``.
|
||||
@@ -84,7 +91,7 @@ class Table(MutableSequence, Selectable):
|
||||
self._rows.remove(row)
|
||||
self._check_selection_range()
|
||||
|
||||
def sort_by(self, column_name, desc=False):
|
||||
def sort_by(self, column_name: str, desc: bool = False) -> None:
|
||||
"""Sort table by ``column_name``.
|
||||
|
||||
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
||||
@@ -105,7 +112,7 @@ class Table(MutableSequence, Selectable):
|
||||
|
||||
# --- Properties
|
||||
@property
|
||||
def footer(self):
|
||||
def footer(self) -> Union["Row", None]:
|
||||
"""If set, a row that always stay at the bottom of the table.
|
||||
|
||||
:class:`Row`. *get/set*.
|
||||
@@ -128,7 +135,7 @@ class Table(MutableSequence, Selectable):
|
||||
return self._footer
|
||||
|
||||
@footer.setter
|
||||
def footer(self, value):
|
||||
def footer(self, value: Union["Row", None]) -> None:
|
||||
if self._footer is not None:
|
||||
self._rows.pop()
|
||||
if value is not None:
|
||||
@@ -136,7 +143,7 @@ class Table(MutableSequence, Selectable):
|
||||
self._footer = value
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
def header(self) -> Union["Row", None]:
|
||||
"""If set, a row that always stay at the bottom of the table.
|
||||
|
||||
See :attr:`footer` for details.
|
||||
@@ -144,7 +151,7 @@ class Table(MutableSequence, Selectable):
|
||||
return self._header
|
||||
|
||||
@header.setter
|
||||
def header(self, value):
|
||||
def header(self, value: Union["Row", None]) -> None:
|
||||
if self._header is not None:
|
||||
self._rows.pop(0)
|
||||
if value is not None:
|
||||
@@ -152,7 +159,7 @@ class Table(MutableSequence, Selectable):
|
||||
self._header = value
|
||||
|
||||
@property
|
||||
def row_count(self):
|
||||
def row_count(self) -> int:
|
||||
"""Number or rows in the table (without counting header and footer).
|
||||
|
||||
*int*. *read-only*.
|
||||
@@ -165,7 +172,7 @@ class Table(MutableSequence, Selectable):
|
||||
return result
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
def rows(self) -> List["Row"]:
|
||||
"""List of rows in the table, excluding header and footer.
|
||||
|
||||
List of :class:`Row`. *read-only*.
|
||||
@@ -179,7 +186,7 @@ class Table(MutableSequence, Selectable):
|
||||
return self[start:end]
|
||||
|
||||
@property
|
||||
def selected_row(self):
|
||||
def selected_row(self) -> "Row":
|
||||
"""Selected row according to :attr:`Selectable.selected_index`.
|
||||
|
||||
:class:`Row`. *get/set*.
|
||||
@@ -190,14 +197,14 @@ class Table(MutableSequence, Selectable):
|
||||
return self[self.selected_index] if self.selected_index is not None else None
|
||||
|
||||
@selected_row.setter
|
||||
def selected_row(self, value):
|
||||
def selected_row(self, value: int) -> None:
|
||||
try:
|
||||
self.selected_index = self.index(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def selected_rows(self):
|
||||
def selected_rows(self) -> List["Row"]:
|
||||
"""List of selected rows based on :attr:`.selected_indexes`.
|
||||
|
||||
List of :class:`Row`. *read-only*.
|
||||
@@ -219,20 +226,20 @@ class GUITableView:
|
||||
Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.
|
||||
"""
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
"""Refreshes the contents of the table widget.
|
||||
|
||||
Ensures that the contents of the table widget is synced with the model. This includes
|
||||
selection.
|
||||
"""
|
||||
|
||||
def start_editing(self):
|
||||
def start_editing(self) -> None:
|
||||
"""Start editing the currently selected row.
|
||||
|
||||
Begin whatever inline editing support that the view supports.
|
||||
"""
|
||||
|
||||
def stop_editing(self):
|
||||
def stop_editing(self) -> None:
|
||||
"""Stop editing if there's an inline editing in effect.
|
||||
|
||||
There's no "aborting" implied in this call, so it's appropriate to send whatever the user
|
||||
@@ -260,33 +267,33 @@ class GUITable(Table, GUIObject):
|
||||
:class:`GUITableView`.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
GUIObject.__init__(self)
|
||||
Table.__init__(self)
|
||||
#: The row being currently edited by the user. ``None`` if no edit is taking place.
|
||||
self.edited = None
|
||||
self._sort_descriptor = None
|
||||
self.edited: Union["Row", None] = None
|
||||
self._sort_descriptor: Union[SortDescriptor, None] = None
|
||||
|
||||
# --- Virtual
|
||||
def _do_add(self):
|
||||
def _do_add(self) -> Tuple["Row", int]:
|
||||
"""(Virtual) Creates a new row, adds it in the table.
|
||||
|
||||
Returns ``(row, insert_index)``.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _do_delete(self):
|
||||
def _do_delete(self) -> None:
|
||||
"""(Virtual) Delete the selected rows."""
|
||||
pass
|
||||
|
||||
def _fill(self):
|
||||
def _fill(self) -> None:
|
||||
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
|
||||
|
||||
Called by :meth:`refresh`. Does nothing by default.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _is_edited_new(self):
|
||||
def _is_edited_new(self) -> bool:
|
||||
"""(Virtual) Returns whether the currently edited row should be considered "new".
|
||||
|
||||
This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a
|
||||
@@ -315,7 +322,7 @@ class GUITable(Table, GUIObject):
|
||||
self.select([len(self) - 1])
|
||||
|
||||
# --- Public
|
||||
def add(self):
|
||||
def add(self) -> None:
|
||||
"""Add a new row in edit mode.
|
||||
|
||||
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
|
||||
@@ -334,7 +341,7 @@ class GUITable(Table, GUIObject):
|
||||
self.edited = row
|
||||
self.view.start_editing()
|
||||
|
||||
def can_edit_cell(self, column_name, row_index):
|
||||
def can_edit_cell(self, column_name: str, row_index: int) -> bool:
|
||||
"""Returns whether the cell at ``row_index`` and ``column_name`` can be edited.
|
||||
|
||||
A row is, by default, editable as soon as it has an attr with the same name as `column`.
|
||||
@@ -346,7 +353,7 @@ class GUITable(Table, GUIObject):
|
||||
row = self[row_index]
|
||||
return row.can_edit_cell(column_name)
|
||||
|
||||
def cancel_edits(self):
|
||||
def cancel_edits(self) -> None:
|
||||
"""Cancels the current edit operation.
|
||||
|
||||
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
|
||||
@@ -364,7 +371,7 @@ class GUITable(Table, GUIObject):
|
||||
self.edited = None
|
||||
self.view.refresh()
|
||||
|
||||
def delete(self):
|
||||
def delete(self) -> None:
|
||||
"""Delete the currently selected rows.
|
||||
|
||||
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
|
||||
@@ -377,7 +384,7 @@ class GUITable(Table, GUIObject):
|
||||
if self:
|
||||
self._do_delete()
|
||||
|
||||
def refresh(self, refresh_view=True):
|
||||
def refresh(self, refresh_view: bool = True) -> None:
|
||||
"""Empty the table and re-create its rows.
|
||||
|
||||
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
|
||||
@@ -399,7 +406,7 @@ class GUITable(Table, GUIObject):
|
||||
if refresh_view:
|
||||
self.view.refresh()
|
||||
|
||||
def save_edits(self):
|
||||
def save_edits(self) -> None:
|
||||
"""Commit user edits to the model.
|
||||
|
||||
This is done by calling :meth:`Row.save`.
|
||||
@@ -410,7 +417,7 @@ class GUITable(Table, GUIObject):
|
||||
self.edited = None
|
||||
row.save()
|
||||
|
||||
def sort_by(self, column_name, desc=False):
|
||||
def sort_by(self, column_name: str, desc: bool = False) -> None:
|
||||
"""Sort table by ``column_name``.
|
||||
|
||||
Overrides :meth:`Table.sort_by`. After having performed sorting, calls
|
||||
@@ -450,18 +457,18 @@ class Row:
|
||||
Of course, this is only default behavior. This can be overriden.
|
||||
"""
|
||||
|
||||
def __init__(self, table):
|
||||
def __init__(self, table: GUITable) -> None:
|
||||
super().__init__()
|
||||
self.table = table
|
||||
|
||||
def _edit(self):
|
||||
def _edit(self) -> None:
|
||||
if self.table.edited is self:
|
||||
return
|
||||
assert self.table.edited is None
|
||||
self.table.edited = self
|
||||
|
||||
# --- Virtual
|
||||
def can_edit(self):
|
||||
def can_edit(self) -> bool:
|
||||
"""(Virtual) Whether the whole row can be edited.
|
||||
|
||||
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
||||
@@ -469,7 +476,7 @@ class Row:
|
||||
"""
|
||||
return True
|
||||
|
||||
def load(self):
|
||||
def load(self) -> None:
|
||||
"""(Virtual/Required) Loads up values from the model to be presented in the table.
|
||||
|
||||
Usually, our model instances contain values that are not quite ready for display. If you
|
||||
@@ -478,7 +485,7 @@ class Row:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def save(self):
|
||||
def save(self) -> None:
|
||||
"""(Virtual/Required) Saves user edits into your model.
|
||||
|
||||
If your table is editable, this is called when the user commits his changes. Usually, these
|
||||
@@ -487,7 +494,7 @@ class Row:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def sort_key_for_column(self, column_name):
|
||||
def sort_key_for_column(self, column_name: str) -> Any:
|
||||
"""(Virtual) Return the value that is to be used to sort by column ``column_name``.
|
||||
|
||||
By default, looks for an attribute with the same name as ``column_name``, but with an
|
||||
@@ -500,7 +507,7 @@ class Row:
|
||||
return getattr(self, column_name)
|
||||
|
||||
# --- Public
|
||||
def can_edit_cell(self, column_name):
|
||||
def can_edit_cell(self, column_name: str) -> bool:
|
||||
"""Returns whether cell for column ``column_name`` can be edited.
|
||||
|
||||
By the default, the check is done in many steps:
|
||||
@@ -530,7 +537,7 @@ class Row:
|
||||
return False
|
||||
return bool(getattr(prop, "fset", None))
|
||||
|
||||
def get_cell_value(self, attrname):
|
||||
def get_cell_value(self, attrname: str) -> Any:
|
||||
"""Get cell value for ``attrname``.
|
||||
|
||||
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
||||
@@ -540,7 +547,7 @@ class Row:
|
||||
attrname = "from_"
|
||||
return getattr(self, attrname)
|
||||
|
||||
def set_cell_value(self, attrname, value):
|
||||
def set_cell_value(self, attrname: str, value: Any) -> None:
|
||||
"""Set cell value to ``value`` for ``attrname``.
|
||||
|
||||
By default, does a simple ``setattr()``, but it is used to allow subclasses to have
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
from typing import Any, Callable, Generator, Iterator, List, Union
|
||||
|
||||
|
||||
class JobCancelled(Exception):
|
||||
"The user has cancelled the job"
|
||||
|
||||
@@ -36,7 +39,7 @@ class Job:
|
||||
"""
|
||||
|
||||
# ---Magic functions
|
||||
def __init__(self, job_proportions, callback):
|
||||
def __init__(self, job_proportions: Union[List[int], int], callback: Callable) -> None:
|
||||
"""Initialize the Job with 'jobcount' jobs. Start every job with
|
||||
start_job(). Every time the job progress is updated, 'callback' is called
|
||||
'callback' takes a 'progress' int param, and a optional 'desc'
|
||||
@@ -55,12 +58,12 @@ class Job:
|
||||
self._currmax = 1
|
||||
|
||||
# ---Private
|
||||
def _subjob_callback(self, progress, desc=""):
|
||||
def _subjob_callback(self, progress: int, desc: str = "") -> bool:
|
||||
"""This is the callback passed to children jobs."""
|
||||
self.set_progress(progress, desc)
|
||||
return True # if JobCancelled has to be raised, it will be at the highest level
|
||||
|
||||
def _do_update(self, desc):
|
||||
def _do_update(self, desc: str) -> None:
|
||||
"""Calls the callback function with a % progress as a parameter.
|
||||
|
||||
The parameter is a int in the 0-100 range.
|
||||
@@ -78,13 +81,16 @@ class Job:
|
||||
raise JobCancelled()
|
||||
|
||||
# ---Public
|
||||
def add_progress(self, progress=1, desc=""):
|
||||
def add_progress(self, progress: int = 1, desc: str = "") -> None:
|
||||
self.set_progress(self._progress + progress, desc)
|
||||
|
||||
def check_if_cancelled(self):
|
||||
def check_if_cancelled(self) -> None:
|
||||
self._do_update("")
|
||||
|
||||
def iter_with_progress(self, iterable, desc_format=None, every=1, count=None):
|
||||
# TODO type hint iterable
|
||||
def iter_with_progress(
|
||||
self, iterable, desc_format: Union[str, None] = None, every: int = 1, count: Union[int, None] = None
|
||||
) -> Generator[Any, None, None]:
|
||||
"""Iterate through ``iterable`` while automatically adding progress.
|
||||
|
||||
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
|
||||
@@ -107,7 +113,7 @@ class Job:
|
||||
desc = desc_format % (count, count)
|
||||
self.set_progress(100, desc)
|
||||
|
||||
def start_job(self, max_progress=100, desc=""):
|
||||
def start_job(self, max_progress: int = 100, desc: str = "") -> None:
|
||||
"""Begin work on the next job. You must not call start_job more than
|
||||
'jobcount' (in __init__) times.
|
||||
'max' is the job units you are to perform.
|
||||
@@ -122,7 +128,7 @@ class Job:
|
||||
self._currmax = max(1, max_progress)
|
||||
self._do_update(desc)
|
||||
|
||||
def start_subjob(self, job_proportions, desc=""):
|
||||
def start_subjob(self, job_proportions: Union[List[int], int], desc: str = "") -> "Job":
|
||||
"""Starts a sub job. Use this when you want to split a job into
|
||||
multiple smaller jobs. Pretty handy when starting a process where you
|
||||
know how many subjobs you will have, but don't know the work unit count
|
||||
@@ -132,7 +138,7 @@ class Job:
|
||||
self.start_job(100, desc)
|
||||
return Job(job_proportions, self._subjob_callback)
|
||||
|
||||
def set_progress(self, progress, desc=""):
|
||||
def set_progress(self, progress: int, desc: str = "") -> None:
|
||||
"""Sets the progress of the current job to 'progress', and call the
|
||||
callback
|
||||
"""
|
||||
@@ -143,29 +149,29 @@ class Job:
|
||||
|
||||
|
||||
class NullJob:
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def add_progress(self, *args, **kwargs):
|
||||
def add_progress(self, *args, **kwargs) -> None:
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def check_if_cancelled(self):
|
||||
def check_if_cancelled(self) -> None:
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
||||
def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator:
|
||||
return iter(sequence)
|
||||
|
||||
def start_job(self, *args, **kwargs):
|
||||
def start_job(self, *args, **kwargs) -> None:
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def start_subjob(self, *args, **kwargs):
|
||||
def start_subjob(self, *args, **kwargs) -> "NullJob":
|
||||
return NullJob()
|
||||
|
||||
def set_progress(self, *args, **kwargs):
|
||||
def set_progress(self, *args, **kwargs) -> None:
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
from threading import Thread
|
||||
import sys
|
||||
from typing import Callable, Tuple, Union
|
||||
|
||||
from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled
|
||||
|
||||
@@ -28,15 +29,15 @@ class ThreadedJobPerformer:
|
||||
last_error = None
|
||||
|
||||
# --- Protected
|
||||
def create_job(self):
|
||||
def create_job(self) -> Job:
|
||||
if self._job_running:
|
||||
raise JobInProgressError()
|
||||
self.last_progress = -1
|
||||
self.last_progress: Union[int, None] = -1
|
||||
self.last_desc = ""
|
||||
self.job_cancelled = False
|
||||
return Job(1, self._update_progress)
|
||||
|
||||
def _async_run(self, *args):
|
||||
def _async_run(self, *args) -> None:
|
||||
target = args[0]
|
||||
args = tuple(args[1:])
|
||||
self._job_running = True
|
||||
@@ -52,7 +53,7 @@ class ThreadedJobPerformer:
|
||||
self._job_running = False
|
||||
self.last_progress = None
|
||||
|
||||
def reraise_if_error(self):
|
||||
def reraise_if_error(self) -> None:
|
||||
"""Reraises the error that happened in the thread if any.
|
||||
|
||||
Call this after the caller of run_threaded detected that self._job_running returned to False
|
||||
@@ -60,13 +61,13 @@ class ThreadedJobPerformer:
|
||||
if self.last_error is not None:
|
||||
raise self.last_error.with_traceback(self.last_traceback)
|
||||
|
||||
def _update_progress(self, newprogress, newdesc=""):
|
||||
def _update_progress(self, newprogress: int, newdesc: str = "") -> bool:
|
||||
self.last_progress = newprogress
|
||||
if newdesc:
|
||||
self.last_desc = newdesc
|
||||
return not self.job_cancelled
|
||||
|
||||
def run_threaded(self, target, args=()):
|
||||
def run_threaded(self, target: Callable, args: Tuple = ()) -> None:
|
||||
if self._job_running:
|
||||
raise JobInProgressError()
|
||||
args = (target,) + args
|
||||
|
||||
@@ -15,7 +15,7 @@ tr = trget("ui")
|
||||
|
||||
|
||||
class DetailsDialog(DetailsDialogBase):
|
||||
def _setupUi(self):
|
||||
def _setupUi(self) -> None:
|
||||
self.setWindowTitle(tr("Details"))
|
||||
self.resize(502, 295)
|
||||
self.setMinimumSize(QSize(250, 250))
|
||||
|
||||
@@ -4,27 +4,22 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from typing import Callable
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QWidget,
|
||||
)
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
|
||||
|
||||
from hscommon.trans import trget
|
||||
from core.app import AppMode
|
||||
from core.scanner import ScanType
|
||||
from qt.preferences import Preferences
|
||||
|
||||
from qt.preferences_dialog import PreferencesDialogBase
|
||||
from qt.preferences_dialog import PreferencesDialogBase, Sections
|
||||
|
||||
tr = trget("ui")
|
||||
|
||||
|
||||
class PreferencesDialog(PreferencesDialogBase):
|
||||
def _setupPreferenceWidgets(self):
|
||||
def _setupPreferenceWidgets(self) -> None:
|
||||
self._setupFilterHardnessBox()
|
||||
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
||||
self.widget = QWidget(self)
|
||||
@@ -70,7 +65,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
||||
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
|
||||
self._setupBottomPart()
|
||||
|
||||
def _load(self, prefs, setchecked, section):
|
||||
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
|
||||
setchecked(self.tagTrackBox, prefs.scan_tag_track)
|
||||
setchecked(self.tagArtistBox, prefs.scan_tag_artist)
|
||||
setchecked(self.tagAlbumBox, prefs.scan_tag_album)
|
||||
@@ -99,7 +94,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
||||
self.tagGenreBox.setEnabled(tag_based)
|
||||
self.tagYearBox.setEnabled(tag_based)
|
||||
|
||||
def _save(self, prefs, ischecked):
|
||||
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
|
||||
prefs.scan_tag_track = ischecked(self.tagTrackBox)
|
||||
prefs.scan_tag_artist = ischecked(self.tagArtistBox)
|
||||
prefs.scan_tag_album = ischecked(self.tagAlbumBox)
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
|
||||
from typing import Union
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt5.QtWidgets import QWidget, QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
|
||||
from PyQt5.QtGui import QResizeEvent
|
||||
from hscommon.trans import trget
|
||||
from qt import app
|
||||
from qt.details_dialog import DetailsDialog as DetailsDialogBase
|
||||
from qt.details_table import DetailsTable
|
||||
from qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
|
||||
@@ -16,15 +18,15 @@ tr = trget("ui")
|
||||
|
||||
|
||||
class DetailsDialog(DetailsDialogBase):
|
||||
def __init__(self, parent, app):
|
||||
self.vController = None
|
||||
def __init__(self, parent: QWidget, app: "app.DupeGuru") -> None:
|
||||
self.vController: Union[ScrollAreaController, None] = None
|
||||
super().__init__(parent, app)
|
||||
|
||||
def _setupUi(self):
|
||||
def _setupUi(self) -> None:
|
||||
self.setWindowTitle(tr("Details"))
|
||||
self.resize(502, 502)
|
||||
self.setMinimumSize(QSize(250, 250))
|
||||
self.splitter = QSplitter(Qt.Vertical)
|
||||
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
||||
self.topFrame = EmittingFrame()
|
||||
self.topFrame.setFrameShape(QFrame.StyledPanel)
|
||||
self.horizontalLayout = QGridLayout()
|
||||
@@ -47,8 +49,8 @@ class DetailsDialog(DetailsDialogBase):
|
||||
self.vController = ScrollAreaController(self)
|
||||
|
||||
self.verticalToolBar = ViewerToolBar(self, self.vController)
|
||||
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
|
||||
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
|
||||
self.verticalToolBar.setOrientation(Qt.Orientation.Vertical)
|
||||
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
|
||||
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
|
||||
@@ -73,7 +75,7 @@ class DetailsDialog(DetailsDialogBase):
|
||||
|
||||
self.topFrame.resized.connect(self.resizeEvent)
|
||||
|
||||
def _update(self):
|
||||
def _update(self) -> None:
|
||||
if self.vController is None: # Not yet constructed!
|
||||
return
|
||||
if not self.app.model.selected_dupes:
|
||||
@@ -87,15 +89,14 @@ class DetailsDialog(DetailsDialogBase):
|
||||
self.vController.updateView(ref, dupe, group)
|
||||
|
||||
# --- Override
|
||||
@pyqtSlot(QResizeEvent)
|
||||
def resizeEvent(self, event):
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.ensure_same_sizes()
|
||||
if self.vController is None or not self.vController.bestFit:
|
||||
return
|
||||
# Only update the scaled down pixmaps
|
||||
self.vController.updateBothImages()
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
# Give the splitter a maximum height to reach. This is assuming that
|
||||
# all rows below their headers have the same height
|
||||
self.tableView.setMaximumHeight(
|
||||
@@ -108,7 +109,7 @@ class DetailsDialog(DetailsDialogBase):
|
||||
self.ensure_same_sizes()
|
||||
self._update()
|
||||
|
||||
def ensure_same_sizes(self):
|
||||
def ensure_same_sizes(self) -> None:
|
||||
# HACK This ensures same size while shrinking.
|
||||
# ReferenceViewer might be 1 pixel shorter in width
|
||||
# due to the toolbar in the middle keeping the same width,
|
||||
@@ -126,7 +127,7 @@ class DetailsDialog(DetailsDialogBase):
|
||||
self.selectedImageViewer.resize(self.referenceImageViewer.size())
|
||||
|
||||
# model --> view
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
DetailsDialogBase.refresh(self)
|
||||
if self.isVisible():
|
||||
self._update()
|
||||
@@ -137,5 +138,5 @@ class EmittingFrame(QFrame):
|
||||
|
||||
resized = pyqtSignal(QResizeEvent)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.resized.emit(event)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from typing import Union, cast
|
||||
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
|
||||
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
|
||||
from PyQt5.QtWidgets import (
|
||||
@@ -17,6 +18,7 @@ from PyQt5.QtWidgets import (
|
||||
QAbstractScrollArea,
|
||||
QStyle,
|
||||
)
|
||||
from qt.details_dialog import DetailsDialog
|
||||
from hscommon.trans import trget
|
||||
from hscommon.plat import ISLINUX
|
||||
|
||||
@@ -26,7 +28,7 @@ MAX_SCALE = 12.0
|
||||
MIN_SCALE = 0.1
|
||||
|
||||
|
||||
def create_actions(actions, target):
|
||||
def create_actions(actions: list, target: QObject) -> None:
|
||||
# actions are list of (name, shortcut, icon, desc, func)
|
||||
for name, shortcut, icon, desc, func in actions:
|
||||
action = QAction(target)
|
||||
@@ -40,9 +42,9 @@ def create_actions(actions, target):
|
||||
|
||||
|
||||
class ViewerToolBar(QToolBar):
|
||||
def __init__(self, parent, controller):
|
||||
def __init__(self, parent: DetailsDialog, controller: "BaseController") -> None:
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.setParent(parent)
|
||||
self.controller = controller
|
||||
self.setupActions(controller)
|
||||
self.createButtons()
|
||||
@@ -52,24 +54,21 @@ class ViewerToolBar(QToolBar):
|
||||
self.buttonNormalSize.setEnabled(False)
|
||||
self.buttonBestFit.setEnabled(False)
|
||||
|
||||
def setupActions(self, controller):
|
||||
def setupActions(self, controller: "BaseController") -> None:
|
||||
override_icons = cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
|
||||
# actions are list of (name, shortcut, icon, desc, func)
|
||||
ACTIONS = [
|
||||
(
|
||||
"actionZoomIn",
|
||||
QKeySequence.ZoomIn,
|
||||
QIcon.fromTheme("zoom-in")
|
||||
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||
else QIcon(QPixmap(":/" + "zoom_in")),
|
||||
QIcon.fromTheme("zoom-in") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_in")),
|
||||
tr("Increase zoom"),
|
||||
controller.zoomIn,
|
||||
),
|
||||
(
|
||||
"actionZoomOut",
|
||||
QKeySequence.ZoomOut,
|
||||
QIcon.fromTheme("zoom-out")
|
||||
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||
else QIcon(QPixmap(":/" + "zoom_out")),
|
||||
QIcon.fromTheme("zoom-out") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_out")),
|
||||
tr("Decrease zoom"),
|
||||
controller.zoomOut,
|
||||
),
|
||||
@@ -77,7 +76,7 @@ class ViewerToolBar(QToolBar):
|
||||
"actionNormalSize",
|
||||
tr("Ctrl+/"),
|
||||
QIcon.fromTheme("zoom-original")
|
||||
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||
if ISLINUX and not override_icons
|
||||
else QIcon(QPixmap(":/" + "zoom_original")),
|
||||
tr("Normal size"),
|
||||
controller.zoomNormalSize,
|
||||
@@ -86,7 +85,7 @@ class ViewerToolBar(QToolBar):
|
||||
"actionBestFit",
|
||||
tr("Ctrl+*"),
|
||||
QIcon.fromTheme("zoom-best-fit")
|
||||
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||
if ISLINUX and not override_icons
|
||||
else QIcon(QPixmap(":/" + "zoom_best_fit")),
|
||||
tr("Best fit"),
|
||||
controller.zoomBestFit,
|
||||
@@ -96,12 +95,12 @@ class ViewerToolBar(QToolBar):
|
||||
# the popup menu work in the toolbar (if resized below minimum height)
|
||||
create_actions(ACTIONS, self)
|
||||
|
||||
def createButtons(self):
|
||||
def createButtons(self) -> None:
|
||||
self.buttonImgSwap = QToolButton(self)
|
||||
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.buttonImgSwap.setIcon(
|
||||
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload))
|
||||
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
|
||||
if ISLINUX and not cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
|
||||
else QIcon(QPixmap(":/" + "exchange"))
|
||||
)
|
||||
self.buttonImgSwap.setText("Swap images")
|
||||
@@ -110,22 +109,22 @@ class ViewerToolBar(QToolBar):
|
||||
self.buttonImgSwap.released.connect(self.controller.swapImages)
|
||||
|
||||
self.buttonZoomIn = QToolButton(self)
|
||||
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.buttonZoomIn.setDefaultAction(self.actionZoomIn)
|
||||
self.buttonZoomIn.setEnabled(False)
|
||||
|
||||
self.buttonZoomOut = QToolButton(self)
|
||||
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.buttonZoomOut.setDefaultAction(self.actionZoomOut)
|
||||
self.buttonZoomOut.setEnabled(False)
|
||||
|
||||
self.buttonNormalSize = QToolButton(self)
|
||||
self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.buttonNormalSize.setDefaultAction(self.actionNormalSize)
|
||||
self.buttonNormalSize.setEnabled(True)
|
||||
|
||||
self.buttonBestFit = QToolButton(self)
|
||||
self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
||||
self.buttonBestFit.setDefaultAction(self.actionBestFit)
|
||||
self.buttonBestFit.setEnabled(False)
|
||||
|
||||
@@ -141,10 +140,10 @@ class BaseController(QObject):
|
||||
Base proxy interface to keep image viewers synchronized.
|
||||
Relays function calls, keep tracks of things."""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent: QObject) -> None:
|
||||
super().__init__()
|
||||
self.selectedViewer = None
|
||||
self.referenceViewer = None
|
||||
self.selectedViewer: Union[ScrollAreaImageViewer, None] = None
|
||||
self.referenceViewer: Union[ScrollAreaImageViewer, None] = None
|
||||
# cached pixmaps
|
||||
self.selectedPixmap = QPixmap()
|
||||
self.referencePixmap = QPixmap()
|
||||
@@ -152,22 +151,24 @@ class BaseController(QObject):
|
||||
self.scaledReferencePixmap = QPixmap()
|
||||
self.current_scale = 1.0
|
||||
self.bestFit = True
|
||||
self.parent = parent # To change buttons' states
|
||||
self.setParent(parent) # To change buttons' states
|
||||
self.cached_group = None
|
||||
self.same_dimensions = True
|
||||
|
||||
def setupViewers(self, selected_viewer, reference_viewer):
|
||||
def setupViewers(self, selected_viewer: "ScrollAreaImageViewer", reference_viewer: "ScrollAreaImageViewer") -> None:
|
||||
self.selectedViewer = selected_viewer
|
||||
self.referenceViewer = reference_viewer
|
||||
self.selectedViewer.controller = self
|
||||
self.referenceViewer.controller = self
|
||||
self._setupConnections()
|
||||
|
||||
def _setupConnections(self):
|
||||
self.selectedViewer.connectMouseSignals()
|
||||
self.referenceViewer.connectMouseSignals()
|
||||
def _setupConnections(self) -> None:
|
||||
if self.selectedViewer is not None:
|
||||
self.selectedViewer.connectMouseSignals()
|
||||
if self.referenceViewer is not None:
|
||||
self.referenceViewer.connectMouseSignals()
|
||||
|
||||
def updateView(self, ref, dupe, group):
|
||||
def updateView(self, ref, dupe, group) -> None:
|
||||
# To keep current scale accross dupes from the same group
|
||||
previous_same_dimensions = self.same_dimensions
|
||||
self.same_dimensions = True
|
||||
@@ -206,13 +207,15 @@ class BaseController(QObject):
|
||||
if ignore_update:
|
||||
self.selectedViewer.ignore_signal = False
|
||||
|
||||
def _updateImage(self, pixmap, viewer, same_group=False):
|
||||
def _updateImage(
|
||||
self, pixmap: QPixmap, viewer: "ScrollAreaImageViewer", same_group: bool = False
|
||||
) -> Union[QSize, None]:
|
||||
# WARNING this is called on every resize event, might need to split
|
||||
# into a separate function depending on the implementation used
|
||||
if pixmap.isNull():
|
||||
# This should disable the blank widget
|
||||
viewer.setImage(pixmap)
|
||||
return
|
||||
return None
|
||||
target_size = viewer.size()
|
||||
if not viewer.bestFit:
|
||||
if same_group:
|
||||
@@ -220,14 +223,18 @@ class BaseController(QObject):
|
||||
return target_size
|
||||
# zoomed in state, expand
|
||||
# only if not same_group, we need full update
|
||||
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
|
||||
scaledpixmap = pixmap.scaled(
|
||||
target_size, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.FastTransformation
|
||||
)
|
||||
else:
|
||||
# best fit, keep ratio always
|
||||
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
|
||||
scaledpixmap = pixmap.scaled(
|
||||
target_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation
|
||||
)
|
||||
viewer.setImage(scaledpixmap)
|
||||
return target_size
|
||||
|
||||
def resetState(self):
|
||||
def resetState(self) -> None:
|
||||
"""Only called when the group of dupes has changed. We reset our
|
||||
controller internal state and buttons, center view on viewers."""
|
||||
self.selectedPixmap = QPixmap()
|
||||
@@ -248,7 +255,7 @@ class BaseController(QObject):
|
||||
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
|
||||
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
|
||||
|
||||
def resetViewersState(self):
|
||||
def resetViewersState(self) -> None:
|
||||
"""No item from the model, disable and clear everything."""
|
||||
# only called by the details dialog
|
||||
self.selectedPixmap = QPixmap()
|
||||
@@ -277,36 +284,40 @@ class BaseController(QObject):
|
||||
self.referenceViewer.setEnabled(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def zoomIn(self):
|
||||
def zoomIn(self) -> None:
|
||||
self.scaleImagesBy(1.25)
|
||||
|
||||
@pyqtSlot()
|
||||
def zoomOut(self):
|
||||
def zoomOut(self) -> None:
|
||||
self.scaleImagesBy(0.8)
|
||||
|
||||
@pyqtSlot(float)
|
||||
def scaleImagesBy(self, factor):
|
||||
def scaleImagesBy(self, factor: float) -> None:
|
||||
"""Compute new scale from factor and scale."""
|
||||
self.current_scale *= factor
|
||||
self.selectedViewer.scaleBy(factor)
|
||||
self.referenceViewer.scaleBy(factor)
|
||||
if self.selectedViewer is not None:
|
||||
self.selectedViewer.scaleBy(factor)
|
||||
if self.referenceViewer is not None:
|
||||
self.referenceViewer.scaleBy(factor)
|
||||
self.updateButtons()
|
||||
|
||||
@pyqtSlot(float)
|
||||
def scaleImagesAt(self, scale):
|
||||
def scaleImagesAt(self, scale: float) -> None:
|
||||
"""Scale at a pre-computed scale."""
|
||||
self.current_scale = scale
|
||||
self.selectedViewer.scaleAt(scale)
|
||||
self.referenceViewer.scaleAt(scale)
|
||||
if self.selectedViewer is not None:
|
||||
self.selectedViewer.scaleAt(scale)
|
||||
if self.referenceViewer is not None:
|
||||
self.referenceViewer.scaleAt(scale)
|
||||
self.updateButtons()
|
||||
|
||||
def updateButtons(self):
|
||||
def updateButtons(self) -> None:
|
||||
self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE)
|
||||
self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE)
|
||||
self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0)
|
||||
self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)
|
||||
|
||||
def updateButtonsAsPerDimensions(self, previous_same_dimensions):
|
||||
def updateButtonsAsPerDimensions(self, previous_same_dimensions: bool) -> None:
|
||||
if not self.same_dimensions:
|
||||
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
|
||||
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
|
||||
@@ -323,7 +334,7 @@ class BaseController(QObject):
|
||||
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
|
||||
|
||||
@pyqtSlot()
|
||||
def zoomBestFit(self):
|
||||
def zoomBestFit(self) -> None:
|
||||
"""Setup before scaling to bestfit"""
|
||||
self.setBestFit(True)
|
||||
self.current_scale = 1.0
|
||||
@@ -352,7 +363,7 @@ class BaseController(QObject):
|
||||
self.referenceViewer.bestFit = value
|
||||
|
||||
@pyqtSlot()
|
||||
def zoomNormalSize(self):
|
||||
def zoomNormalSize(self) -> None:
|
||||
self.setBestFit(False)
|
||||
self.current_scale = 1.0
|
||||
|
||||
@@ -373,14 +384,14 @@ class BaseController(QObject):
|
||||
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
|
||||
self.parent.verticalToolBar.buttonBestFit.setEnabled(True)
|
||||
|
||||
def centerViews(self, only_selected=False):
|
||||
def centerViews(self, only_selected: bool = False) -> None:
|
||||
self.selectedViewer.centerViewAndUpdate()
|
||||
if only_selected:
|
||||
return
|
||||
self.referenceViewer.centerViewAndUpdate()
|
||||
|
||||
@pyqtSlot()
|
||||
def swapImages(self):
|
||||
def swapImages(self) -> None:
|
||||
# swap the columns in the details table as well
|
||||
self.parent.tableView.horizontalHeader().swapSections(0, 1)
|
||||
|
||||
@@ -388,17 +399,17 @@ class BaseController(QObject):
|
||||
class QWidgetController(BaseController):
|
||||
"""Specialized version for QWidget-based viewers."""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent: QObject) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
def _updateImage(self, *args):
|
||||
def _updateImage(self, *args) -> Union[QSize, None]:
|
||||
ret = super()._updateImage(*args)
|
||||
# Fix alignment when resizing window
|
||||
self.centerViews()
|
||||
return ret
|
||||
|
||||
@pyqtSlot(QPointF)
|
||||
def onDraggedMouse(self, delta):
|
||||
def onDraggedMouse(self, delta) -> None:
|
||||
if not self.same_dimensions:
|
||||
return
|
||||
if self.sender() is self.referenceViewer:
|
||||
@@ -407,7 +418,7 @@ class QWidgetController(BaseController):
|
||||
self.referenceViewer.onDraggedMouse(delta)
|
||||
|
||||
@pyqtSlot()
|
||||
def swapImages(self):
|
||||
def swapImages(self) -> None:
|
||||
self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap)
|
||||
self.selectedViewer.centerViewAndUpdate()
|
||||
self.referenceViewer.centerViewAndUpdate()
|
||||
@@ -417,15 +428,15 @@ class QWidgetController(BaseController):
|
||||
class ScrollAreaController(BaseController):
|
||||
"""Specialized version fro QLabel-based viewers."""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent: QObject) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
def _setupConnections(self):
|
||||
def _setupConnections(self) -> None:
|
||||
super()._setupConnections()
|
||||
self.selectedViewer.connectScrollBars()
|
||||
self.referenceViewer.connectScrollBars()
|
||||
|
||||
def updateBothImages(self, same_group=False):
|
||||
def updateBothImages(self, same_group: bool = False) -> None:
|
||||
super().updateBothImages(same_group)
|
||||
if not self.referenceViewer.isEnabled():
|
||||
return
|
||||
@@ -433,7 +444,7 @@ class ScrollAreaController(BaseController):
|
||||
self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())
|
||||
|
||||
@pyqtSlot(QPoint)
|
||||
def onDraggedMouse(self, delta):
|
||||
def onDraggedMouse(self, delta) -> None:
|
||||
self.selectedViewer.ignore_signal = True
|
||||
self.referenceViewer.ignore_signal = True
|
||||
|
||||
@@ -450,21 +461,21 @@ class ScrollAreaController(BaseController):
|
||||
self.referenceViewer.ignore_signal = False
|
||||
|
||||
@pyqtSlot()
|
||||
def swapImages(self):
|
||||
def swapImages(self) -> None:
|
||||
self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)
|
||||
self.referenceViewer.setCachedPixmap()
|
||||
self.selectedViewer.setCachedPixmap()
|
||||
super().swapImages()
|
||||
|
||||
@pyqtSlot(float, QPointF)
|
||||
def onMouseWheel(self, scale, delta):
|
||||
def onMouseWheel(self, scale: float, delta: QPointF) -> None:
|
||||
self.scaleImagesAt(scale)
|
||||
self.selectedViewer.adjustScrollBarsScaled(delta)
|
||||
# Signal from scrollbars will automatically change the other:
|
||||
# self.referenceViewer.adjustScrollBarsScaled(delta)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def onVScrollBarChanged(self, value):
|
||||
def onVScrollBarChanged(self, value: int) -> None:
|
||||
if not self.same_dimensions:
|
||||
return
|
||||
if self.sender() is self.referenceViewer._verticalScrollBar:
|
||||
@@ -475,7 +486,7 @@ class ScrollAreaController(BaseController):
|
||||
self.referenceViewer._verticalScrollBar.setValue(value)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def onHScrollBarChanged(self, value):
|
||||
def onHScrollBarChanged(self, value: int) -> None:
|
||||
if not self.same_dimensions:
|
||||
return
|
||||
if self.sender() is self.referenceViewer._horizontalScrollBar:
|
||||
@@ -486,13 +497,13 @@ class ScrollAreaController(BaseController):
|
||||
self.referenceViewer._horizontalScrollBar.setValue(value)
|
||||
|
||||
@pyqtSlot(float)
|
||||
def scaleImagesBy(self, factor):
|
||||
def scaleImagesBy(self, factor: float) -> None:
|
||||
super().scaleImagesBy(factor)
|
||||
# The other is automatically updated via sigals
|
||||
self.selectedViewer.adjustScrollBarsFactor(factor)
|
||||
|
||||
@pyqtSlot()
|
||||
def zoomBestFit(self):
|
||||
def zoomBestFit(self) -> None:
|
||||
# Disable scrollbars to avoid GridLayout size rounding glitch
|
||||
super().zoomBestFit()
|
||||
if self.referencePixmap.isNull():
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from typing import Callable, Union, cast
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog,
|
||||
@@ -28,11 +29,12 @@ from PyQt5.QtWidgets import (
|
||||
QGroupBox,
|
||||
QFormLayout,
|
||||
)
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QShowEvent
|
||||
from hscommon import desktop, plat
|
||||
|
||||
from hscommon.trans import trget
|
||||
from hscommon.plat import ISLINUX
|
||||
from qt import app
|
||||
from qt.util import horizontal_wrap, move_to_screen_center
|
||||
from qt.preferences import get_langnames
|
||||
from enum import Flag, auto
|
||||
@@ -52,8 +54,10 @@ class Sections(Flag):
|
||||
|
||||
|
||||
class PreferencesDialogBase(QDialog):
|
||||
def __init__(self, parent, app, **kwargs):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
def __init__(self, parent: QWidget, app: "app.DupeGuru", **kwargs) -> None:
|
||||
flags = Qt.WindowType(
|
||||
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
|
||||
)
|
||||
super().__init__(parent, flags, **kwargs)
|
||||
self.app = app
|
||||
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
|
||||
@@ -65,7 +69,7 @@ class PreferencesDialogBase(QDialog):
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
|
||||
def _setupFilterHardnessBox(self):
|
||||
def _setupFilterHardnessBox(self) -> None:
|
||||
self.filterHardnessHLayout = QHBoxLayout()
|
||||
self.filterHardnessLabel = QLabel(self)
|
||||
self.filterHardnessLabel.setText(tr("Filter Hardness:"))
|
||||
@@ -84,7 +88,7 @@ class PreferencesDialogBase(QDialog):
|
||||
self.filterHardnessSlider.setMinimum(1)
|
||||
self.filterHardnessSlider.setMaximum(100)
|
||||
self.filterHardnessSlider.setTracking(True)
|
||||
self.filterHardnessSlider.setOrientation(Qt.Horizontal)
|
||||
self.filterHardnessSlider.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)
|
||||
self.filterHardnessLabel = QLabel(self)
|
||||
self.filterHardnessLabel.setText("100")
|
||||
@@ -104,7 +108,7 @@ class PreferencesDialogBase(QDialog):
|
||||
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
||||
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
||||
|
||||
def _setupBottomPart(self):
|
||||
def _setupBottomPart(self) -> None:
|
||||
# The bottom part of the pref panel is always the same in all editions.
|
||||
self.copyMoveLabel = QLabel(self)
|
||||
self.copyMoveLabel.setText(tr("Copy and Move:"))
|
||||
@@ -120,7 +124,7 @@ class PreferencesDialogBase(QDialog):
|
||||
self.customCommandEdit = QLineEdit(self)
|
||||
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
||||
|
||||
def _setupDisplayPage(self):
|
||||
def _setupDisplayPage(self) -> None:
|
||||
self.ui_groupbox = QGroupBox("&" + tr("General Interface"))
|
||||
layout = QVBoxLayout()
|
||||
self.languageLabel = QLabel(tr("Language:"), self)
|
||||
@@ -171,7 +175,7 @@ On MacOS, the tab bar will fill up the window's width instead."
|
||||
formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color)
|
||||
self.result_table_delta_foreground_color = ColorPickerButton(self)
|
||||
formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color)
|
||||
formlayout.setLabelAlignment(Qt.AlignLeft)
|
||||
formlayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Keep same vertical spacing as parent layout for consistency
|
||||
formlayout.setVerticalSpacing(self.displayVLayout.spacing())
|
||||
@@ -213,7 +217,7 @@ use the modifier key to drag the floating window around"
|
||||
details_groupbox.setLayout(self.details_groupbox_layout)
|
||||
self.displayVLayout.addWidget(details_groupbox)
|
||||
|
||||
def _setupDebugPage(self):
|
||||
def _setupDebugPage(self) -> None:
|
||||
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
|
||||
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
|
||||
self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization."))
|
||||
@@ -221,22 +225,22 @@ use the modifier key to drag the floating window around"
|
||||
self.debugVLayout.addWidget(self.profile_scan_box)
|
||||
self.debug_location_label = QLabel(
|
||||
tr('Logs located in: <a href="{}">{}</a>').format(self.app.model.appdata, self.app.model.appdata),
|
||||
wordWrap=True,
|
||||
)
|
||||
self.debug_location_label.setWordWrap(True)
|
||||
self.debugVLayout.addWidget(self.debug_location_label)
|
||||
|
||||
def _setupAddCheckbox(self, name, label, parent=None):
|
||||
def _setupAddCheckbox(self, name: str, label: str, parent: Union[QWidget, None] = None) -> None:
|
||||
if parent is None:
|
||||
parent = self
|
||||
cb = QCheckBox(parent)
|
||||
cb.setText(label)
|
||||
setattr(self, name, cb)
|
||||
|
||||
def _setupPreferenceWidgets(self):
|
||||
def _setupPreferenceWidgets(self) -> None:
|
||||
# Edition-specific
|
||||
pass
|
||||
|
||||
def _setupUi(self):
|
||||
def _setupUi(self) -> None:
|
||||
self.setWindowTitle(tr("Options"))
|
||||
self.setSizeGripEnabled(False)
|
||||
self.setModal(True)
|
||||
@@ -262,7 +266,7 @@ use the modifier key to drag the floating window around"
|
||||
)
|
||||
self.mainVLayout.addWidget(self.tabwidget)
|
||||
self.mainVLayout.addWidget(self.buttonBox)
|
||||
self.layout().setSizeConstraint(QLayout.SetFixedSize)
|
||||
self.layout().setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
self.tabwidget.addTab(self.page_general, tr("General"))
|
||||
self.tabwidget.addTab(self.page_display, tr("Display"))
|
||||
self.tabwidget.addTab(self.page_debug, tr("Debug"))
|
||||
@@ -270,20 +274,20 @@ use the modifier key to drag the floating window around"
|
||||
self.widgetsVLayout.addStretch(0)
|
||||
self.debugVLayout.addStretch(0)
|
||||
|
||||
def _load(self, prefs, setchecked, section):
|
||||
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
|
||||
# Edition-specific
|
||||
pass
|
||||
|
||||
def _save(self, prefs, ischecked):
|
||||
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
|
||||
# Edition-specific
|
||||
pass
|
||||
|
||||
def load(self, prefs=None, section=Sections.ALL):
|
||||
def load(self, prefs: Union[Preferences, None] = None, section: Sections = Sections.ALL) -> None:
|
||||
if prefs is None:
|
||||
prefs = self.app.prefs
|
||||
|
||||
def setchecked(cb, b):
|
||||
cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
|
||||
def setchecked(cb: QCheckBox, b: bool) -> None:
|
||||
cb.setCheckState(Qt.CheckState.Checked if b else Qt.CheckState.Unchecked)
|
||||
|
||||
if section & Sections.GENERAL:
|
||||
self.filterHardnessSlider.setValue(prefs.filter_hardness)
|
||||
@@ -323,12 +327,12 @@ use the modifier key to drag the floating window around"
|
||||
setchecked(self.profile_scan_box, prefs.profile_scan)
|
||||
self._load(prefs, setchecked, section)
|
||||
|
||||
def save(self):
|
||||
def save(self) -> None:
|
||||
prefs = self.app.prefs
|
||||
prefs.filter_hardness = self.filterHardnessSlider.value()
|
||||
|
||||
def ischecked(cb):
|
||||
return cb.checkState() == Qt.Checked
|
||||
def ischecked(cb: QCheckBox) -> bool:
|
||||
return cb.checkState() == Qt.CheckState.Checked
|
||||
|
||||
prefs.mix_file_kind = ischecked(self.mixFileKindBox)
|
||||
prefs.use_regexp = ischecked(self.useRegexpBox)
|
||||
@@ -363,11 +367,11 @@ use the modifier key to drag the floating window around"
|
||||
self.app.prefs.language = lang_code
|
||||
self._save(prefs, ischecked)
|
||||
|
||||
def resetToDefaults(self, section_to_update):
|
||||
def resetToDefaults(self, section_to_update: Sections) -> None:
|
||||
self.load(Preferences(), section_to_update)
|
||||
|
||||
# --- Events
|
||||
def buttonClicked(self, button):
|
||||
def buttonClicked(self, button: QDialogButtonBox) -> None:
|
||||
role = self.buttonBox.buttonRole(button)
|
||||
if role == QDialogButtonBox.ResetRole:
|
||||
current_tab = self.tabwidget.currentWidget()
|
||||
@@ -380,30 +384,32 @@ use the modifier key to drag the floating window around"
|
||||
section_to_update = Sections.DEBUG
|
||||
self.resetToDefaults(section_to_update)
|
||||
|
||||
def showEvent(self, event):
|
||||
def showEvent(self, event: QShowEvent) -> None:
|
||||
# have to do this here as the frameGeometry is not correct until shown
|
||||
move_to_screen_center(self)
|
||||
super().showEvent(event)
|
||||
|
||||
|
||||
class ColorPickerButton(QPushButton):
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.setParent(parent)
|
||||
self.color = None
|
||||
self.clicked.connect(self.onClicked)
|
||||
|
||||
@pyqtSlot()
|
||||
def onClicked(self):
|
||||
color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent)
|
||||
def onClicked(self) -> None:
|
||||
color = QColorDialog.getColor(
|
||||
self.color if self.color is not None else Qt.GlobalColor.white, cast(QWidget, self.parent())
|
||||
)
|
||||
self.setColor(color)
|
||||
|
||||
def setColor(self, color):
|
||||
def setColor(self, color) -> None:
|
||||
size = QSize(16, 16)
|
||||
px = QPixmap(size)
|
||||
if color is None:
|
||||
size.width = 0
|
||||
size.height = 0
|
||||
size.setWidth(0)
|
||||
size.setHeight(0)
|
||||
elif not color.isValid():
|
||||
return
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user