mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-03-12 03:31:37 +00:00
Compare commits
13 Commits
7865e4aeac
...
feature/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
2f23f34b91
|
|||
|
ec35c2df8f
|
|||
|
4d56bd3515
|
|||
|
66e69a3854
|
|||
|
1f1dfa88dc
|
|||
|
916c5204cf
|
|||
|
71af825b37
|
|||
|
97f490b8b7
|
|||
|
d369bcddd7
|
|||
|
360dceca7b
|
|||
|
92b27801c3
|
|||
|
|
b9aabb8545 | ||
|
d5eeab4a17
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
name: Build Cpp
|
name: Build Cpp
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install python3-pyqt5
|
sudo apt-get install python3-pyqt6
|
||||||
make modules
|
make modules
|
||||||
- if: matrix.language == 'python'
|
- if: matrix.language == 'python'
|
||||||
name: Autobuild
|
name: Autobuild
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -60,8 +60,8 @@ ifndef NO_VENV
|
|||||||
@${PYTHON} -m venv -h > /dev/null || \
|
@${PYTHON} -m venv -h > /dev/null || \
|
||||||
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
|
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
|
||||||
endif
|
endif
|
||||||
@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \
|
@${PYTHON} -c 'import PyQt6' >/dev/null 2>&1 || \
|
||||||
{ echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
|
{ echo "PyQt 6.3+ required. Install it and try again. Aborting"; exit 1; }
|
||||||
|
|
||||||
env: | reqs
|
env: | reqs
|
||||||
ifndef NO_VENV
|
ifndef NO_VENV
|
||||||
|
|||||||
@@ -32,18 +32,15 @@ For macos instructions (qt version) see the [macOS Instructions](macos.md).
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
* [Python 3.7+][python]
|
* [Python 3.7+][python]
|
||||||
* PyQt5
|
* PyQt6
|
||||||
|
|
||||||
### System Setup
|
### System Setup
|
||||||
When running in a linux based environment the following system packages or equivalents are needed to build:
|
When running in a linux based environment the following system packages or equivalents are needed to build:
|
||||||
* python3-pyqt5
|
* python3-pyqt6
|
||||||
* pyqt5-dev-tools (on some systems, see note)
|
|
||||||
* python3-venv (only if using a virtual environment)
|
* python3-venv (only if using a virtual environment)
|
||||||
* python3-dev
|
* python3-dev
|
||||||
* build-essential
|
* build-essential
|
||||||
|
|
||||||
Note: On some linux systems pyrcc5 is not put on the path when installing python3-pyqt5, this will cause some issues with the resource files (and icons). These systems should have a respective pyqt5-dev-tools package, which should also be installed. The presence of pyrcc5 can be checked with `which pyrcc5`. Debian based systems need the extra package, and Arch does not.
|
|
||||||
|
|
||||||
To create packages the following are also needed:
|
To create packages the following are also needed:
|
||||||
* python3-setuptools
|
* python3-setuptools
|
||||||
* debhelper
|
* debhelper
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ To build with a different python version 3.7 vs 3.8 or 32 bit vs 64 bit specify
|
|||||||
### With makefile
|
### With makefile
|
||||||
It is possible to build dupeGuru with the makefile on windows using a compatable POSIX environment. The following steps have been tested using [msys2][msys2]. Before running make:
|
It is possible to build dupeGuru with the makefile on windows using a compatable POSIX environment. The following steps have been tested using [msys2][msys2]. Before running make:
|
||||||
1. Install msys2 or other POSIX environment
|
1. Install msys2 or other POSIX environment
|
||||||
2. Install PyQt5 globally via pip
|
2. Install PyQt6 globally via pip
|
||||||
3. Use the respective console for msys2 it is `msys2 msys`
|
3. Use the respective console for msys2 it is `msys2 msys`
|
||||||
|
|
||||||
Then the following execution of the makefile should work. Pass the correct value for PYTHON to the makefile if not on the path as python3.
|
Then the following execution of the makefile should work. Pass the correct value for PYTHON to the makefile if not on the path as python3.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "4.2.1"
|
__version__ = "4.3.1"
|
||||||
__appname__ = "dupeGuru"
|
__appname__ = "dupeGuru"
|
||||||
|
|||||||
10
core/app.py
10
core/app.py
@@ -555,9 +555,15 @@ class DupeGuru(Broadcaster):
|
|||||||
# a workaround to make the damn thing work.
|
# a workaround to make the damn thing work.
|
||||||
exepath, args = match.groups()
|
exepath, args = match.groups()
|
||||||
path, exename = op.split(exepath)
|
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:
|
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):
|
def load(self):
|
||||||
"""Load directory selection and ignore list from files in appdata.
|
"""Load directory selection and ignore list from files in appdata.
|
||||||
|
|||||||
@@ -303,12 +303,13 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
|||||||
# skip hashing for zero length files
|
# skip hashing for zero length files
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
continue
|
continue
|
||||||
if first.digest_partial == second.digest_partial:
|
# if digests are the same (and not None) then files match
|
||||||
|
if first.digest_partial == second.digest_partial and first.digest_partial is not None:
|
||||||
if bigsize > 0 and first.size > bigsize:
|
if bigsize > 0 and first.size > bigsize:
|
||||||
if first.digest_samples == second.digest_samples:
|
if first.digest_samples == second.digest_samples and first.digest_samples is not None:
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
else:
|
else:
|
||||||
if first.digest == second.digest:
|
if first.digest == second.digest and first.digest is not None:
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
group_count += 1
|
group_count += 1
|
||||||
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
||||||
|
|||||||
63
core/fs.py
63
core/fs.py
@@ -144,13 +144,17 @@ class FilesDB:
|
|||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
size = stat.st_size
|
size = stat.st_size
|
||||||
mtime_ns = stat.st_mtime_ns
|
mtime_ns = stat.st_mtime_ns
|
||||||
|
try:
|
||||||
|
with self.lock:
|
||||||
|
self.cur.execute(
|
||||||
|
self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns}
|
||||||
|
)
|
||||||
|
result = self.cur.fetchone()
|
||||||
|
|
||||||
with self.lock:
|
if result:
|
||||||
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
|
return result[0]
|
||||||
result = self.cur.fetchone()
|
except Exception as ex:
|
||||||
|
logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}")
|
||||||
if result:
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -158,12 +162,14 @@ class FilesDB:
|
|||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
size = stat.st_size
|
size = stat.st_size
|
||||||
mtime_ns = stat.st_mtime_ns
|
mtime_ns = stat.st_mtime_ns
|
||||||
|
try:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.cur.execute(
|
self.cur.execute(
|
||||||
self.insert_query.format(key=key),
|
self.insert_query.format(key=key),
|
||||||
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
||||||
)
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logging.warning(f"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}")
|
||||||
|
|
||||||
def commit(self) -> None:
|
def commit(self) -> None:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -265,34 +271,25 @@ class File:
|
|||||||
self.size = nonone(stats.st_size, 0)
|
self.size = nonone(stats.st_size, 0)
|
||||||
self.mtime = nonone(stats.st_mtime, 0)
|
self.mtime = nonone(stats.st_mtime, 0)
|
||||||
elif field == "digest_partial":
|
elif field == "digest_partial":
|
||||||
try:
|
self.digest_partial = filesdb.get(self.path, "digest_partial")
|
||||||
self.digest_partial = filesdb.get(self.path, "digest_partial")
|
if self.digest_partial is None:
|
||||||
if self.digest_partial is None:
|
self.digest_partial = self._calc_digest_partial()
|
||||||
self.digest_partial = self._calc_digest_partial()
|
filesdb.put(self.path, "digest_partial", self.digest_partial)
|
||||||
filesdb.put(self.path, "digest_partial", self.digest_partial)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning("Couldn't get digest_partial for %s: %s", self.path, e)
|
|
||||||
elif field == "digest":
|
elif field == "digest":
|
||||||
try:
|
self.digest = filesdb.get(self.path, "digest")
|
||||||
self.digest = filesdb.get(self.path, "digest")
|
if self.digest is None:
|
||||||
if self.digest is None:
|
self.digest = self._calc_digest()
|
||||||
self.digest = self._calc_digest()
|
filesdb.put(self.path, "digest", self.digest)
|
||||||
filesdb.put(self.path, "digest", self.digest)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning("Couldn't get digest for %s: %s", self.path, e)
|
|
||||||
elif field == "digest_samples":
|
elif field == "digest_samples":
|
||||||
size = self.size
|
size = self.size
|
||||||
# Might as well hash such small files entirely.
|
# Might as well hash such small files entirely.
|
||||||
if size <= MIN_FILE_SIZE:
|
if size <= MIN_FILE_SIZE:
|
||||||
setattr(self, field, self.digest)
|
setattr(self, field, self.digest)
|
||||||
return
|
return
|
||||||
try:
|
self.digest_samples = filesdb.get(self.path, "digest_samples")
|
||||||
self.digest_samples = filesdb.get(self.path, "digest_samples")
|
if self.digest_samples is None:
|
||||||
if self.digest_samples is None:
|
self.digest_samples = self._calc_digest_samples()
|
||||||
self.digest_samples = self._calc_digest_samples()
|
filesdb.put(self.path, "digest_samples", self.digest_samples)
|
||||||
filesdb.put(self.path, "digest_samples", self.digest_samples)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Couldn't get digest_samples for {self.path}: {e}")
|
|
||||||
|
|
||||||
def _read_all_info(self, attrnames=None):
|
def _read_all_info(self, attrnames=None):
|
||||||
"""Cache all possible info.
|
"""Cache all possible info.
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2014-03-15
|
|
||||||
# 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
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import plistlib
|
|
||||||
|
|
||||||
|
|
||||||
class IPhotoPlistParser(plistlib._PlistParser):
|
|
||||||
"""A parser for iPhoto plists.
|
|
||||||
|
|
||||||
iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more
|
|
||||||
lenient.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
plistlib._PlistParser.__init__(self, use_builtin_types=True, dict_type=dict)
|
|
||||||
# For debugging purposes, we remember the last bit of data to be analyzed so that we can
|
|
||||||
# log it in case of an exception
|
|
||||||
self.lastdata = ""
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
self.lastdata = plistlib._PlistParser.get_data(self)
|
|
||||||
return self.lastdata
|
|
||||||
|
|
||||||
def end_integer(self):
|
|
||||||
try:
|
|
||||||
self.add_object(int(self.get_data()))
|
|
||||||
except ValueError:
|
|
||||||
self.add_object(0)
|
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
=== 4.3.1 (2022-07-08)
|
||||||
|
* Fix issue where cache db exceptions could prevent files being hashed (#1015)
|
||||||
|
* Add extra guard for non-zero length files without digests to prevent false duplicates
|
||||||
|
* Update Italian translations
|
||||||
|
|
||||||
|
=== 4.3.0 (2022-07-01)
|
||||||
|
* Redirect stdout from custom command to the log files (#1008)
|
||||||
|
* Update translations
|
||||||
|
* Fix typo in debian control file (#989)
|
||||||
|
* Add option to profile scans
|
||||||
|
* Update fs.py to optimize stat() calls
|
||||||
|
* Fix Error when delete after scan (#988)
|
||||||
|
* Update directory scanning to use os.scandir() and DirEntry objects
|
||||||
|
* Improve performance of Directories.get_state()
|
||||||
|
* Migrate from hscommon.path to pathlib
|
||||||
|
* Switch file hashing to xxhash with fallback to md5
|
||||||
|
* Add update check feature to about box
|
||||||
|
|
||||||
=== 4.2.1 (2022-03-25)
|
=== 4.2.1 (2022-03-25)
|
||||||
* Default to English on unsupported system language (#976)
|
* Default to English on unsupported system language (#976)
|
||||||
* Fix image viewer zoom datatype issue (#978)
|
* Fix image viewer zoom datatype issue (#978)
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ def special_folder_path(special_folder: SpecialFolder, portable: bool = False) -
|
|||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtCore import QUrl, QStandardPaths
|
from PyQt6.QtCore import QUrl, QStandardPaths
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt6.QtGui import QDesktopServices
|
||||||
from qt.util import get_appdata
|
from qt.util import get_appdata
|
||||||
from core.util import executable_folder
|
from core.util import executable_folder
|
||||||
from hscommon.plat import ISWINDOWS, ISOSX
|
from hscommon.plat import ISWINDOWS, ISOSX
|
||||||
@@ -71,7 +71,7 @@ try:
|
|||||||
if ISWINDOWS and portable:
|
if ISWINDOWS and portable:
|
||||||
folder = op.join(executable_folder(), "cache")
|
folder = op.join(executable_folder(), "cache")
|
||||||
else:
|
else:
|
||||||
folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
|
folder = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.CacheLocation)[0]
|
||||||
else:
|
else:
|
||||||
folder = get_appdata(portable)
|
folder = get_appdata(portable)
|
||||||
return folder
|
return folder
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ class GUIObject:
|
|||||||
``multibind`` flag to ``True`` and the safeguard will be disabled.
|
``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._view = None
|
||||||
self._multibind = multibind
|
self._multibind = multibind
|
||||||
|
|
||||||
def _view_updated(self):
|
def _view_updated(self) -> None:
|
||||||
"""(Virtual) Called after :attr:`view` has been set.
|
"""(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
|
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).
|
(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))
|
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -67,7 +67,7 @@ class GUIObject:
|
|||||||
return self._view
|
return self._view
|
||||||
|
|
||||||
@view.setter
|
@view.setter
|
||||||
def view(self, value):
|
def view(self, value) -> None:
|
||||||
if self._view is None and value is None:
|
if self._view is None and value is None:
|
||||||
# Initial view assignment
|
# Initial view assignment
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
|
from hscommon.gui.table import GUITable
|
||||||
|
|
||||||
|
|
||||||
class Column:
|
class Column:
|
||||||
@@ -17,7 +19,7 @@ class Column:
|
|||||||
These attributes are then used to correctly configure the column on the "view" side.
|
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
|
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
|
||||||
#: as :meth:`Columns.column_by_name`.
|
#: as :meth:`Columns.column_by_name`.
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -52,14 +54,14 @@ class ColumnsView:
|
|||||||
callbacks.
|
callbacks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def restore_columns(self):
|
def restore_columns(self) -> None:
|
||||||
"""Update all columns according to the model.
|
"""Update all columns according to the model.
|
||||||
|
|
||||||
When this is called, our view has to update the columns title, order and visibility of all
|
When this is called, our view has to update the columns title, order and visibility of all
|
||||||
columns.
|
columns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_column_visible(self, colname, visible):
|
def set_column_visible(self, colname: str, visible: bool) -> None:
|
||||||
"""Update visibility of column ``colname``.
|
"""Update visibility of column ``colname``.
|
||||||
|
|
||||||
Called when the user toggles the visibility of a column, we must update the column
|
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.*
|
*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.
|
"""Retrieve the value for ``key`` in the currently running app's preference store.
|
||||||
|
|
||||||
If the key doesn't exist, return ``fallback_value``.
|
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."""
|
"""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.
|
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)
|
GUIObject.__init__(self)
|
||||||
self.table = table
|
self.table = table
|
||||||
self.prefaccess = prefaccess
|
self.prefaccess = prefaccess
|
||||||
self.savename = savename
|
self.savename = savename
|
||||||
# We use copy here for test isolation. If we don't, changing a column affects all tests.
|
# 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):
|
for i, column in enumerate(self.column_list):
|
||||||
column.logical_index = i
|
column.logical_index = i
|
||||||
column.ordered_index = i
|
column.ordered_index = i
|
||||||
self.coldata = {col.name: col for col in self.column_list}
|
self.coldata = {col.name: col for col in self.column_list}
|
||||||
|
|
||||||
# --- Private
|
# --- Private
|
||||||
def _get_colname_attr(self, colname, attrname, default):
|
def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:
|
||||||
try:
|
try:
|
||||||
return getattr(self.coldata[colname], attrname)
|
return getattr(self.coldata[colname], attrname)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _set_colname_attr(self, colname, attrname, value):
|
def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:
|
||||||
try:
|
try:
|
||||||
col = self.coldata[colname]
|
col = self.coldata[colname]
|
||||||
setattr(col, attrname, value)
|
setattr(col, attrname, value)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _optional_columns(self):
|
def _optional_columns(self) -> List[Column]:
|
||||||
return [c for c in self.column_list if c.optional]
|
return [c for c in self.column_list if c.optional]
|
||||||
|
|
||||||
# --- Override
|
# --- Override
|
||||||
def _view_updated(self):
|
def _view_updated(self) -> None:
|
||||||
self.restore_columns()
|
self.restore_columns()
|
||||||
|
|
||||||
# --- Public
|
# --- 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 the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
|
||||||
return self.column_list[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 the :class:`Column` having the :attr:`~Column.name` ``name``."""
|
||||||
return self.coldata[name]
|
return self.coldata[name]
|
||||||
|
|
||||||
def columns_count(self):
|
def columns_count(self) -> int:
|
||||||
"""Returns the number of columns in our set."""
|
"""Returns the number of columns in our set."""
|
||||||
return len(self.column_list)
|
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."""
|
"""Returns display name for column named ``colname``, or ``''`` if there's none."""
|
||||||
return self._get_colname_attr(colname, "display", "")
|
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."""
|
"""Returns visibility for column named ``colname``, or ``True`` if there's none."""
|
||||||
return self._get_colname_attr(colname, "visible", True)
|
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."""
|
"""Returns width for column named ``colname``, or ``0`` if there's none."""
|
||||||
return self._get_colname_attr(colname, "width", 0)
|
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``.
|
"""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
|
"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
|
index = column.ordered_index
|
||||||
return [col.name for col in self.column_list if (col.visible and col.ordered_index > 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 items convenient for quick visibility menu generation.
|
||||||
|
|
||||||
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
|
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()]
|
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``.
|
"""Moves column ``colname`` to ``index``.
|
||||||
|
|
||||||
The column will be placed just in front of the column currently having that index, or to the
|
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)
|
colnames.insert(index, colname)
|
||||||
self.set_column_order(colnames)
|
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."""
|
"""Reset all columns' width and visibility to their default values."""
|
||||||
self.set_column_order([col.name for col in self.column_list])
|
self.set_column_order([col.name for col in self.column_list])
|
||||||
for col in self._optional_columns():
|
for col in self._optional_columns():
|
||||||
@@ -203,11 +205,11 @@ class Columns(GUIObject):
|
|||||||
col.width = col.default_width
|
col.width = col.default_width
|
||||||
self.view.restore_columns()
|
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``."""
|
"""Set column ``colname``'s width to ``newwidth``."""
|
||||||
self._set_colname_attr(colname, "width", 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`."""
|
"""Restore's column persistent attributes from the last :meth:`save_columns`."""
|
||||||
if not (self.prefaccess and self.savename and self.coldata):
|
if not (self.prefaccess and self.savename and self.coldata):
|
||||||
if (not self.savename) and (self.coldata):
|
if (not self.savename) and (self.coldata):
|
||||||
@@ -226,7 +228,7 @@ class Columns(GUIObject):
|
|||||||
col.visible = coldata["visible"]
|
col.visible = coldata["visible"]
|
||||||
self.view.restore_columns()
|
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`."""
|
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
|
||||||
if not (self.prefaccess and self.savename and self.coldata):
|
if not (self.prefaccess and self.savename and self.coldata):
|
||||||
return
|
return
|
||||||
@@ -237,7 +239,8 @@ class Columns(GUIObject):
|
|||||||
coldata["visible"] = col.visible
|
coldata["visible"] = col.visible
|
||||||
self.prefaccess.set_default(pref_name, coldata)
|
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``.
|
"""Change the columns order so it matches the order in ``colnames``.
|
||||||
|
|
||||||
:param colnames: A list of column names in the desired order.
|
:param colnames: A list of column names in the desired order.
|
||||||
@@ -247,17 +250,17 @@ class Columns(GUIObject):
|
|||||||
col = self.coldata[colname]
|
col = self.coldata[colname]
|
||||||
col.ordered_index = i
|
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``."""
|
"""Set the visibility of column ``colname``."""
|
||||||
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
|
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._set_colname_attr(colname, "visible", visible)
|
||||||
self.view.set_column_visible(colname, 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``."""
|
"""Set the default width or column ``colname``."""
|
||||||
self._set_colname_attr(colname, "default_width", width)
|
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.
|
"""Toggles the visibility of an optional column.
|
||||||
|
|
||||||
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
|
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
|
||||||
@@ -271,11 +274,11 @@ class Columns(GUIObject):
|
|||||||
|
|
||||||
# --- Properties
|
# --- Properties
|
||||||
@property
|
@property
|
||||||
def ordered_columns(self):
|
def ordered_columns(self) -> List[Column]:
|
||||||
"""List of :class:`Column` in visible order."""
|
"""List of :class:`Column` in visible order."""
|
||||||
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
|
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colnames(self):
|
def colnames(self) -> List[str]:
|
||||||
"""List of column names in visible order."""
|
"""List of column names in visible order."""
|
||||||
return [col.name for col in self.ordered_columns]
|
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
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
from typing import Callable, Tuple, Union
|
||||||
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
from hscommon.gui.text_field import TextField
|
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.
|
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."""
|
"""Show the dialog."""
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""Close the dialog."""
|
"""Close the dialog."""
|
||||||
|
|
||||||
def set_progress(self, progress):
|
def set_progress(self, progress: int) -> None:
|
||||||
"""Set the progress of the progress bar to ``progress``.
|
"""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
|
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.
|
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.
|
# finish_func(jobid) is the function that is called when a job is completed.
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
ThreadedJobPerformer.__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
|
#: :class:`.TextField`. It contains the job textual update that the function might yield
|
||||||
#: during its course.
|
#: during its course.
|
||||||
self.progressdesc_textfield = TextField()
|
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."""
|
"""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
|
# 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
|
# 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:
|
if self._job_running:
|
||||||
self.job_cancelled = True
|
self.job_cancelled = True
|
||||||
|
|
||||||
def pulse(self):
|
def pulse(self) -> None:
|
||||||
"""Update progress reports in the GUI.
|
"""Update progress reports in the GUI.
|
||||||
|
|
||||||
Call this regularly from the GUI main run loop. The values might change before
|
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.progressdesc_textfield.text = last_desc
|
||||||
self.view.set_progress(last_progress)
|
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.
|
"""Starts a threaded job.
|
||||||
|
|
||||||
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
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.abc import MutableSequence
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
from hscommon.gui.selectable_list import Selectable
|
from hscommon.gui.selectable_list import Selectable
|
||||||
@@ -27,12 +28,16 @@ class Table(MutableSequence, Selectable):
|
|||||||
Subclasses :class:`.Selectable`.
|
Subclasses :class:`.Selectable`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
# Should be List[Column], but have circular import...
|
||||||
Selectable.__init__(self)
|
COLUMNS: List = []
|
||||||
self._rows = []
|
|
||||||
self._header = None
|
|
||||||
self._footer = None
|
|
||||||
|
|
||||||
|
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):
|
def __delitem__(self, key):
|
||||||
self._rows.__delitem__(key)
|
self._rows.__delitem__(key)
|
||||||
if self._header is not None and ((not self) or (self[0] is not self._header)):
|
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._footer = None
|
||||||
self._check_selection_range()
|
self._check_selection_range()
|
||||||
|
|
||||||
def __getitem__(self, key):
|
# TODO type hint for key
|
||||||
|
def __getitem__(self, key) -> Any:
|
||||||
return self._rows.__getitem__(key)
|
return self._rows.__getitem__(key)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self._rows)
|
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)
|
self._rows.__setitem__(key, value)
|
||||||
|
|
||||||
def append(self, item):
|
def append(self, item: "Row") -> None:
|
||||||
"""Appends ``item`` at the end of the table.
|
"""Appends ``item`` at the end of the table.
|
||||||
|
|
||||||
If there's a footer, the item is inserted before it.
|
If there's a footer, the item is inserted before it.
|
||||||
@@ -60,7 +67,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
else:
|
else:
|
||||||
self._rows.append(item)
|
self._rows.append(item)
|
||||||
|
|
||||||
def insert(self, index, item):
|
def insert(self, index: int, item: "Row") -> None:
|
||||||
"""Inserts ``item`` at ``index`` in the table.
|
"""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
|
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
|
index = len(self) - 1
|
||||||
self._rows.insert(index, item)
|
self._rows.insert(index, item)
|
||||||
|
|
||||||
def remove(self, row):
|
def remove(self, row: "Row") -> None:
|
||||||
"""Removes ``row`` from table.
|
"""Removes ``row`` from table.
|
||||||
|
|
||||||
If ``row`` is a header or footer, that header or footer will be set to ``None``.
|
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._rows.remove(row)
|
||||||
self._check_selection_range()
|
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 table by ``column_name``.
|
||||||
|
|
||||||
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
||||||
@@ -105,7 +112,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
|
|
||||||
# --- Properties
|
# --- Properties
|
||||||
@property
|
@property
|
||||||
def footer(self):
|
def footer(self) -> Union["Row", None]:
|
||||||
"""If set, a row that always stay at the bottom of the table.
|
"""If set, a row that always stay at the bottom of the table.
|
||||||
|
|
||||||
:class:`Row`. *get/set*.
|
:class:`Row`. *get/set*.
|
||||||
@@ -128,7 +135,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self._footer
|
return self._footer
|
||||||
|
|
||||||
@footer.setter
|
@footer.setter
|
||||||
def footer(self, value):
|
def footer(self, value: Union["Row", None]) -> None:
|
||||||
if self._footer is not None:
|
if self._footer is not None:
|
||||||
self._rows.pop()
|
self._rows.pop()
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -136,7 +143,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._footer = value
|
self._footer = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def header(self):
|
def header(self) -> Union["Row", None]:
|
||||||
"""If set, a row that always stay at the bottom of the table.
|
"""If set, a row that always stay at the bottom of the table.
|
||||||
|
|
||||||
See :attr:`footer` for details.
|
See :attr:`footer` for details.
|
||||||
@@ -144,7 +151,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self._header
|
return self._header
|
||||||
|
|
||||||
@header.setter
|
@header.setter
|
||||||
def header(self, value):
|
def header(self, value: Union["Row", None]) -> None:
|
||||||
if self._header is not None:
|
if self._header is not None:
|
||||||
self._rows.pop(0)
|
self._rows.pop(0)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -152,7 +159,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._header = value
|
self._header = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def row_count(self):
|
def row_count(self) -> int:
|
||||||
"""Number or rows in the table (without counting header and footer).
|
"""Number or rows in the table (without counting header and footer).
|
||||||
|
|
||||||
*int*. *read-only*.
|
*int*. *read-only*.
|
||||||
@@ -165,7 +172,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rows(self):
|
def rows(self) -> List["Row"]:
|
||||||
"""List of rows in the table, excluding header and footer.
|
"""List of rows in the table, excluding header and footer.
|
||||||
|
|
||||||
List of :class:`Row`. *read-only*.
|
List of :class:`Row`. *read-only*.
|
||||||
@@ -179,7 +186,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self[start:end]
|
return self[start:end]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_row(self):
|
def selected_row(self) -> "Row":
|
||||||
"""Selected row according to :attr:`Selectable.selected_index`.
|
"""Selected row according to :attr:`Selectable.selected_index`.
|
||||||
|
|
||||||
:class:`Row`. *get/set*.
|
: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
|
return self[self.selected_index] if self.selected_index is not None else None
|
||||||
|
|
||||||
@selected_row.setter
|
@selected_row.setter
|
||||||
def selected_row(self, value):
|
def selected_row(self, value: int) -> None:
|
||||||
try:
|
try:
|
||||||
self.selected_index = self.index(value)
|
self.selected_index = self.index(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_rows(self):
|
def selected_rows(self) -> List["Row"]:
|
||||||
"""List of selected rows based on :attr:`.selected_indexes`.
|
"""List of selected rows based on :attr:`.selected_indexes`.
|
||||||
|
|
||||||
List of :class:`Row`. *read-only*.
|
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`.
|
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.
|
"""Refreshes the contents of the table widget.
|
||||||
|
|
||||||
Ensures that the contents of the table widget is synced with the model. This includes
|
Ensures that the contents of the table widget is synced with the model. This includes
|
||||||
selection.
|
selection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def start_editing(self):
|
def start_editing(self) -> None:
|
||||||
"""Start editing the currently selected row.
|
"""Start editing the currently selected row.
|
||||||
|
|
||||||
Begin whatever inline editing support that the view supports.
|
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.
|
"""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
|
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`.
|
:class:`GUITableView`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
Table.__init__(self)
|
Table.__init__(self)
|
||||||
#: The row being currently edited by the user. ``None`` if no edit is taking place.
|
#: The row being currently edited by the user. ``None`` if no edit is taking place.
|
||||||
self.edited = None
|
self.edited: Union["Row", None] = None
|
||||||
self._sort_descriptor = None
|
self._sort_descriptor: Union[SortDescriptor, None] = None
|
||||||
|
|
||||||
# --- Virtual
|
# --- Virtual
|
||||||
def _do_add(self):
|
def _do_add(self) -> Tuple["Row", int]:
|
||||||
"""(Virtual) Creates a new row, adds it in the table.
|
"""(Virtual) Creates a new row, adds it in the table.
|
||||||
|
|
||||||
Returns ``(row, insert_index)``.
|
Returns ``(row, insert_index)``.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _do_delete(self):
|
def _do_delete(self) -> None:
|
||||||
"""(Virtual) Delete the selected rows."""
|
"""(Virtual) Delete the selected rows."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _fill(self):
|
def _fill(self) -> None:
|
||||||
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
|
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
|
||||||
|
|
||||||
Called by :meth:`refresh`. Does nothing by default.
|
Called by :meth:`refresh`. Does nothing by default.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_edited_new(self):
|
def _is_edited_new(self) -> bool:
|
||||||
"""(Virtual) Returns whether the currently edited row should be considered "new".
|
"""(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
|
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])
|
self.select([len(self) - 1])
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def add(self):
|
def add(self) -> None:
|
||||||
"""Add a new row in edit mode.
|
"""Add a new row in edit mode.
|
||||||
|
|
||||||
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
|
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.edited = row
|
||||||
self.view.start_editing()
|
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.
|
"""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`.
|
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]
|
row = self[row_index]
|
||||||
return row.can_edit_cell(column_name)
|
return row.can_edit_cell(column_name)
|
||||||
|
|
||||||
def cancel_edits(self):
|
def cancel_edits(self) -> None:
|
||||||
"""Cancels the current edit operation.
|
"""Cancels the current edit operation.
|
||||||
|
|
||||||
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
|
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.edited = None
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self) -> None:
|
||||||
"""Delete the currently selected rows.
|
"""Delete the currently selected rows.
|
||||||
|
|
||||||
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
|
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:
|
if self:
|
||||||
self._do_delete()
|
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.
|
"""Empty the table and re-create its rows.
|
||||||
|
|
||||||
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
|
: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:
|
if refresh_view:
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
def save_edits(self):
|
def save_edits(self) -> None:
|
||||||
"""Commit user edits to the model.
|
"""Commit user edits to the model.
|
||||||
|
|
||||||
This is done by calling :meth:`Row.save`.
|
This is done by calling :meth:`Row.save`.
|
||||||
@@ -410,7 +417,7 @@ class GUITable(Table, GUIObject):
|
|||||||
self.edited = None
|
self.edited = None
|
||||||
row.save()
|
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``.
|
"""Sort table by ``column_name``.
|
||||||
|
|
||||||
Overrides :meth:`Table.sort_by`. After having performed sorting, calls
|
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.
|
Of course, this is only default behavior. This can be overriden.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, table):
|
def __init__(self, table: GUITable) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.table = table
|
self.table = table
|
||||||
|
|
||||||
def _edit(self):
|
def _edit(self) -> None:
|
||||||
if self.table.edited is self:
|
if self.table.edited is self:
|
||||||
return
|
return
|
||||||
assert self.table.edited is None
|
assert self.table.edited is None
|
||||||
self.table.edited = self
|
self.table.edited = self
|
||||||
|
|
||||||
# --- Virtual
|
# --- Virtual
|
||||||
def can_edit(self):
|
def can_edit(self) -> bool:
|
||||||
"""(Virtual) Whether the whole row can be edited.
|
"""(Virtual) Whether the whole row can be edited.
|
||||||
|
|
||||||
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
||||||
@@ -469,7 +476,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load(self):
|
def load(self) -> None:
|
||||||
"""(Virtual/Required) Loads up values from the model to be presented in the table.
|
"""(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
|
Usually, our model instances contain values that are not quite ready for display. If you
|
||||||
@@ -478,7 +485,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> None:
|
||||||
"""(Virtual/Required) Saves user edits into your model.
|
"""(Virtual/Required) Saves user edits into your model.
|
||||||
|
|
||||||
If your table is editable, this is called when the user commits his changes. Usually, these
|
If your table is editable, this is called when the user commits his changes. Usually, these
|
||||||
@@ -487,7 +494,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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``.
|
"""(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
|
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)
|
return getattr(self, column_name)
|
||||||
|
|
||||||
# --- Public
|
# --- 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.
|
"""Returns whether cell for column ``column_name`` can be edited.
|
||||||
|
|
||||||
By the default, the check is done in many steps:
|
By the default, the check is done in many steps:
|
||||||
@@ -530,7 +537,7 @@ class Row:
|
|||||||
return False
|
return False
|
||||||
return bool(getattr(prop, "fset", None))
|
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``.
|
"""Get cell value for ``attrname``.
|
||||||
|
|
||||||
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
||||||
@@ -540,7 +547,7 @@ class Row:
|
|||||||
attrname = "from_"
|
attrname = "from_"
|
||||||
return getattr(self, attrname)
|
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``.
|
"""Set cell value to ``value`` for ``attrname``.
|
||||||
|
|
||||||
By default, does a simple ``setattr()``, but it is used to allow subclasses to have
|
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
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Callable, Generator, Iterator, List, Union
|
||||||
|
|
||||||
|
|
||||||
class JobCancelled(Exception):
|
class JobCancelled(Exception):
|
||||||
"The user has cancelled the job"
|
"The user has cancelled the job"
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class Job:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# ---Magic functions
|
# ---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
|
"""Initialize the Job with 'jobcount' jobs. Start every job with
|
||||||
start_job(). Every time the job progress is updated, 'callback' is called
|
start_job(). Every time the job progress is updated, 'callback' is called
|
||||||
'callback' takes a 'progress' int param, and a optional 'desc'
|
'callback' takes a 'progress' int param, and a optional 'desc'
|
||||||
@@ -55,12 +58,12 @@ class Job:
|
|||||||
self._currmax = 1
|
self._currmax = 1
|
||||||
|
|
||||||
# ---Private
|
# ---Private
|
||||||
def _subjob_callback(self, progress, desc=""):
|
def _subjob_callback(self, progress: int, desc: str = "") -> bool:
|
||||||
"""This is the callback passed to children jobs."""
|
"""This is the callback passed to children jobs."""
|
||||||
self.set_progress(progress, desc)
|
self.set_progress(progress, desc)
|
||||||
return True # if JobCancelled has to be raised, it will be at the highest level
|
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.
|
"""Calls the callback function with a % progress as a parameter.
|
||||||
|
|
||||||
The parameter is a int in the 0-100 range.
|
The parameter is a int in the 0-100 range.
|
||||||
@@ -78,13 +81,16 @@ class Job:
|
|||||||
raise JobCancelled()
|
raise JobCancelled()
|
||||||
|
|
||||||
# ---Public
|
# ---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)
|
self.set_progress(self._progress + progress, desc)
|
||||||
|
|
||||||
def check_if_cancelled(self):
|
def check_if_cancelled(self) -> None:
|
||||||
self._do_update("")
|
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.
|
"""Iterate through ``iterable`` while automatically adding progress.
|
||||||
|
|
||||||
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
|
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)
|
desc = desc_format % (count, count)
|
||||||
self.set_progress(100, desc)
|
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
|
"""Begin work on the next job. You must not call start_job more than
|
||||||
'jobcount' (in __init__) times.
|
'jobcount' (in __init__) times.
|
||||||
'max' is the job units you are to perform.
|
'max' is the job units you are to perform.
|
||||||
@@ -122,7 +128,7 @@ class Job:
|
|||||||
self._currmax = max(1, max_progress)
|
self._currmax = max(1, max_progress)
|
||||||
self._do_update(desc)
|
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
|
"""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
|
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
|
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)
|
self.start_job(100, desc)
|
||||||
return Job(job_proportions, self._subjob_callback)
|
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
|
"""Sets the progress of the current job to 'progress', and call the
|
||||||
callback
|
callback
|
||||||
"""
|
"""
|
||||||
@@ -143,29 +149,29 @@ class Job:
|
|||||||
|
|
||||||
|
|
||||||
class NullJob:
|
class NullJob:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_progress(self, *args, **kwargs):
|
def add_progress(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_if_cancelled(self):
|
def check_if_cancelled(self) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator:
|
||||||
return iter(sequence)
|
return iter(sequence)
|
||||||
|
|
||||||
def start_job(self, *args, **kwargs):
|
def start_job(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_subjob(self, *args, **kwargs):
|
def start_subjob(self, *args, **kwargs) -> "NullJob":
|
||||||
return NullJob()
|
return NullJob()
|
||||||
|
|
||||||
def set_progress(self, *args, **kwargs):
|
def set_progress(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Callable, Tuple, Union
|
||||||
|
|
||||||
from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled
|
from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled
|
||||||
|
|
||||||
@@ -28,15 +29,15 @@ class ThreadedJobPerformer:
|
|||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
# --- Protected
|
# --- Protected
|
||||||
def create_job(self):
|
def create_job(self) -> Job:
|
||||||
if self._job_running:
|
if self._job_running:
|
||||||
raise JobInProgressError()
|
raise JobInProgressError()
|
||||||
self.last_progress = -1
|
self.last_progress: Union[int, None] = -1
|
||||||
self.last_desc = ""
|
self.last_desc = ""
|
||||||
self.job_cancelled = False
|
self.job_cancelled = False
|
||||||
return Job(1, self._update_progress)
|
return Job(1, self._update_progress)
|
||||||
|
|
||||||
def _async_run(self, *args):
|
def _async_run(self, *args) -> None:
|
||||||
target = args[0]
|
target = args[0]
|
||||||
args = tuple(args[1:])
|
args = tuple(args[1:])
|
||||||
self._job_running = True
|
self._job_running = True
|
||||||
@@ -52,7 +53,7 @@ class ThreadedJobPerformer:
|
|||||||
self._job_running = False
|
self._job_running = False
|
||||||
self.last_progress = None
|
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.
|
"""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
|
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:
|
if self.last_error is not None:
|
||||||
raise self.last_error.with_traceback(self.last_traceback)
|
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
|
self.last_progress = newprogress
|
||||||
if newdesc:
|
if newdesc:
|
||||||
self.last_desc = newdesc
|
self.last_desc = newdesc
|
||||||
return not self.job_cancelled
|
return not self.job_cancelled
|
||||||
|
|
||||||
def run_threaded(self, target, args=()):
|
def run_threaded(self, target: Callable, args: Tuple = ()) -> None:
|
||||||
if self._job_running:
|
if self._job_running:
|
||||||
raise JobInProgressError()
|
raise JobInProgressError()
|
||||||
args = (target,) + args
|
args = (target,) + args
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def get_locale_name(lang: str) -> Union[str, None]:
|
|||||||
|
|
||||||
# --- Qt
|
# --- Qt
|
||||||
def install_qt_trans(lang: str = None) -> None:
|
def install_qt_trans(lang: str = None) -> None:
|
||||||
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale
|
from PyQt6.QtCore import QCoreApplication, QTranslator, QLocale
|
||||||
|
|
||||||
if not lang:
|
if not lang:
|
||||||
lang = str(QLocale.system().name())[:2]
|
lang = str(QLocale.system().name())[:2]
|
||||||
@@ -139,7 +139,7 @@ def install_gettext_trans_under_qt(base_folder: os.PathLike, lang: str = None) -
|
|||||||
# So, we install the gettext locale, great, but we also should try to install qt_*.qm if
|
# So, we install the gettext locale, great, but we also should try to install qt_*.qm if
|
||||||
# available so that strings that are inside Qt itself over which I have no control are in the
|
# available so that strings that are inside Qt itself over which I have no control are in the
|
||||||
# right language.
|
# right language.
|
||||||
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo
|
from PyQt6.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo
|
||||||
|
|
||||||
if not lang:
|
if not lang:
|
||||||
lang = str(QLocale.system().name())[:2]
|
lang = str(QLocale.system().name())[:2]
|
||||||
@@ -155,7 +155,7 @@ def install_gettext_trans_under_qt(base_folder: os.PathLike, lang: str = None) -
|
|||||||
if ISLINUX:
|
if ISLINUX:
|
||||||
# Under linux, a full Qt installation is already available in the system, we didn't bundle
|
# Under linux, a full Qt installation is already available in the system, we didn't bundle
|
||||||
# up the qm files in our package, so we have to load translations from the system.
|
# up the qm files in our package, so we have to load translations from the system.
|
||||||
qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname)
|
qmpath = op.join(QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath), qmname)
|
||||||
else:
|
else:
|
||||||
qmpath = op.join(base_folder, qmname)
|
qmpath = op.join(base_folder, qmname)
|
||||||
qtr = QTranslator(QCoreApplication.instance())
|
qtr = QTranslator(QCoreApplication.instance())
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Emanuele, 2022
|
# Emanuele, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2022
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
|
# Giovanni, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
"Last-Translator: Giovanni, 2022\n"
|
||||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||||
|
|
||||||
#: qt/app.py:81
|
#: qt/app.py:81
|
||||||
msgid "Quit"
|
msgid "Quit"
|
||||||
@@ -979,37 +980,40 @@ msgstr "Ignora file più grandi di"
|
|||||||
|
|
||||||
#: qt\app.py:135 qt\app.py:293
|
#: qt\app.py:135 qt\app.py:293
|
||||||
msgid "Clear Cache"
|
msgid "Clear Cache"
|
||||||
msgstr ""
|
msgstr "Svuota cache"
|
||||||
|
|
||||||
#: qt\app.py:294
|
#: qt\app.py:294
|
||||||
msgid ""
|
msgid ""
|
||||||
"Do you really want to clear the cache? This will remove all cached file "
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
"hashes and picture analysis."
|
"hashes and picture analysis."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Vuoi davvero svuotare la cache? Ciò rimuoverà tutti gli hash dei file "
|
||||||
|
"memorizzati nella cache e le analisi delle immagini."
|
||||||
|
|
||||||
#: qt\app.py:299
|
#: qt\app.py:299
|
||||||
msgid "Cache cleared."
|
msgid "Cache cleared."
|
||||||
msgstr ""
|
msgstr "Cache svuotata"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr "Usa stile scuro"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:241
|
#: qt\preferences_dialog.py:241
|
||||||
msgid "Profile scan operation"
|
msgid "Profile scan operation"
|
||||||
msgstr ""
|
msgstr "Profila l'operazione di scansione"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:242
|
#: qt\preferences_dialog.py:242
|
||||||
msgid "Profile the scan operation and save logs for optimization."
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Profila l'operazione di scansione e salva i registri per l'ottimizzazione."
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:246
|
#: qt\preferences_dialog.py:246
|
||||||
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
msgstr ""
|
msgstr "I log si trovano in: <a href=\"{}\">{}</a>"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:291
|
#: qt\preferences_dialog.py:291
|
||||||
msgid "Debug"
|
msgid "Debug"
|
||||||
msgstr ""
|
msgstr "Debug"
|
||||||
|
|
||||||
#: qt\about_box.py:31
|
#: qt\about_box.py:31
|
||||||
msgid "About {}"
|
msgid "About {}"
|
||||||
@@ -1021,7 +1025,7 @@ msgstr "Versione {}"
|
|||||||
|
|
||||||
#: qt\about_box.py:49 qt\about_box.py:75
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
msgid "Checking for updates..."
|
msgid "Checking for updates..."
|
||||||
msgstr ""
|
msgstr "Controllo degli aggiornamenti..."
|
||||||
|
|
||||||
#: qt\about_box.py:54
|
#: qt\about_box.py:54
|
||||||
msgid "Licensed under GPLv3"
|
msgid "Licensed under GPLv3"
|
||||||
@@ -1029,11 +1033,11 @@ msgstr "Distribuito sotto licenza GPLv3"
|
|||||||
|
|
||||||
#: qt\about_box.py:68
|
#: qt\about_box.py:68
|
||||||
msgid "No update available."
|
msgid "No update available."
|
||||||
msgstr ""
|
msgstr "Nessun aggiornamento disponibile."
|
||||||
|
|
||||||
#: qt\about_box.py:71
|
#: qt\about_box.py:71
|
||||||
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
msgstr ""
|
msgstr "È disponibile la nuova versione {}, scaricabile <a href=\"{}\">qui</a>."
|
||||||
|
|
||||||
#: qt\error_report_dialog.py:50
|
#: qt\error_report_dialog.py:50
|
||||||
msgid "Error Report"
|
msgid "Error Report"
|
||||||
|
|||||||
@@ -1,138 +1,139 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Yuji Sasaki, 2022
|
||||||
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
||||||
"Language: ja\n"
|
"Language: ja\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
#: core\app.py:42
|
#: core\app.py:44
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr "マークされた重複はありません。 何も行われていません。"
|
msgstr "チェックを入れた重複はありません。 何も行われませんでした。"
|
||||||
|
|
||||||
#: core\app.py:43
|
#: core\app.py:45
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr "選択された重複はありません。 何も行われていません。"
|
msgstr "選択された重複はありません。 何も行われていません。"
|
||||||
|
|
||||||
#: core\app.py:44
|
#: core\app.py:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You're about to open many files at once. Depending on what those files are "
|
"You're about to open many files at once. Depending on what those files are "
|
||||||
"opened with, doing so can create quite a mess. Continue?"
|
"opened with, doing so can create quite a mess. Continue?"
|
||||||
msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?"
|
msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:71
|
#: core\app.py:73
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
msgstr "重複のスキャン"
|
msgstr "重複のスキャン"
|
||||||
|
|
||||||
#: core\app.py:72
|
#: core\app.py:74
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr "読み込み中"
|
msgstr "読み込み中"
|
||||||
|
|
||||||
#: core\app.py:73
|
#: core\app.py:75
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr "移動します"
|
msgstr "移動します"
|
||||||
|
|
||||||
#: core\app.py:74
|
#: core\app.py:76
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr "コピー中"
|
msgstr "コピー中"
|
||||||
|
|
||||||
#: core\app.py:75
|
#: core\app.py:77
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "ごみ箱に送信します"
|
msgstr "ごみ箱に送信します"
|
||||||
|
|
||||||
#: core\app.py:289
|
#: core\app.py:291
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
||||||
|
|
||||||
#: core\app.py:300
|
#: core\app.py:302
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "重複は見つかりませんでした。"
|
msgstr "重複は見つかりませんでした。"
|
||||||
|
|
||||||
#: core\app.py:315
|
|
||||||
msgid "All marked files were copied successfully."
|
|
||||||
msgstr "マークされたファイルはすべて正常にコピーされました。"
|
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "マークされたファイルはすべて正常に移動されました。"
|
msgstr "チェックを入れたファイルをすべてコピーしました。"
|
||||||
|
|
||||||
#: core\app.py:319
|
#: core\app.py:319
|
||||||
msgid "All marked files were deleted successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr "チェックを入れたファイルをすべて移動しました。"
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were deleted successfully."
|
||||||
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
|
msgstr "チェックを入れたファイルをすべて削除しました。"
|
||||||
|
|
||||||
#: core\app.py:326
|
#: core\app.py:323
|
||||||
|
msgid "All marked files were successfully sent to Trash."
|
||||||
|
msgstr "チェックを入れたファイルをすべてごみ箱に移動しました。"
|
||||||
|
|
||||||
|
#: core\app.py:328
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "ファイルを読み込めませんでした:{}"
|
msgstr "ファイルを読み込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:382
|
#: core\app.py:384
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "「{}」既にリストに含まれています。"
|
msgstr "「{}」既にリストに含まれています。"
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' 存在しません。"
|
msgstr "'{}' 存在しません。"
|
||||||
|
|
||||||
#: core\app.py:392
|
#: core\app.py:394
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:469
|
#: core\app.py:471
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:512
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "エクスポートしたCSVの宛先を選択します。"
|
msgstr "エクスポートしたCSVの宛先を選択します。"
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
#: core\app.py:518 core\app.py:773 core\app.py:783
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "ファイルに書き込めませんでした:{}"
|
msgstr "ファイルに書き込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:539
|
#: core\app.py:541
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。"
|
msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。"
|
||||||
|
|
||||||
#: core\app.py:695 core\app.py:707
|
#: core\app.py:697 core\app.py:709
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:743
|
#: core\app.py:745
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
||||||
|
|
||||||
#: core\app.py:790
|
#: core\app.py:792
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
||||||
|
|
||||||
#: core\app.py:803
|
#: core\app.py:808
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "スキャンするファイルを収集しています"
|
msgstr "スキャンするファイルを収集しています"
|
||||||
|
|
||||||
#: core\app.py:850
|
#: core\app.py:858
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d 廃棄)"
|
msgstr "%s (%d 廃棄)"
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:190
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\directories.py:207
|
#: core\directories.py:206
|
||||||
msgid "Collected {} folders to scan"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -200,35 +201,35 @@ msgstr "EXIFタイムスタンプ"
|
|||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "無し"
|
msgstr "無し"
|
||||||
|
|
||||||
#: core\prioritize.py:100
|
#: core\prioritize.py:102
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr "番号で終わっている"
|
msgstr "番号で終わっている"
|
||||||
|
|
||||||
#: core\prioritize.py:101
|
#: core\prioritize.py:103
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr "数字で終わっていない"
|
msgstr "数字で終わっていない"
|
||||||
|
|
||||||
#: core\prioritize.py:102
|
#: core\prioritize.py:104
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr "最長"
|
msgstr "最長"
|
||||||
|
|
||||||
#: core\prioritize.py:103
|
#: core\prioritize.py:105
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr "最短"
|
msgstr "最短"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr "最高"
|
msgstr "最高"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr "最低"
|
msgstr "最低"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr "最新"
|
msgstr "最新"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "最古"
|
msgstr "最古"
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ msgstr "(非対応)"
|
|||||||
|
|
||||||
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Directly delete files"
|
msgid "Directly delete files"
|
||||||
msgstr "ファイルを直接削除する"
|
msgstr "ファイルを完全に削除"
|
||||||
|
|
||||||
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -100,7 +100,7 @@ msgstr "キャンセル"
|
|||||||
|
|
||||||
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Attribute"
|
msgid "Attribute"
|
||||||
msgstr "アトリビュート"
|
msgstr "属性"
|
||||||
|
|
||||||
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Selected"
|
msgid "Selected"
|
||||||
@@ -163,7 +163,7 @@ msgstr "スキャンの種類:"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:135
|
#: qt/directories_dialog.py:135
|
||||||
msgid "More Options"
|
msgid "More Options"
|
||||||
msgstr "もっとオプション"
|
msgstr "詳細設定"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Select folders to scan and press \"Scan\"."
|
msgid "Select folders to scan and press \"Scan\"."
|
||||||
@@ -179,7 +179,7 @@ msgstr "スキャン"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:230
|
#: qt/directories_dialog.py:230
|
||||||
msgid "Unsaved results"
|
msgid "Unsaved results"
|
||||||
msgstr "保存されていない結果"
|
msgstr "未保存の結果"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to quit?"
|
msgid "You have unsaved results, do you really want to quit?"
|
||||||
@@ -280,27 +280,27 @@ msgstr "単語の重み付け"
|
|||||||
#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32
|
#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Match similar words"
|
msgid "Match similar words"
|
||||||
msgstr "類似の単語に一致する"
|
msgstr "類似の単語を一致"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
|
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
|
||||||
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Can mix file kind"
|
msgid "Can mix file kind"
|
||||||
msgstr "ファイルの種類を混在させることができる"
|
msgstr "ファイルの種類を混在"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
||||||
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Use regular expressions when filtering"
|
msgid "Use regular expressions when filtering"
|
||||||
msgstr "フィルタリング時に正規表現を使用する"
|
msgstr "フィルタに正規表現を使用"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
|
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
|
||||||
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove empty folders on delete or move"
|
msgid "Remove empty folders on delete or move"
|
||||||
msgstr "削除または移動時に空のフォルダを削除する"
|
msgstr "削除や移動で空になったフォルダを削除"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
|
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
|
||||||
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Ignore duplicates hardlinking to the same file"
|
msgid "Ignore duplicates hardlinking to the same file"
|
||||||
msgstr "同じファイルへの重複ハードリンクを無視する"
|
msgstr "同じファイルへの重複ハードリンクを無視"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
|
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
|
||||||
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0
|
||||||
@@ -309,11 +309,11 @@ msgstr "デバッグモード(再起動が必要)"
|
|||||||
|
|
||||||
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Match pictures of different dimensions"
|
msgid "Match pictures of different dimensions"
|
||||||
msgstr "異なる寸法の写真を一致させる"
|
msgstr "異なるサイズの写真を一致"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:43
|
#: qt/preferences_dialog.py:43
|
||||||
msgid "Filter Hardness:"
|
msgid "Filter Hardness:"
|
||||||
msgstr "フィルター硬度:"
|
msgstr "フィルタの強さ:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:69
|
#: qt/preferences_dialog.py:69
|
||||||
msgid "More Results"
|
msgid "More Results"
|
||||||
@@ -325,7 +325,7 @@ msgstr "より少ない結果"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:81
|
#: qt/preferences_dialog.py:81
|
||||||
msgid "Font size:"
|
msgid "Font size:"
|
||||||
msgstr "フォントサイズ:"
|
msgstr "文字サイズ:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:85
|
#: qt/preferences_dialog.py:85
|
||||||
msgid "Language:"
|
msgid "Language:"
|
||||||
@@ -333,7 +333,7 @@ msgstr "言語:"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Copy and Move:"
|
msgid "Copy and Move:"
|
||||||
msgstr "コピーと移動:"
|
msgstr "コピーと移動:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
|
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Right in destination"
|
msgid "Right in destination"
|
||||||
@@ -349,7 +349,7 @@ msgstr "絶対パスを再作成"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:99
|
#: qt/preferences_dialog.py:99
|
||||||
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
||||||
msgstr "カスタムコマンド (引数:重複の場合は%d、参照の場合は%r):"
|
msgstr "カスタムコマンド (引数: %dは重複・%rは参照):"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:174
|
#: qt/preferences_dialog.py:174
|
||||||
msgid "dupeGuru has to restart for language changes to take effect."
|
msgid "dupeGuru has to restart for language changes to take effect."
|
||||||
@@ -719,7 +719,7 @@ msgstr "ウィンドウ"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Zoom"
|
msgid "Zoom"
|
||||||
msgstr "ズーム"
|
msgstr "拡大"
|
||||||
|
|
||||||
#: qt\app.py:158
|
#: qt\app.py:158
|
||||||
msgid "Exclusion Filters"
|
msgid "Exclusion Filters"
|
||||||
@@ -909,15 +909,15 @@ msgstr "結果"
|
|||||||
|
|
||||||
#: qt\preferences_dialog.py:150
|
#: qt\preferences_dialog.py:150
|
||||||
msgid "General Interface"
|
msgid "General Interface"
|
||||||
msgstr "一般的なインターフェイス"
|
msgstr "一般"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:176
|
#: qt\preferences_dialog.py:176
|
||||||
msgid "Result Table"
|
msgid "Result Table"
|
||||||
msgstr "結果表"
|
msgstr "結果"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:205
|
#: qt\preferences_dialog.py:205
|
||||||
msgid "Details Window"
|
msgid "Details Window"
|
||||||
msgstr "詳細ウィンドウ"
|
msgstr "詳細画面"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:285
|
#: qt\preferences_dialog.py:285
|
||||||
msgid "General"
|
msgid "General"
|
||||||
@@ -985,7 +985,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\about_box.py:31
|
#: qt\about_box.py:31
|
||||||
msgid "About {}"
|
msgid "About {}"
|
||||||
msgstr "{}について。"
|
msgstr "{}について"
|
||||||
|
|
||||||
#: qt\about_box.py:47
|
#: qt\about_box.py:47
|
||||||
msgid "Version {}"
|
msgid "Version {}"
|
||||||
@@ -997,7 +997,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\about_box.py:54
|
#: qt\about_box.py:54
|
||||||
msgid "Licensed under GPLv3"
|
msgid "Licensed under GPLv3"
|
||||||
msgstr "GPLv3としてライセンス供与。"
|
msgstr "GPLv3のもとでライセンスされています"
|
||||||
|
|
||||||
#: qt\about_box.py:68
|
#: qt\about_box.py:68
|
||||||
msgid "No update available."
|
msgid "No update available."
|
||||||
@@ -1013,7 +1013,7 @@ msgstr "エラーレポート"
|
|||||||
|
|
||||||
#: qt\error_report_dialog.py:54
|
#: qt\error_report_dialog.py:54
|
||||||
msgid "Something went wrong. How about reporting the error?"
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
msgstr "何かがうまくいかなかった。 エラーを報告するのはどうですか?"
|
msgstr "不明な理由により失敗しました。問題を報告しませんか?"
|
||||||
|
|
||||||
#: qt\error_report_dialog.py:60
|
#: qt\error_report_dialog.py:60
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1035,7 +1035,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt\error_report_dialog.py:80
|
#: qt\error_report_dialog.py:80
|
||||||
msgid "Go to Github"
|
msgid "Go to Github"
|
||||||
msgstr "Githubに移動する"
|
msgstr "Githubに移動"
|
||||||
|
|
||||||
#: qt\preferences.py:24
|
#: qt\preferences.py:24
|
||||||
msgid "Czech"
|
msgid "Czech"
|
||||||
|
|||||||
10
macos.md
10
macos.md
@@ -7,19 +7,19 @@ These instructions are for the Qt version of the UI on macOS.
|
|||||||
- [Python 3.7+][python]
|
- [Python 3.7+][python]
|
||||||
- [Xcode 12.3][xcode] or just Xcode command line tools (older versions can be used if not interested in arm macs)
|
- [Xcode 12.3][xcode] or just Xcode command line tools (older versions can be used if not interested in arm macs)
|
||||||
- [Homebrew][homebrew]
|
- [Homebrew][homebrew]
|
||||||
- [qt5](https://www.qt.io/)
|
- [qt6](https://www.qt.io/)
|
||||||
|
|
||||||
#### Prerequisite setup
|
#### Prerequisite setup
|
||||||
1. Install Xcode if desired
|
1. Install Xcode if desired
|
||||||
2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`
|
2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`
|
||||||
with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take
|
with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take
|
||||||
effect.
|
effect.
|
||||||
3. Install qt5 with `brew`. If you are using a version of macos without system python 3.7+ then you will
|
3. Install qt6 with `brew`. If you are using a version of macos without system python 3.7+ then you will
|
||||||
also need to install that via brew or with pyenv.
|
also need to install that via brew or with pyenv.
|
||||||
|
|
||||||
$ brew install qt5
|
$ brew install qt6
|
||||||
|
|
||||||
NOTE: Using `brew` to install qt5 is to allow pyqt5 to build without a native wheel
|
NOTE: Using `brew` to install qt6 is to allow pyqt6 to build without a native wheel
|
||||||
available. If you are using an intel based mac you can probably skip this step.
|
available. If you are using an intel based mac you can probably skip this step.
|
||||||
|
|
||||||
4. May need to launch a new terminal to have everything working.
|
4. May need to launch a new terminal to have everything working.
|
||||||
@@ -27,7 +27,7 @@ also need to install that via brew or with pyenv.
|
|||||||
### With build.py
|
### With build.py
|
||||||
OSX comes with a version of python 3 by default in newer versions of OSX. To produce universal
|
OSX comes with a version of python 3 by default in newer versions of OSX. To produce universal
|
||||||
builds either the 3.8 version shipped in macos or 3.9.1 or newer needs to be used. If needing to
|
builds either the 3.8 version shipped in macos or 3.9.1 or newer needs to be used. If needing to
|
||||||
build pyqt5 from source then the first line below is needed, else it may be omitted. (Path shown is
|
build pyqt6 from source then the first line below is needed, else it may be omitted. (Path shown is
|
||||||
for an arm mac.)
|
for an arm mac.)
|
||||||
|
|
||||||
$ export PATH="/opt/homebrew/opt/qt/bin:$PATH"
|
$ export PATH="/opt/homebrew/opt/qt/bin:$PATH"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Vcs-Git: https://github.com/arsenetar/dupeguru.git
|
|||||||
|
|
||||||
Package: {pkgname}
|
Package: {pkgname}
|
||||||
Architecture: {arch}
|
Architecture: {arch}
|
||||||
Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt5, python3-mutagen, python3-semantic-version
|
Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt6, python3-mutagen, python3-semantic-version
|
||||||
Provides: dupeguru-se, dupeguru-me, dupeguru-pe
|
Provides: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
Replaces: dupeguru-se, dupeguru-me, dupeguru-pe
|
Replaces: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe
|
Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
|
|||||||
@@ -6,36 +6,46 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
|
from PyQt6.QtCore import Qt, QCoreApplication, QTimer
|
||||||
from PyQt5.QtGui import QPixmap, QFont
|
from PyQt6.QtGui import QPixmap, QFont, QShowEvent
|
||||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel
|
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel, QWidget
|
||||||
|
|
||||||
from core.util import check_for_update
|
from core.util import check_for_update
|
||||||
from qt.util import move_to_screen_center
|
from qt.util import move_to_screen_center
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from qt.app import DupeGuru
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class AboutBox(QDialog):
|
class AboutBox(QDialog):
|
||||||
def __init__(self, parent, app, **kwargs):
|
def __init__(self, parent: QWidget, app: "DupeGuru", **kwargs) -> None:
|
||||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
|
flags = (
|
||||||
|
Qt.WindowType.CustomizeWindowHint
|
||||||
|
| Qt.WindowType.WindowTitleHint
|
||||||
|
| Qt.WindowType.WindowSystemMenuHint
|
||||||
|
| Qt.WindowType.MSWindowsFixedSizeDialogHint
|
||||||
|
)
|
||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self.app = app
|
self.app = app
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
|
|
||||||
self.button_box.accepted.connect(self.accept)
|
def _setupUi(self) -> None:
|
||||||
self.button_box.rejected.connect(self.reject)
|
|
||||||
|
|
||||||
def _setupUi(self):
|
|
||||||
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
|
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
|
||||||
size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
size_policy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
self.setSizePolicy(size_policy)
|
self.setSizePolicy(size_policy)
|
||||||
main_layout = QHBoxLayout(self)
|
main_layout = QHBoxLayout(self)
|
||||||
|
|
||||||
logo_label = QLabel()
|
logo_label = QLabel()
|
||||||
logo_label.setPixmap(QPixmap(":/%s_big" % self.app.LOGO_NAME))
|
logo_label.setPixmap(QPixmap(f"images:{self.app.LOGO_NAME}_128.png"))
|
||||||
main_layout.addWidget(logo_label)
|
main_layout.addWidget(logo_label)
|
||||||
|
|
||||||
detail_layout = QVBoxLayout()
|
detail_layout = QVBoxLayout()
|
||||||
|
|
||||||
name_label = QLabel()
|
name_label = QLabel()
|
||||||
font = QFont()
|
font = QFont()
|
||||||
font.setWeight(75)
|
font.setWeight(75)
|
||||||
@@ -43,26 +53,35 @@ class AboutBox(QDialog):
|
|||||||
name_label.setFont(font)
|
name_label.setFont(font)
|
||||||
name_label.setText(QCoreApplication.instance().applicationName())
|
name_label.setText(QCoreApplication.instance().applicationName())
|
||||||
detail_layout.addWidget(name_label)
|
detail_layout.addWidget(name_label)
|
||||||
|
|
||||||
version_label = QLabel()
|
version_label = QLabel()
|
||||||
version_label.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
|
version_label.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
|
||||||
detail_layout.addWidget(version_label)
|
detail_layout.addWidget(version_label)
|
||||||
|
|
||||||
self.update_label = QLabel(tr("Checking for updates..."))
|
self.update_label = QLabel(tr("Checking for updates..."))
|
||||||
self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
self.update_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||||
self.update_label.setOpenExternalLinks(True)
|
self.update_label.setOpenExternalLinks(True)
|
||||||
detail_layout.addWidget(self.update_label)
|
detail_layout.addWidget(self.update_label)
|
||||||
|
|
||||||
license_label = QLabel()
|
license_label = QLabel()
|
||||||
license_label.setText(tr("Licensed under GPLv3"))
|
license_label.setText(tr("Licensed under GPLv3"))
|
||||||
detail_layout.addWidget(license_label)
|
detail_layout.addWidget(license_label)
|
||||||
|
|
||||||
spacer_label = QLabel()
|
spacer_label = QLabel()
|
||||||
spacer_label.setFont(font)
|
spacer_label.setFont(font)
|
||||||
detail_layout.addWidget(spacer_label)
|
detail_layout.addWidget(spacer_label)
|
||||||
self.button_box = QDialogButtonBox()
|
|
||||||
self.button_box.setOrientation(Qt.Horizontal)
|
button_box = QDialogButtonBox()
|
||||||
self.button_box.setStandardButtons(QDialogButtonBox.Ok)
|
button_box.setOrientation(Qt.Orientation.Horizontal)
|
||||||
detail_layout.addWidget(self.button_box)
|
button_box.setStandardButtons(QDialogButtonBox.StandardButton.Ok)
|
||||||
|
detail_layout.addWidget(button_box)
|
||||||
|
|
||||||
main_layout.addLayout(detail_layout)
|
main_layout.addLayout(detail_layout)
|
||||||
|
|
||||||
def _check_for_update(self):
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
def _check_for_update(self) -> None:
|
||||||
update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False)
|
update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False)
|
||||||
if update is None:
|
if update is None:
|
||||||
self.update_label.setText(tr("No update available."))
|
self.update_label.setText(tr("No update available."))
|
||||||
@@ -71,7 +90,7 @@ class AboutBox(QDialog):
|
|||||||
tr('New version {} available, download <a href="{}">here</a>.').format(update["version"], update["url"])
|
tr('New version {} available, download <a href="{}">here</a>.').format(update["version"], update["url"])
|
||||||
)
|
)
|
||||||
|
|
||||||
def showEvent(self, event):
|
def showEvent(self, event: QShowEvent) -> None:
|
||||||
self.update_label.setText(tr("Checking for updates..."))
|
self.update_label.setText(tr("Checking for updates..."))
|
||||||
# have to do this here as the frameGeometry is not correct until shown
|
# have to do this here as the frameGeometry is not correct until shown
|
||||||
move_to_screen_center(self)
|
move_to_screen_center(self)
|
||||||
|
|||||||
106
qt/app.py
106
qt/app.py
@@ -6,15 +6,18 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os.path as op
|
import os.path as op
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt
|
from PyQt6.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt
|
||||||
from PyQt5.QtGui import QColor, QDesktopServices, QPalette
|
from PyQt6.QtGui import QColor, QDesktopServices, QPalette
|
||||||
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip
|
from PyQt6.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon import desktop, plat
|
from hscommon import desktop, plat
|
||||||
|
|
||||||
from qt.about_box import AboutBox
|
from qt.about_box import AboutBox
|
||||||
|
from qt.details_dialog import DetailsDialog
|
||||||
|
from qt.preferences_dialog import PreferencesDialogBase
|
||||||
from qt.recent import Recent
|
from qt.recent import Recent
|
||||||
from qt.util import create_actions
|
from qt.util import create_actions
|
||||||
from qt.progress_window import ProgressWindow
|
from qt.progress_window import ProgressWindow
|
||||||
@@ -42,10 +45,10 @@ tr = trget("ui")
|
|||||||
|
|
||||||
|
|
||||||
class DupeGuru(QObject):
|
class DupeGuru(QObject):
|
||||||
LOGO_NAME = "logo_se"
|
LOGO_NAME = "dgse_logo"
|
||||||
NAME = "dupeGuru"
|
NAME = "dupeGuru"
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.prefs = Preferences()
|
self.prefs = Preferences()
|
||||||
self.prefs.load()
|
self.prefs.load()
|
||||||
@@ -56,7 +59,7 @@ class DupeGuru(QObject):
|
|||||||
self._setup()
|
self._setup()
|
||||||
|
|
||||||
# --- Private
|
# --- Private
|
||||||
def _setup(self):
|
def _setup(self) -> None:
|
||||||
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
|
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
|
||||||
self._setupActions()
|
self._setupActions()
|
||||||
self.details_dialog = None
|
self.details_dialog = None
|
||||||
@@ -108,7 +111,7 @@ class DupeGuru(QObject):
|
|||||||
# that the application haven't launched.
|
# that the application haven't launched.
|
||||||
QTimer.singleShot(0, self.finishedLaunching)
|
QTimer.singleShot(0, self.finishedLaunching)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self) -> None:
|
||||||
# Setup actions that are common to both the directory dialog and the results window.
|
# Setup actions that are common to both the directory dialog and the results window.
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
@@ -154,7 +157,7 @@ class DupeGuru(QObject):
|
|||||||
]
|
]
|
||||||
create_actions(ACTIONS, self)
|
create_actions(ACTIONS, self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self) -> None:
|
||||||
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
|
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
|
||||||
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
|
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
|
||||||
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
|
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
|
||||||
@@ -200,7 +203,7 @@ class DupeGuru(QObject):
|
|||||||
self._set_style("dark" if self.prefs.use_dark_style else "light")
|
self._set_style("dark" if self.prefs.use_dark_style else "light")
|
||||||
|
|
||||||
# --- Private
|
# --- Private
|
||||||
def _get_details_dialog_class(self):
|
def _get_details_dialog_class(self) -> Type[DetailsDialog]:
|
||||||
if self.model.app_mode == AppMode.PICTURE:
|
if self.model.app_mode == AppMode.PICTURE:
|
||||||
return DetailsDialogPicture
|
return DetailsDialogPicture
|
||||||
elif self.model.app_mode == AppMode.MUSIC:
|
elif self.model.app_mode == AppMode.MUSIC:
|
||||||
@@ -208,7 +211,7 @@ class DupeGuru(QObject):
|
|||||||
else:
|
else:
|
||||||
return DetailsDialogStandard
|
return DetailsDialogStandard
|
||||||
|
|
||||||
def _get_preferences_dialog_class(self):
|
def _get_preferences_dialog_class(self) -> Type[PreferencesDialogBase]:
|
||||||
if self.model.app_mode == AppMode.PICTURE:
|
if self.model.app_mode == AppMode.PICTURE:
|
||||||
return PreferencesDialogPicture
|
return PreferencesDialogPicture
|
||||||
elif self.model.app_mode == AppMode.MUSIC:
|
elif self.model.app_mode == AppMode.MUSIC:
|
||||||
@@ -216,7 +219,7 @@ class DupeGuru(QObject):
|
|||||||
else:
|
else:
|
||||||
return PreferencesDialogStandard
|
return PreferencesDialogStandard
|
||||||
|
|
||||||
def _set_style(self, style="light"):
|
def _set_style(self, style: str = "light") -> None:
|
||||||
# Only support this feature on windows for now
|
# Only support this feature on windows for now
|
||||||
if not plat.ISWINDOWS:
|
if not plat.ISWINDOWS:
|
||||||
return
|
return
|
||||||
@@ -224,18 +227,18 @@ class DupeGuru(QObject):
|
|||||||
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
||||||
palette = QApplication.style().standardPalette()
|
palette = QApplication.style().standardPalette()
|
||||||
palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
|
palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
|
||||||
palette.setColor(QPalette.ColorRole.WindowText, Qt.white)
|
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
|
||||||
palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
|
palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
|
||||||
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
|
||||||
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(53, 53, 53))
|
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(53, 53, 53))
|
||||||
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.white)
|
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
|
||||||
palette.setColor(QPalette.ColorRole.Text, Qt.white)
|
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
|
||||||
palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
|
palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
|
||||||
palette.setColor(QPalette.ColorRole.ButtonText, Qt.white)
|
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
|
||||||
palette.setColor(QPalette.ColorRole.BrightText, Qt.red)
|
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||||
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
||||||
palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
|
palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
|
||||||
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.black)
|
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(164, 166, 168))
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(164, 166, 168))
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(164, 166, 168))
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(164, 166, 168))
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(164, 166, 168))
|
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(164, 166, 168))
|
||||||
@@ -250,29 +253,31 @@ class DupeGuru(QObject):
|
|||||||
QApplication.setPalette(palette)
|
QApplication.setPalette(palette)
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def add_selected_to_ignore_list(self):
|
def add_selected_to_ignore_list(self) -> None:
|
||||||
self.model.add_selected_to_ignore_list()
|
self.model.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self) -> None:
|
||||||
self.model.remove_selected(self)
|
self.model.remove_selected()
|
||||||
|
|
||||||
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
def confirm(
|
||||||
|
self, title: str, msg: str, default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.Yes
|
||||||
|
) -> bool:
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
buttons = QMessageBox.Yes | QMessageBox.No
|
buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
||||||
return answer == QMessageBox.Yes
|
return answer == QMessageBox.StandardButton.Yes
|
||||||
|
|
||||||
def invokeCustomCommand(self):
|
def invokeCustomCommand(self) -> None:
|
||||||
self.model.invoke_custom_command()
|
self.model.invoke_custom_command()
|
||||||
|
|
||||||
def show_details(self):
|
def show_details(self) -> None:
|
||||||
if self.details_dialog is not None:
|
if self.details_dialog is not None:
|
||||||
if not self.details_dialog.isVisible():
|
if not self.details_dialog.isVisible():
|
||||||
self.details_dialog.show()
|
self.details_dialog.show()
|
||||||
else:
|
else:
|
||||||
self.details_dialog.hide()
|
self.details_dialog.hide()
|
||||||
|
|
||||||
def showResultsWindow(self):
|
def showResultsWindow(self) -> None:
|
||||||
if self.resultWindow is not None:
|
if self.resultWindow is not None:
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
if self.main_window.indexOfWidget(self.resultWindow) < 0:
|
if self.main_window.indexOfWidget(self.resultWindow) < 0:
|
||||||
@@ -282,14 +287,14 @@ class DupeGuru(QObject):
|
|||||||
else:
|
else:
|
||||||
self.resultWindow.show()
|
self.resultWindow.show()
|
||||||
|
|
||||||
def showDirectoriesWindow(self):
|
def showDirectoriesWindow(self) -> None:
|
||||||
if self.directories_dialog is not None:
|
if self.directories_dialog is not None:
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.main_window.showTab(self.directories_dialog)
|
self.main_window.showTab(self.directories_dialog)
|
||||||
else:
|
else:
|
||||||
self.directories_dialog.show()
|
self.directories_dialog.show()
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self) -> None:
|
||||||
self.willSavePrefs.emit()
|
self.willSavePrefs.emit()
|
||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self.model.save()
|
self.model.save()
|
||||||
@@ -304,7 +309,7 @@ class DupeGuru(QObject):
|
|||||||
SIGTERM = pyqtSignal()
|
SIGTERM = pyqtSignal()
|
||||||
|
|
||||||
# --- Events
|
# --- Events
|
||||||
def finishedLaunching(self):
|
def finishedLaunching(self) -> None:
|
||||||
if sys.getfilesystemencoding() == "ascii":
|
if sys.getfilesystemencoding() == "ascii":
|
||||||
# No need to localize this, it's a debugging message.
|
# No need to localize this, it's a debugging message.
|
||||||
msg = (
|
msg = (
|
||||||
@@ -324,28 +329,28 @@ class DupeGuru(QObject):
|
|||||||
self.model.load_from(results)
|
self.model.load_from(results)
|
||||||
self.recentResults.insertItem(results)
|
self.recentResults.insertItem(results)
|
||||||
|
|
||||||
def clearCacheTriggered(self):
|
def clearCacheTriggered(self) -> None:
|
||||||
title = tr("Clear Cache")
|
title = tr("Clear Cache")
|
||||||
msg = tr("Do you really want to clear the cache? This will remove all cached file hashes and picture analysis.")
|
msg = tr("Do you really want to clear the cache? This will remove all cached file hashes and picture analysis.")
|
||||||
if self.confirm(title, msg, QMessageBox.No):
|
if self.confirm(title, msg, QMessageBox.StandardButton.No):
|
||||||
self.model.clear_picture_cache()
|
self.model.clear_picture_cache()
|
||||||
self.model.clear_hash_cache()
|
self.model.clear_hash_cache()
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
QMessageBox.information(active, title, tr("Cache cleared."))
|
QMessageBox.information(active, title, tr("Cache cleared."))
|
||||||
|
|
||||||
def ignoreListTriggered(self):
|
def ignoreListTriggered(self) -> None:
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.showTriggeredTabbedDialog(self.ignoreListDialog, tr("Ignore List"))
|
self.showTriggeredTabbedDialog(self.ignoreListDialog, tr("Ignore List"))
|
||||||
else: # floating windows
|
else: # floating windows
|
||||||
self.model.ignore_list_dialog.show()
|
self.model.ignore_list_dialog.show()
|
||||||
|
|
||||||
def excludeListTriggered(self):
|
def excludeListTriggered(self) -> None:
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
|
self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
|
||||||
else: # floating windows
|
else: # floating windows
|
||||||
self.model.exclude_list_dialog.show()
|
self.model.exclude_list_dialog.show()
|
||||||
|
|
||||||
def showTriggeredTabbedDialog(self, dialog, desc_string):
|
def showTriggeredTabbedDialog(self, dialog, desc_string: str) -> None:
|
||||||
"""Add tab for dialog, name the tab with desc_string, then show it."""
|
"""Add tab for dialog, name the tab with desc_string, then show it."""
|
||||||
index = self.main_window.indexOfWidget(dialog)
|
index = self.main_window.indexOfWidget(dialog)
|
||||||
# Create the tab if it doesn't exist already
|
# Create the tab if it doesn't exist already
|
||||||
@@ -354,23 +359,22 @@ class DupeGuru(QObject):
|
|||||||
# Show the tab for that widget
|
# Show the tab for that widget
|
||||||
self.main_window.setCurrentIndex(index)
|
self.main_window.setCurrentIndex(index)
|
||||||
|
|
||||||
def openDebugLogTriggered(self):
|
def openDebugLogTriggered(self) -> None:
|
||||||
debug_log_path = op.join(self.model.appdata, "debug.log")
|
debug_log_path = op.join(self.model.appdata, "debug.log")
|
||||||
desktop.open_path(debug_log_path)
|
desktop.open_path(debug_log_path)
|
||||||
|
|
||||||
def preferencesTriggered(self):
|
def preferencesTriggered(self) -> None:
|
||||||
preferences_dialog = self._get_preferences_dialog_class()(
|
preferences_dialog = self._get_preferences_dialog_class()(
|
||||||
self.main_window if self.main_window else self.directories_dialog, self
|
self.main_window if self.main_window else self.directories_dialog, self
|
||||||
)
|
)
|
||||||
preferences_dialog.load()
|
preferences_dialog.load()
|
||||||
result = preferences_dialog.exec()
|
result = preferences_dialog.exec()
|
||||||
if result == QDialog.Accepted:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
preferences_dialog.save()
|
preferences_dialog.save()
|
||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self._update_options()
|
self._update_options()
|
||||||
preferences_dialog.setParent(None)
|
|
||||||
|
|
||||||
def quitTriggered(self):
|
def quitTriggered(self) -> None:
|
||||||
if self.details_dialog is not None:
|
if self.details_dialog is not None:
|
||||||
self.details_dialog.close()
|
self.details_dialog.close()
|
||||||
|
|
||||||
@@ -379,10 +383,10 @@ class DupeGuru(QObject):
|
|||||||
else:
|
else:
|
||||||
self.directories_dialog.close()
|
self.directories_dialog.close()
|
||||||
|
|
||||||
def showAboutBoxTriggered(self):
|
def showAboutBoxTriggered(self) -> None:
|
||||||
self.about_box.show()
|
self.about_box.show()
|
||||||
|
|
||||||
def showHelpTriggered(self):
|
def showHelpTriggered(self) -> None:
|
||||||
base_path = platform.HELP_PATH
|
base_path = platform.HELP_PATH
|
||||||
help_path = op.abspath(op.join(base_path, "index.html"))
|
help_path = op.abspath(op.join(base_path, "index.html"))
|
||||||
if op.exists(help_path):
|
if op.exists(help_path):
|
||||||
@@ -391,7 +395,7 @@ class DupeGuru(QObject):
|
|||||||
url = QUrl("https://dupeguru.voltaicideas.net/help/en/")
|
url = QUrl("https://dupeguru.voltaicideas.net/help/en/")
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def handleSIGTERM(self):
|
def handleSIGTERM(self) -> None:
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
|
|
||||||
# --- model --> view
|
# --- model --> view
|
||||||
@@ -401,20 +405,20 @@ class DupeGuru(QObject):
|
|||||||
def set_default(self, key, value):
|
def set_default(self, key, value):
|
||||||
self.prefs.set_value(key, value)
|
self.prefs.set_value(key, value)
|
||||||
|
|
||||||
def show_message(self, msg):
|
def show_message(self, msg: str) -> None:
|
||||||
window = QApplication.activeWindow()
|
window = QApplication.activeWindow()
|
||||||
QMessageBox.information(window, "", msg)
|
QMessageBox.information(window, "", msg)
|
||||||
|
|
||||||
def ask_yes_no(self, prompt):
|
def ask_yes_no(self, prompt: str) -> bool:
|
||||||
return self.confirm("", prompt)
|
return self.confirm("", prompt)
|
||||||
|
|
||||||
def create_results_window(self):
|
def create_results_window(self) -> None:
|
||||||
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``."""
|
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``."""
|
||||||
if self.details_dialog is not None:
|
if self.details_dialog is not None:
|
||||||
# The object is not deleted entirely, avoid saving its geometry in the future
|
# The object is not deleted entirely, avoid saving its geometry in the future
|
||||||
# self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)
|
# self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)
|
||||||
# or simply delete it on close which is probably cleaner:
|
# or simply delete it on close which is probably cleaner:
|
||||||
self.details_dialog.setAttribute(Qt.WA_DeleteOnClose)
|
self.details_dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
self.details_dialog.close()
|
self.details_dialog.close()
|
||||||
# if we don't do the following, Qt will crash when we recreate the Results dialog
|
# if we don't do the following, Qt will crash when we recreate the Results dialog
|
||||||
self.details_dialog.setParent(None)
|
self.details_dialog.setParent(None)
|
||||||
@@ -429,17 +433,17 @@ class DupeGuru(QObject):
|
|||||||
self.directories_dialog._updateActionsState()
|
self.directories_dialog._updateActionsState()
|
||||||
self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self)
|
self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self)
|
||||||
|
|
||||||
def show_results_window(self):
|
def show_results_window(self) -> None:
|
||||||
self.showResultsWindow()
|
self.showResultsWindow()
|
||||||
|
|
||||||
def show_problem_dialog(self):
|
def show_problem_dialog(self) -> None:
|
||||||
self.problemDialog.show()
|
self.problemDialog.show()
|
||||||
|
|
||||||
def select_dest_folder(self, prompt):
|
def select_dest_folder(self, prompt: str) -> str:
|
||||||
flags = QFileDialog.ShowDirsOnly
|
flags = QFileDialog.Option.ShowDirsOnly
|
||||||
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, "", flags)
|
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, "", flags)
|
||||||
|
|
||||||
def select_dest_file(self, prompt, extension):
|
def select_dest_file(self, prompt: str, extension: str) -> str:
|
||||||
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
||||||
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
|
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
|
||||||
if not destination.endswith(f".{extension}"):
|
if not destination.endswith(f".{extension}"):
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication, QSize
|
from PyQt6.QtCore import Qt, QCoreApplication, QSize
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@@ -30,7 +30,7 @@ tr = trget("ui")
|
|||||||
|
|
||||||
class ErrorReportDialog(QDialog):
|
class ErrorReportDialog(QDialog):
|
||||||
def __init__(self, parent, github_url, error, **kwargs):
|
def __init__(self, parent, github_url, error, **kwargs):
|
||||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
|
||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
name = QCoreApplication.applicationName()
|
name = QCoreApplication.applicationName()
|
||||||
@@ -40,23 +40,23 @@ class ErrorReportDialog(QDialog):
|
|||||||
)
|
)
|
||||||
# Under windows, we end up with an error report without linesep if we don't mangle it
|
# Under windows, we end up with an error report without linesep if we don't mangle it
|
||||||
error_text = error_text.replace("\n", os.linesep)
|
error_text = error_text.replace("\n", os.linesep)
|
||||||
self.errorTextEdit.setPlainText(error_text)
|
self.error_text_edit.setPlainText(error_text)
|
||||||
self.github_url = github_url
|
self.github_url = github_url
|
||||||
|
|
||||||
self.sendButton.clicked.connect(self.goToGithub)
|
|
||||||
self.dontSendButton.clicked.connect(self.reject)
|
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Error Report"))
|
self.setWindowTitle(tr("Error Report"))
|
||||||
self.resize(553, 349)
|
self.resize(553, 349)
|
||||||
self.verticalLayout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
self.label = QLabel(self)
|
|
||||||
self.label.setText(tr("Something went wrong. How about reporting the error?"))
|
title_label = QLabel(self)
|
||||||
self.label.setWordWrap(True)
|
title_label.setText(tr("Something went wrong. How about reporting the error?"))
|
||||||
self.verticalLayout.addWidget(self.label)
|
title_label.setWordWrap(True)
|
||||||
self.errorTextEdit = QPlainTextEdit(self)
|
main_layout.addWidget(title_label)
|
||||||
self.errorTextEdit.setReadOnly(True)
|
|
||||||
self.verticalLayout.addWidget(self.errorTextEdit)
|
self.error_text_edit = QPlainTextEdit(self)
|
||||||
|
self.error_text_edit.setReadOnly(True)
|
||||||
|
main_layout.addWidget(self.error_text_edit)
|
||||||
|
|
||||||
msg = tr(
|
msg = tr(
|
||||||
"Error reports should be reported as Github issues. You can copy the error traceback "
|
"Error reports should be reported as Github issues. You can copy the error traceback "
|
||||||
"above and paste it in a new issue.\n\nPlease make sure to run a search for any already "
|
"above and paste it in a new issue.\n\nPlease make sure to run a search for any already "
|
||||||
@@ -67,21 +67,28 @@ class ErrorReportDialog(QDialog):
|
|||||||
"Although the application should continue to run after this error, it may be in an "
|
"Although the application should continue to run after this error, it may be in an "
|
||||||
"unstable state, so it is recommended that you restart the application."
|
"unstable state, so it is recommended that you restart the application."
|
||||||
)
|
)
|
||||||
self.label2 = QLabel(msg)
|
instructions_label = QLabel(msg)
|
||||||
self.label2.setWordWrap(True)
|
instructions_label.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.label2)
|
main_layout.addWidget(instructions_label)
|
||||||
self.horizontalLayout = QHBoxLayout()
|
|
||||||
self.horizontalLayout.addItem(horizontal_spacer())
|
button_layout = QHBoxLayout()
|
||||||
self.dontSendButton = QPushButton(self)
|
button_layout.addItem(horizontal_spacer())
|
||||||
self.dontSendButton.setText(tr("Close"))
|
|
||||||
self.dontSendButton.setMinimumSize(QSize(110, 0))
|
close_button = QPushButton(self)
|
||||||
self.horizontalLayout.addWidget(self.dontSendButton)
|
close_button.setText(tr("Close"))
|
||||||
self.sendButton = QPushButton(self)
|
close_button.setMinimumSize(QSize(110, 0))
|
||||||
self.sendButton.setText(tr("Go to Github"))
|
button_layout.addWidget(close_button)
|
||||||
self.sendButton.setMinimumSize(QSize(110, 0))
|
|
||||||
self.sendButton.setDefault(True)
|
report_button = QPushButton(self)
|
||||||
self.horizontalLayout.addWidget(self.sendButton)
|
report_button.setText(tr("Go to Github"))
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
report_button.setMinimumSize(QSize(110, 0))
|
||||||
|
report_button.setDefault(True)
|
||||||
|
button_layout.addWidget(report_button)
|
||||||
|
|
||||||
|
main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
report_button.clicked.connect(self.goToGithub)
|
||||||
|
close_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
def goToGithub(self):
|
def goToGithub(self):
|
||||||
open_url(self.github_url)
|
open_url(self.github_url)
|
||||||
@@ -91,6 +98,6 @@ def install_excepthook(github_url):
|
|||||||
def my_excepthook(exctype, value, tb):
|
def my_excepthook(exctype, value, tb):
|
||||||
s = "".join(traceback.format_exception(exctype, value, tb))
|
s = "".join(traceback.format_exception(exctype, value, tb))
|
||||||
dialog = ErrorReportDialog(None, github_url, s)
|
dialog = ErrorReportDialog(None, github_url, s)
|
||||||
dialog.exec_()
|
dialog.exec()
|
||||||
|
|
||||||
sys.excepthook = my_excepthook
|
sys.excepthook = my_excepthook
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt6.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import QAbstractItemView
|
from PyQt6.QtWidgets import QAbstractItemView
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qt.details_dialog import DetailsDialog as DetailsDialogBase
|
from qt.details_dialog import DetailsDialog as DetailsDialogBase
|
||||||
@@ -15,12 +15,12 @@ tr = trget("ui")
|
|||||||
|
|
||||||
|
|
||||||
class DetailsDialog(DetailsDialogBase):
|
class DetailsDialog(DetailsDialogBase):
|
||||||
def _setupUi(self):
|
def _setupUi(self) -> None:
|
||||||
self.setWindowTitle(tr("Details"))
|
self.setWindowTitle(tr("Details"))
|
||||||
self.resize(502, 295)
|
self.resize(502, 295)
|
||||||
self.setMinimumSize(QSize(250, 250))
|
self.setMinimumSize(QSize(250, 250))
|
||||||
self.tableView = DetailsTable(self)
|
self.tableView = DetailsTable(self)
|
||||||
self.tableView.setAlternatingRowColors(True)
|
self.tableView.setAlternatingRowColors(True)
|
||||||
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.tableView.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
self.tableView.setShowGrid(False)
|
self.tableView.setShowGrid(False)
|
||||||
self.setWidget(self.tableView)
|
self.setWidget(self.tableView)
|
||||||
|
|||||||
@@ -4,27 +4,22 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import QSize
|
from typing import Callable
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtCore import QSize
|
||||||
QVBoxLayout,
|
from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QSizePolicy,
|
|
||||||
QSpacerItem,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from core.app import AppMode
|
from core.app import AppMode
|
||||||
from core.scanner import ScanType
|
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")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self) -> None:
|
||||||
self._setupFilterHardnessBox()
|
self._setupFilterHardnessBox()
|
||||||
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
||||||
self.widget = QWidget(self)
|
self.widget = QWidget(self)
|
||||||
@@ -37,7 +32,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.verticalLayout_4.addWidget(self.label_6)
|
self.verticalLayout_4.addWidget(self.label_6)
|
||||||
self.horizontalLayout_2 = QHBoxLayout()
|
self.horizontalLayout_2 = QHBoxLayout()
|
||||||
self.horizontalLayout_2.setSpacing(0)
|
self.horizontalLayout_2.setSpacing(0)
|
||||||
spacer_item = QSpacerItem(15, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)
|
spacer_item = QSpacerItem(15, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
|
||||||
self.horizontalLayout_2.addItem(spacer_item)
|
self.horizontalLayout_2.addItem(spacer_item)
|
||||||
self._setupAddCheckbox("tagTrackBox", tr("Track"), self.widget)
|
self._setupAddCheckbox("tagTrackBox", tr("Track"), self.widget)
|
||||||
self.horizontalLayout_2.addWidget(self.tagTrackBox)
|
self.horizontalLayout_2.addWidget(self.tagTrackBox)
|
||||||
@@ -70,7 +65,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
|
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
|
||||||
self._setupBottomPart()
|
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.tagTrackBox, prefs.scan_tag_track)
|
||||||
setchecked(self.tagArtistBox, prefs.scan_tag_artist)
|
setchecked(self.tagArtistBox, prefs.scan_tag_artist)
|
||||||
setchecked(self.tagAlbumBox, prefs.scan_tag_album)
|
setchecked(self.tagAlbumBox, prefs.scan_tag_album)
|
||||||
@@ -99,7 +94,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.tagGenreBox.setEnabled(tag_based)
|
self.tagGenreBox.setEnabled(tag_based)
|
||||||
self.tagYearBox.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_track = ischecked(self.tagTrackBox)
|
||||||
prefs.scan_tag_artist = ischecked(self.tagArtistBox)
|
prefs.scan_tag_artist = ischecked(self.tagArtistBox)
|
||||||
prefs.scan_tag_album = ischecked(self.tagAlbumBox)
|
prefs.scan_tag_album = ischecked(self.tagAlbumBox)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from typing import Tuple, List, Union
|
from typing import Tuple, List, Union
|
||||||
from PyQt5.QtGui import QImage
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
_block = Tuple[int, int, int]
|
_block = Tuple[int, int, int]
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,23 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QFormLayout
|
from typing import Callable
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtWidgets import QFormLayout, QCheckBox
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon.plat import ISLINUX
|
from hscommon.plat import ISLINUX
|
||||||
|
from qt.preferences import Preferences
|
||||||
from qt.radio_box import RadioBox
|
from qt.radio_box import RadioBox
|
||||||
from core.scanner import ScanType
|
from core.scanner import ScanType
|
||||||
from core.app import AppMode
|
from core.app import AppMode
|
||||||
|
|
||||||
from qt.preferences_dialog import PreferencesDialogBase
|
from qt.preferences_dialog import PreferencesDialogBase, Sections
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self) -> None:
|
||||||
self._setupFilterHardnessBox()
|
self._setupFilterHardnessBox()
|
||||||
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
||||||
self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
|
self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
|
||||||
@@ -37,12 +39,12 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
|
|
||||||
self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False)
|
self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False)
|
||||||
cache_form = QFormLayout()
|
cache_form = QFormLayout()
|
||||||
cache_form.setLabelAlignment(Qt.AlignLeft)
|
cache_form.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||||
cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio)
|
cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio)
|
||||||
self.widgetsVLayout.addLayout(cache_form)
|
self.widgetsVLayout.addLayout(cache_form)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _setupDisplayPage(self):
|
def _setupDisplayPage(self) -> None:
|
||||||
super()._setupDisplayPage()
|
super()._setupDisplayPage()
|
||||||
self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
|
self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
|
||||||
self.details_dialog_override_theme_icons.setToolTip(
|
self.details_dialog_override_theme_icons.setToolTip(
|
||||||
@@ -62,7 +64,7 @@ show scrollbars to span the view around"
|
|||||||
)
|
)
|
||||||
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
|
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked, section):
|
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
|
||||||
setchecked(self.matchScaledBox, prefs.match_scaled)
|
setchecked(self.matchScaledBox, prefs.match_scaled)
|
||||||
self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
|
self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ show scrollbars to span the view around"
|
|||||||
setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)
|
setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)
|
||||||
setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
|
setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
|
||||||
prefs.match_scaled = ischecked(self.matchScaledBox)
|
prefs.match_scaled = ischecked(self.matchScaledBox)
|
||||||
prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
|
prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
|
||||||
prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)
|
prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication, QDockWidget
|
from typing import Any, Tuple
|
||||||
from PyQt5.QtCore import Qt, QRect, QObject, pyqtSignal
|
from PyQt6.QtWidgets import QApplication, QDockWidget
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt6.QtCore import Qt, QRect, QObject, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QColor
|
||||||
|
|
||||||
from hscommon import trans
|
from hscommon import trans
|
||||||
from hscommon.plat import ISLINUX
|
from hscommon.plat import ISLINUX
|
||||||
@@ -126,7 +127,7 @@ class PreferencesBase(QObject):
|
|||||||
def set_value(self, name, value):
|
def set_value(self, name, value):
|
||||||
self._settings.setValue(name, _normalize_for_serialization(value))
|
self._settings.setValue(name, _normalize_for_serialization(value))
|
||||||
|
|
||||||
def saveGeometry(self, name, widget):
|
def saveGeometry(self, name, widget) -> None:
|
||||||
# We save geometry under a 7-sized int array: first item is a flag
|
# We save geometry under a 7-sized int array: first item is a flag
|
||||||
# for whether the widget is maximized, second item is a flag for whether
|
# for whether the widget is maximized, second item is a flag for whether
|
||||||
# the widget is docked, third item is a Qt::DockWidgetArea enum value,
|
# the widget is docked, third item is a Qt::DockWidgetArea enum value,
|
||||||
@@ -138,12 +139,12 @@ class PreferencesBase(QObject):
|
|||||||
rect_as_list = [r.x(), r.y(), r.width(), r.height()]
|
rect_as_list = [r.x(), r.y(), r.width(), r.height()]
|
||||||
self.set_value(name, [m, d, area] + rect_as_list)
|
self.set_value(name, [m, d, area] + rect_as_list)
|
||||||
|
|
||||||
def restoreGeometry(self, name, widget):
|
def restoreGeometry(self, name, widget) -> Tuple[bool, Any]:
|
||||||
geometry = self.get_value(name)
|
geometry = self.get_value(name)
|
||||||
if geometry and len(geometry) == 7:
|
if geometry and len(geometry) == 7:
|
||||||
m, d, area, x, y, w, h = geometry
|
m, d, area, x, y, w, h = geometry
|
||||||
if m:
|
if m:
|
||||||
widget.setWindowState(Qt.WindowMaximized)
|
widget.setWindowState(Qt.WindowState.WindowMaximized)
|
||||||
else:
|
else:
|
||||||
r = QRect(x, y, w, h)
|
r = QRect(x, y, w, h)
|
||||||
widget.setGeometry(r)
|
widget.setGeometry(r)
|
||||||
@@ -154,7 +155,7 @@ class PreferencesBase(QObject):
|
|||||||
|
|
||||||
|
|
||||||
class Preferences(PreferencesBase):
|
class Preferences(PreferencesBase):
|
||||||
def _load_values(self, settings):
|
def _load_values(self, settings) -> None:
|
||||||
get = self.get_value
|
get = self.get_value
|
||||||
self.filter_hardness = get("FilterHardness", self.filter_hardness)
|
self.filter_hardness = get("FilterHardness", self.filter_hardness)
|
||||||
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
|
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
|
||||||
@@ -225,7 +226,7 @@ class Preferences(PreferencesBase):
|
|||||||
self.match_scaled = get("MatchScaled", self.match_scaled)
|
self.match_scaled = get("MatchScaled", self.match_scaled)
|
||||||
self.picture_cache_type = get("PictureCacheType", self.picture_cache_type)
|
self.picture_cache_type = get("PictureCacheType", self.picture_cache_type)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self) -> None:
|
||||||
self.filter_hardness = 95
|
self.filter_hardness = 95
|
||||||
self.mix_file_kind = True
|
self.mix_file_kind = True
|
||||||
self.use_regexp = False
|
self.use_regexp = False
|
||||||
@@ -247,8 +248,8 @@ class Preferences(PreferencesBase):
|
|||||||
# By default use internal icons on platforms other than Linux for now
|
# By default use internal icons on platforms other than Linux for now
|
||||||
self.details_dialog_override_theme_icons = False if not ISLINUX else True
|
self.details_dialog_override_theme_icons = False if not ISLINUX else True
|
||||||
self.details_dialog_viewers_show_scrollbars = True
|
self.details_dialog_viewers_show_scrollbars = True
|
||||||
self.result_table_ref_foreground_color = QColor(Qt.blue)
|
self.result_table_ref_foreground_color = QColor(Qt.GlobalColor.blue)
|
||||||
self.result_table_ref_background_color = QColor(Qt.lightGray)
|
self.result_table_ref_background_color = QColor(Qt.GlobalColor.lightGray)
|
||||||
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
|
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
|
||||||
self.resultWindowIsMaximized = False
|
self.resultWindowIsMaximized = False
|
||||||
self.resultWindowRect = None
|
self.resultWindowRect = None
|
||||||
@@ -276,7 +277,7 @@ class Preferences(PreferencesBase):
|
|||||||
self.match_scaled = False
|
self.match_scaled = False
|
||||||
self.picture_cache_type = "sqlite"
|
self.picture_cache_type = "sqlite"
|
||||||
|
|
||||||
def _save_values(self, settings):
|
def _save_values(self, settings) -> None:
|
||||||
set_ = self.set_value
|
set_ = self.set_value
|
||||||
set_("FilterHardness", self.filter_hardness)
|
set_("FilterHardness", self.filter_hardness)
|
||||||
set_("MixFileKind", self.mix_file_kind)
|
set_("MixFileKind", self.mix_file_kind)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
from typing import Union
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtCore import Qt, QSize, pyqtSlot
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -28,7 +29,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QGroupBox,
|
QGroupBox,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
)
|
)
|
||||||
from PyQt5.QtGui import QPixmap, QIcon
|
from PyQt6.QtGui import QPixmap, QIcon, QShowEvent
|
||||||
from hscommon import desktop, plat
|
from hscommon import desktop, plat
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@@ -39,6 +40,11 @@ from enum import Flag, auto
|
|||||||
|
|
||||||
from qt.preferences import Preferences
|
from qt.preferences import Preferences
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from qt.app import DupeGuru
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
@@ -52,8 +58,8 @@ class Sections(Flag):
|
|||||||
|
|
||||||
|
|
||||||
class PreferencesDialogBase(QDialog):
|
class PreferencesDialogBase(QDialog):
|
||||||
def __init__(self, parent, app, **kwargs):
|
def __init__(self, parent: QWidget, app: "DupeGuru", **kwargs):
|
||||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
|
||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
|
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
|
||||||
@@ -65,7 +71,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupFilterHardnessBox(self):
|
def _setupFilterHardnessBox(self) -> None:
|
||||||
self.filterHardnessHLayout = QHBoxLayout()
|
self.filterHardnessHLayout = QHBoxLayout()
|
||||||
self.filterHardnessLabel = QLabel(self)
|
self.filterHardnessLabel = QLabel(self)
|
||||||
self.filterHardnessLabel.setText(tr("Filter Hardness:"))
|
self.filterHardnessLabel.setText(tr("Filter Hardness:"))
|
||||||
@@ -76,7 +82,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.filterHardnessHLayoutSub1 = QHBoxLayout()
|
self.filterHardnessHLayoutSub1 = QHBoxLayout()
|
||||||
self.filterHardnessHLayoutSub1.setSpacing(12)
|
self.filterHardnessHLayoutSub1.setSpacing(12)
|
||||||
self.filterHardnessSlider = QSlider(self)
|
self.filterHardnessSlider = QSlider(self)
|
||||||
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
size_policy.setHorizontalStretch(0)
|
size_policy.setHorizontalStretch(0)
|
||||||
size_policy.setVerticalStretch(0)
|
size_policy.setVerticalStretch(0)
|
||||||
size_policy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth())
|
size_policy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth())
|
||||||
@@ -84,7 +90,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.filterHardnessSlider.setMinimum(1)
|
self.filterHardnessSlider.setMinimum(1)
|
||||||
self.filterHardnessSlider.setMaximum(100)
|
self.filterHardnessSlider.setMaximum(100)
|
||||||
self.filterHardnessSlider.setTracking(True)
|
self.filterHardnessSlider.setTracking(True)
|
||||||
self.filterHardnessSlider.setOrientation(Qt.Horizontal)
|
self.filterHardnessSlider.setOrientation(Qt.Orientation.Horizontal)
|
||||||
self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)
|
self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)
|
||||||
self.filterHardnessLabel = QLabel(self)
|
self.filterHardnessLabel = QLabel(self)
|
||||||
self.filterHardnessLabel.setText("100")
|
self.filterHardnessLabel.setText("100")
|
||||||
@@ -96,7 +102,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.moreResultsLabel = QLabel(self)
|
self.moreResultsLabel = QLabel(self)
|
||||||
self.moreResultsLabel.setText(tr("More Results"))
|
self.moreResultsLabel.setText(tr("More Results"))
|
||||||
self.filterHardnessHLayoutSub2.addWidget(self.moreResultsLabel)
|
self.filterHardnessHLayoutSub2.addWidget(self.moreResultsLabel)
|
||||||
spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
self.filterHardnessHLayoutSub2.addItem(spacer_item)
|
self.filterHardnessHLayoutSub2.addItem(spacer_item)
|
||||||
self.fewerResultsLabel = QLabel(self)
|
self.fewerResultsLabel = QLabel(self)
|
||||||
self.fewerResultsLabel.setText(tr("Fewer Results"))
|
self.fewerResultsLabel.setText(tr("Fewer Results"))
|
||||||
@@ -104,7 +110,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
||||||
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
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.
|
# The bottom part of the pref panel is always the same in all editions.
|
||||||
self.copyMoveLabel = QLabel(self)
|
self.copyMoveLabel = QLabel(self)
|
||||||
self.copyMoveLabel.setText(tr("Copy and Move:"))
|
self.copyMoveLabel.setText(tr("Copy and Move:"))
|
||||||
@@ -120,7 +126,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.customCommandEdit = QLineEdit(self)
|
self.customCommandEdit = QLineEdit(self)
|
||||||
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
||||||
|
|
||||||
def _setupDisplayPage(self):
|
def _setupDisplayPage(self) -> None:
|
||||||
self.ui_groupbox = QGroupBox("&" + tr("General Interface"))
|
self.ui_groupbox = QGroupBox("&" + tr("General Interface"))
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.languageLabel = QLabel(tr("Language:"), self)
|
self.languageLabel = QLabel(tr("Language:"), self)
|
||||||
@@ -171,7 +177,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)
|
formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color)
|
||||||
self.result_table_delta_foreground_color = ColorPickerButton(self)
|
self.result_table_delta_foreground_color = ColorPickerButton(self)
|
||||||
formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color)
|
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
|
# Keep same vertical spacing as parent layout for consistency
|
||||||
formlayout.setVerticalSpacing(self.displayVLayout.spacing())
|
formlayout.setVerticalSpacing(self.displayVLayout.spacing())
|
||||||
@@ -213,7 +219,7 @@ use the modifier key to drag the floating window around"
|
|||||||
details_groupbox.setLayout(self.details_groupbox_layout)
|
details_groupbox.setLayout(self.details_groupbox_layout)
|
||||||
self.displayVLayout.addWidget(details_groupbox)
|
self.displayVLayout.addWidget(details_groupbox)
|
||||||
|
|
||||||
def _setupDebugPage(self):
|
def _setupDebugPage(self) -> None:
|
||||||
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
|
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
|
||||||
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
|
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
|
||||||
self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization."))
|
self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization."))
|
||||||
@@ -225,7 +231,7 @@ use the modifier key to drag the floating window around"
|
|||||||
)
|
)
|
||||||
self.debugVLayout.addWidget(self.debug_location_label)
|
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:
|
if parent is None:
|
||||||
parent = self
|
parent = self
|
||||||
cb = QCheckBox(parent)
|
cb = QCheckBox(parent)
|
||||||
@@ -236,7 +242,7 @@ use the modifier key to drag the floating window around"
|
|||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self) -> None:
|
||||||
self.setWindowTitle(tr("Options"))
|
self.setWindowTitle(tr("Options"))
|
||||||
self.setSizeGripEnabled(False)
|
self.setSizeGripEnabled(False)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
@@ -258,11 +264,13 @@ use the modifier key to drag the floating window around"
|
|||||||
# self.mainVLayout.addLayout(self.widgetsVLayout)
|
# self.mainVLayout.addLayout(self.widgetsVLayout)
|
||||||
self.buttonBox = QDialogButtonBox(self)
|
self.buttonBox = QDialogButtonBox(self)
|
||||||
self.buttonBox.setStandardButtons(
|
self.buttonBox.setStandardButtons(
|
||||||
QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.RestoreDefaults
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
| QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.RestoreDefaults
|
||||||
)
|
)
|
||||||
self.mainVLayout.addWidget(self.tabwidget)
|
self.mainVLayout.addWidget(self.tabwidget)
|
||||||
self.mainVLayout.addWidget(self.buttonBox)
|
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_general, tr("General"))
|
||||||
self.tabwidget.addTab(self.page_display, tr("Display"))
|
self.tabwidget.addTab(self.page_display, tr("Display"))
|
||||||
self.tabwidget.addTab(self.page_debug, tr("Debug"))
|
self.tabwidget.addTab(self.page_debug, tr("Debug"))
|
||||||
@@ -270,20 +278,20 @@ use the modifier key to drag the floating window around"
|
|||||||
self.widgetsVLayout.addStretch(0)
|
self.widgetsVLayout.addStretch(0)
|
||||||
self.debugVLayout.addStretch(0)
|
self.debugVLayout.addStretch(0)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked, section):
|
def _load(self, prefs, setchecked, section) -> None:
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked) -> None:
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def load(self, prefs=None, section=Sections.ALL):
|
def load(self, prefs: Preferences = None, section: Sections = Sections.ALL) -> None:
|
||||||
if prefs is None:
|
if prefs is None:
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
|
|
||||||
def setchecked(cb, b):
|
def setchecked(cb: QCheckBox, b: bool) -> None:
|
||||||
cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
|
cb.setCheckState(Qt.CheckState.Checked if b else Qt.CheckState.Unchecked)
|
||||||
|
|
||||||
if section & Sections.GENERAL:
|
if section & Sections.GENERAL:
|
||||||
self.filterHardnessSlider.setValue(prefs.filter_hardness)
|
self.filterHardnessSlider.setValue(prefs.filter_hardness)
|
||||||
@@ -323,12 +331,12 @@ use the modifier key to drag the floating window around"
|
|||||||
setchecked(self.profile_scan_box, prefs.profile_scan)
|
setchecked(self.profile_scan_box, prefs.profile_scan)
|
||||||
self._load(prefs, setchecked, section)
|
self._load(prefs, setchecked, section)
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> None:
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
prefs.filter_hardness = self.filterHardnessSlider.value()
|
prefs.filter_hardness = self.filterHardnessSlider.value()
|
||||||
|
|
||||||
def ischecked(cb):
|
def ischecked(cb: QCheckBox) -> bool:
|
||||||
return cb.checkState() == Qt.Checked
|
return cb.checkState() == Qt.CheckState.Checked
|
||||||
|
|
||||||
prefs.mix_file_kind = ischecked(self.mixFileKindBox)
|
prefs.mix_file_kind = ischecked(self.mixFileKindBox)
|
||||||
prefs.use_regexp = ischecked(self.useRegexpBox)
|
prefs.use_regexp = ischecked(self.useRegexpBox)
|
||||||
@@ -363,13 +371,13 @@ use the modifier key to drag the floating window around"
|
|||||||
self.app.prefs.language = lang_code
|
self.app.prefs.language = lang_code
|
||||||
self._save(prefs, ischecked)
|
self._save(prefs, ischecked)
|
||||||
|
|
||||||
def resetToDefaults(self, section_to_update):
|
def resetToDefaults(self, section_to_update: Sections) -> None:
|
||||||
self.load(Preferences(), section_to_update)
|
self.load(Preferences(), section_to_update)
|
||||||
|
|
||||||
# --- Events
|
# --- Events
|
||||||
def buttonClicked(self, button):
|
def buttonClicked(self, button: QPushButton) -> None:
|
||||||
role = self.buttonBox.buttonRole(button)
|
role = self.buttonBox.buttonRole(button)
|
||||||
if role == QDialogButtonBox.ResetRole:
|
if role == QDialogButtonBox.ButtonRole.ResetRole:
|
||||||
current_tab = self.tabwidget.currentWidget()
|
current_tab = self.tabwidget.currentWidget()
|
||||||
section_to_update = Sections.ALL
|
section_to_update = Sections.ALL
|
||||||
if current_tab is self.page_general:
|
if current_tab is self.page_general:
|
||||||
@@ -380,30 +388,31 @@ use the modifier key to drag the floating window around"
|
|||||||
section_to_update = Sections.DEBUG
|
section_to_update = Sections.DEBUG
|
||||||
self.resetToDefaults(section_to_update)
|
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
|
# have to do this here as the frameGeometry is not correct until shown
|
||||||
move_to_screen_center(self)
|
move_to_screen_center(self)
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
|
|
||||||
|
|
||||||
class ColorPickerButton(QPushButton):
|
class ColorPickerButton(QPushButton):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent: QWidget) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.parent = parent
|
|
||||||
self.color = None
|
self.color = None
|
||||||
self.clicked.connect(self.onClicked)
|
self.clicked.connect(self.onClicked)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def onClicked(self):
|
def onClicked(self) -> None:
|
||||||
color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent)
|
color = QColorDialog.getColor(
|
||||||
|
self.color if self.color is not None else Qt.GlobalColor.white, self.parentWidget()
|
||||||
|
)
|
||||||
self.setColor(color)
|
self.setColor(color)
|
||||||
|
|
||||||
def setColor(self, color):
|
def setColor(self, color) -> None:
|
||||||
size = QSize(16, 16)
|
size = QSize(16, 16)
|
||||||
px = QPixmap(size)
|
px = QPixmap(size)
|
||||||
if color is None:
|
if color is None:
|
||||||
size.width = 0
|
size.setWidth(0)
|
||||||
size.height = 0
|
size.setHeight(0)
|
||||||
elif not color.isValid():
|
elif not color.isValid():
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtGui import QShowEvent
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@@ -17,8 +18,10 @@ from PyQt5.QtWidgets import (
|
|||||||
QLabel,
|
QLabel,
|
||||||
QTableView,
|
QTableView,
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from core.gui.problem_dialog import ProblemDialog as ProblemDiaglogModel
|
||||||
from qt.util import move_to_screen_center
|
from qt.util import move_to_screen_center
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qt.problem_table import ProblemTable
|
from qt.problem_table import ProblemTable
|
||||||
@@ -27,52 +30,56 @@ tr = trget("ui")
|
|||||||
|
|
||||||
|
|
||||||
class ProblemDialog(QDialog):
|
class ProblemDialog(QDialog):
|
||||||
def __init__(self, parent, model, **kwargs):
|
def __init__(self, parent: QWidget, model: ProblemDiaglogModel, **kwargs) -> None:
|
||||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
|
||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self._setupUi()
|
|
||||||
self.model = model
|
self.model = model
|
||||||
self.model.view = self
|
self.table_view = QTableView(self)
|
||||||
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
|
self.table = ProblemTable(self.model.problem_table, view=self.table_view)
|
||||||
|
self._setupUi()
|
||||||
|
|
||||||
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
|
def _setupUi(self) -> None:
|
||||||
self.closeButton.clicked.connect(self.accept)
|
|
||||||
|
|
||||||
def _setupUi(self):
|
|
||||||
self.setWindowTitle(tr("Problems!"))
|
self.setWindowTitle(tr("Problems!"))
|
||||||
self.resize(413, 323)
|
self.resize(413, 323)
|
||||||
self.verticalLayout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
self.label = QLabel(self)
|
notice_label = QLabel(self)
|
||||||
msg = tr(
|
msg = tr(
|
||||||
"There were problems processing some (or all) of the files. The cause of "
|
"There were problems processing some (or all) of the files. The cause of "
|
||||||
"these problems are described in the table below. Those files were not "
|
"these problems are described in the table below. Those files were not "
|
||||||
"removed from your results."
|
"removed from your results."
|
||||||
)
|
)
|
||||||
self.label.setText(msg)
|
notice_label.setText(msg)
|
||||||
self.label.setWordWrap(True)
|
notice_label.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.label)
|
main_layout.addWidget(notice_label)
|
||||||
self.tableView = QTableView(self)
|
|
||||||
self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
||||||
self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
||||||
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
||||||
self.tableView.setShowGrid(False)
|
|
||||||
self.tableView.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.tableView.verticalHeader().setDefaultSectionSize(18)
|
|
||||||
self.tableView.verticalHeader().setHighlightSections(False)
|
|
||||||
self.verticalLayout.addWidget(self.tableView)
|
|
||||||
self.horizontalLayout = QHBoxLayout()
|
|
||||||
self.revealButton = QPushButton(self)
|
|
||||||
self.revealButton.setText(tr("Reveal Selected"))
|
|
||||||
self.horizontalLayout.addWidget(self.revealButton)
|
|
||||||
spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
||||||
self.horizontalLayout.addItem(spacer_item)
|
|
||||||
self.closeButton = QPushButton(self)
|
|
||||||
self.closeButton.setText(tr("Close"))
|
|
||||||
self.closeButton.setDefault(True)
|
|
||||||
self.horizontalLayout.addWidget(self.closeButton)
|
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
|
||||||
|
|
||||||
def showEvent(self, event):
|
self.table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.table_view.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
self.table_view.setShowGrid(False)
|
||||||
|
self.table_view.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.table_view.verticalHeader().setDefaultSectionSize(18)
|
||||||
|
self.table_view.verticalHeader().setHighlightSections(False)
|
||||||
|
main_layout.addWidget(self.table_view)
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
reveal_button = QPushButton(self)
|
||||||
|
reveal_button.setText(tr("Reveal Selected"))
|
||||||
|
button_layout.addWidget(reveal_button)
|
||||||
|
|
||||||
|
spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
button_layout.addItem(spacer_item)
|
||||||
|
|
||||||
|
close_button = QPushButton(self)
|
||||||
|
close_button.setText(tr("Close"))
|
||||||
|
close_button.setDefault(True)
|
||||||
|
button_layout.addWidget(close_button)
|
||||||
|
|
||||||
|
main_layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
reveal_button.clicked.connect(self.model.reveal_selected_dupe)
|
||||||
|
close_button.clicked.connect(self.accept)
|
||||||
|
|
||||||
|
def showEvent(self, event: QShowEvent) -> None:
|
||||||
# have to do this here as the frameGeometry is not correct until shown
|
# have to do this here as the frameGeometry is not correct until shown
|
||||||
move_to_screen_center(self)
|
move_to_screen_center(self)
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
|
|||||||
@@ -4,29 +4,23 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import QSize
|
from typing import Callable
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtCore import QSize
|
||||||
QSpinBox,
|
from PyQt6.QtWidgets import QSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QSizePolicy,
|
|
||||||
QSpacerItem,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
from core.app import AppMode
|
from core.app import AppMode
|
||||||
from core.scanner import ScanType
|
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")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self) -> None:
|
||||||
self._setupFilterHardnessBox()
|
self._setupFilterHardnessBox()
|
||||||
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
||||||
self.widget = QWidget(self)
|
self.widget = QWidget(self)
|
||||||
@@ -50,7 +44,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
|
self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
|
||||||
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
|
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
|
||||||
self.sizeThresholdSpinBox = QSpinBox(self.widget)
|
self.sizeThresholdSpinBox = QSpinBox(self.widget)
|
||||||
size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
size_policy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
|
||||||
size_policy.setHorizontalStretch(0)
|
size_policy.setHorizontalStretch(0)
|
||||||
size_policy.setVerticalStretch(0)
|
size_policy.setVerticalStretch(0)
|
||||||
size_policy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
|
size_policy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
|
||||||
@@ -61,14 +55,14 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.label_6 = QLabel(self.widget)
|
self.label_6 = QLabel(self.widget)
|
||||||
self.label_6.setText(tr("KB"))
|
self.label_6.setText(tr("KB"))
|
||||||
self.horizontalLayout_2.addWidget(self.label_6)
|
self.horizontalLayout_2.addWidget(self.label_6)
|
||||||
spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
self.horizontalLayout_2.addItem(spacer_item1)
|
self.horizontalLayout_2.addItem(spacer_item1)
|
||||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
||||||
self.horizontalLayout_2a = QHBoxLayout()
|
self.horizontalLayout_2a = QHBoxLayout()
|
||||||
self._setupAddCheckbox("ignoreLargeFilesBox", tr("Ignore files larger than"), self.widget)
|
self._setupAddCheckbox("ignoreLargeFilesBox", tr("Ignore files larger than"), self.widget)
|
||||||
self.horizontalLayout_2a.addWidget(self.ignoreLargeFilesBox)
|
self.horizontalLayout_2a.addWidget(self.ignoreLargeFilesBox)
|
||||||
self.sizeSaturationSpinBox = QSpinBox(self.widget)
|
self.sizeSaturationSpinBox = QSpinBox(self.widget)
|
||||||
size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
size_policy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
|
||||||
self.sizeSaturationSpinBox.setSizePolicy(size_policy)
|
self.sizeSaturationSpinBox.setSizePolicy(size_policy)
|
||||||
self.sizeSaturationSpinBox.setMaximumSize(QSize(300, 16777215))
|
self.sizeSaturationSpinBox.setMaximumSize(QSize(300, 16777215))
|
||||||
self.sizeSaturationSpinBox.setRange(0, 1000000)
|
self.sizeSaturationSpinBox.setRange(0, 1000000)
|
||||||
@@ -76,7 +70,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.label_6a = QLabel(self.widget)
|
self.label_6a = QLabel(self.widget)
|
||||||
self.label_6a.setText(tr("MB"))
|
self.label_6a.setText(tr("MB"))
|
||||||
self.horizontalLayout_2a.addWidget(self.label_6a)
|
self.horizontalLayout_2a.addWidget(self.label_6a)
|
||||||
spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
self.horizontalLayout_2a.addItem(spacer_item3)
|
self.horizontalLayout_2a.addItem(spacer_item3)
|
||||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2a)
|
self.verticalLayout_4.addLayout(self.horizontalLayout_2a)
|
||||||
self.horizontalLayout_2b = QHBoxLayout()
|
self.horizontalLayout_2b = QHBoxLayout()
|
||||||
@@ -94,7 +88,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.label_6b = QLabel(self.widget)
|
self.label_6b = QLabel(self.widget)
|
||||||
self.label_6b.setText(tr("MB"))
|
self.label_6b.setText(tr("MB"))
|
||||||
self.horizontalLayout_2b.addWidget(self.label_6b)
|
self.horizontalLayout_2b.addWidget(self.label_6b)
|
||||||
spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
self.horizontalLayout_2b.addItem(spacer_item2)
|
self.horizontalLayout_2b.addItem(spacer_item2)
|
||||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2b)
|
self.verticalLayout_4.addLayout(self.horizontalLayout_2b)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox(
|
||||||
@@ -106,7 +100,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.widgetsVLayout.addWidget(self.widget)
|
self.widgetsVLayout.addWidget(self.widget)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _load(self, prefs, setchecked, section):
|
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
|
||||||
setchecked(self.matchSimilarBox, prefs.match_similar)
|
setchecked(self.matchSimilarBox, prefs.match_similar)
|
||||||
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
||||||
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
||||||
@@ -123,7 +117,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.matchSimilarBox.setEnabled(word_based)
|
self.matchSimilarBox.setEnabled(word_based)
|
||||||
self.wordWeightingBox.setEnabled(word_based)
|
self.wordWeightingBox.setEnabled(word_based)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
|
||||||
prefs.match_similar = ischecked(self.matchSimilarBox)
|
prefs.match_similar = ischecked(self.matchSimilarBox)
|
||||||
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
||||||
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
||||||
|
|||||||
42
qt/util.py
42
qt/util.py
@@ -11,22 +11,18 @@ import io
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
from core.util import executable_folder
|
from core.util import executable_folder
|
||||||
from hscommon.util import first
|
from hscommon.util import first
|
||||||
from hscommon.plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
|
|
||||||
from PyQt5.QtCore import QStandardPaths, QSettings
|
from PyQt6.QtCore import QStandardPaths, QSettings
|
||||||
from PyQt5.QtGui import QPixmap, QIcon, QGuiApplication
|
from PyQt6.QtGui import QPixmap, QIcon, QGuiApplication, QAction
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy, QHBoxLayout, QWidget
|
||||||
QSpacerItem,
|
|
||||||
QSizePolicy,
|
|
||||||
QAction,
|
|
||||||
QHBoxLayout,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def move_to_screen_center(widget):
|
def move_to_screen_center(widget: QWidget) -> None:
|
||||||
frame = widget.frameGeometry()
|
frame = widget.frameGeometry()
|
||||||
if QGuiApplication.screenAt(frame.center()) is None:
|
if QGuiApplication.screenAt(frame.center()) is None:
|
||||||
# if center not on any screen use default screen
|
# if center not on any screen use default screen
|
||||||
@@ -43,21 +39,21 @@ def move_to_screen_center(widget):
|
|||||||
widget.move(frame.topLeft())
|
widget.move(frame.topLeft())
|
||||||
|
|
||||||
|
|
||||||
def vertical_spacer(size=None):
|
def vertical_spacer(size: Union[int, None] = None) -> QSpacerItem:
|
||||||
if size:
|
if size:
|
||||||
return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed)
|
return QSpacerItem(1, size, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
else:
|
else:
|
||||||
return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
|
return QSpacerItem(1, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding)
|
||||||
|
|
||||||
|
|
||||||
def horizontal_spacer(size=None):
|
def horizontal_spacer(size: Union[int, None] = None) -> QSpacerItem:
|
||||||
if size:
|
if size:
|
||||||
return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed)
|
return QSpacerItem(size, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
else:
|
else:
|
||||||
return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
return QSpacerItem(1, 1, QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed)
|
||||||
|
|
||||||
|
|
||||||
def horizontal_wrap(widgets):
|
def horizontal_wrap(widgets: List[Union[QWidget, int, None]]) -> QHBoxLayout:
|
||||||
"""Wrap all widgets in `widgets` in a horizontal layout.
|
"""Wrap all widgets in `widgets` in a horizontal layout.
|
||||||
|
|
||||||
If, instead of placing a widget in your list, you place an int or None, an horizontal spacer
|
If, instead of placing a widget in your list, you place an int or None, an horizontal spacer
|
||||||
@@ -77,7 +73,7 @@ def create_actions(actions, target):
|
|||||||
for name, shortcut, icon, desc, func in actions:
|
for name, shortcut, icon, desc, func in actions:
|
||||||
action = QAction(target)
|
action = QAction(target)
|
||||||
if icon:
|
if icon:
|
||||||
action.setIcon(QIcon(QPixmap(":/" + icon)))
|
action.setIcon(QIcon(QPixmap(":/" + icon))) # TODO stop using qrc file path
|
||||||
if shortcut:
|
if shortcut:
|
||||||
action.setShortcut(shortcut)
|
action.setShortcut(shortcut)
|
||||||
action.setText(desc)
|
action.setText(desc)
|
||||||
@@ -100,11 +96,11 @@ def set_accel_keys(menu):
|
|||||||
action.setText(newtext)
|
action.setText(newtext)
|
||||||
|
|
||||||
|
|
||||||
def get_appdata(portable=False):
|
def get_appdata(portable: bool = False) -> str:
|
||||||
if portable:
|
if portable:
|
||||||
return op.join(executable_folder(), "data")
|
return op.join(executable_folder(), "data")
|
||||||
else:
|
else:
|
||||||
return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0]
|
return QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)[0]
|
||||||
|
|
||||||
|
|
||||||
class SysWrapper(io.IOBase):
|
class SysWrapper(io.IOBase):
|
||||||
@@ -140,18 +136,18 @@ def escape_amp(s):
|
|||||||
return s.replace("&", "&&")
|
return s.replace("&", "&&")
|
||||||
|
|
||||||
|
|
||||||
def create_qsettings():
|
def create_qsettings() -> QSettings:
|
||||||
# Create a QSettings instance with the correct arguments.
|
# Create a QSettings instance with the correct arguments.
|
||||||
config_location = op.join(executable_folder(), "settings.ini")
|
config_location = op.join(executable_folder(), "settings.ini")
|
||||||
if op.isfile(config_location):
|
if op.isfile(config_location):
|
||||||
settings = QSettings(config_location, QSettings.IniFormat)
|
settings = QSettings(config_location, QSettings.Format.IniFormat)
|
||||||
settings.setValue("Portable", True)
|
settings.setValue("Portable", True)
|
||||||
elif ISWINDOWS:
|
elif ISWINDOWS:
|
||||||
# On windows use an ini file in the AppDataLocation instead of registry if possible as it
|
# On windows use an ini file in the AppDataLocation instead of registry if possible as it
|
||||||
# makes it easier for a user to clear it out when there are issues.
|
# makes it easier for a user to clear it out when there are issues.
|
||||||
locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)
|
locations = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)
|
||||||
if locations:
|
if locations:
|
||||||
settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat)
|
settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.Format.IniFormat)
|
||||||
else:
|
else:
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
settings.setValue("Portable", False)
|
settings.setValue("Portable", False)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pytest>=6,<7
|
pytest>=6,<7.2
|
||||||
flake8
|
flake8
|
||||||
black
|
black
|
||||||
pyinstaller>=4.5,<5.0; sys_platform != 'linux'
|
pyinstaller>=4.5,<5.0; sys_platform != 'linux'
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
distro>=1.5.0
|
distro>=1.5.0, <2.0
|
||||||
mutagen>=1.44.0
|
mutagen>=1.44.0, <2.0
|
||||||
polib>=1.1.0
|
polib>=1.1.0, <2.0
|
||||||
PyQt5 >=5.14.1,<6.0; sys_platform != 'linux'
|
PyQt6 >=6.3,<7.0; sys_platform != 'linux'
|
||||||
pywin32>=228; sys_platform == 'win32'
|
pywin32>=228; sys_platform == 'win32'
|
||||||
semantic-version>=2.0.0,<3.0.0
|
semantic-version>=2.0.0,<3.0.0
|
||||||
Send2Trash>=1.3.0
|
Send2Trash>=1.3.0
|
||||||
sphinx>=3.0.0
|
sphinx>=5.0.0, <6.0
|
||||||
xxhash>=3.0.0,<4.0.0
|
xxhash>=3.0.0,<4.0.0
|
||||||
|
|||||||
17
run.py
17
run.py
@@ -9,9 +9,9 @@ import sys
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
import gc
|
import gc
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
from PyQt6.QtCore import QDir
|
||||||
from PyQt5.QtGui import QIcon, QPixmap
|
from PyQt6.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
from hscommon.trans import install_gettext_trans_under_qt
|
from hscommon.trans import install_gettext_trans_under_qt
|
||||||
from qt.error_report_dialog import install_excepthook
|
from qt.error_report_dialog import install_excepthook
|
||||||
@@ -48,9 +48,10 @@ def setup_signals():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
QCoreApplication.setOrganizationName("Hardcoded Software")
|
QApplication.setOrganizationName("Hardcoded Software")
|
||||||
QCoreApplication.setApplicationName(__appname__)
|
QApplication.setApplicationName(__appname__)
|
||||||
QCoreApplication.setApplicationVersion(__version__)
|
QApplication.setApplicationVersion(__version__)
|
||||||
|
QDir.addSearchPath("images", op.join(BASE_PATH, "images"))
|
||||||
setup_qt_logging()
|
setup_qt_logging()
|
||||||
settings = create_qsettings()
|
settings = create_qsettings()
|
||||||
lang = settings.value("Language")
|
lang = settings.value("Language")
|
||||||
@@ -61,7 +62,7 @@ def main():
|
|||||||
# Let the Python interpreter runs every 500ms to handle signals. This is
|
# Let the Python interpreter runs every 500ms to handle signals. This is
|
||||||
# required because Python cannot handle signals while the Qt event loop is
|
# required because Python cannot handle signals while the Qt event loop is
|
||||||
# running.
|
# running.
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt6.QtCore import QTimer
|
||||||
|
|
||||||
timer = QTimer()
|
timer = QTimer()
|
||||||
timer.start(500)
|
timer.start(500)
|
||||||
@@ -70,7 +71,7 @@ def main():
|
|||||||
# has been installed
|
# has been installed
|
||||||
from qt.app import DupeGuru
|
from qt.app import DupeGuru
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(QPixmap(f":/{DupeGuru.LOGO_NAME}")))
|
app.setWindowIcon(QIcon(f"images:{DupeGuru.LOGO_NAME}_32.png"))
|
||||||
global dgapp
|
global dgapp
|
||||||
dgapp = DupeGuru()
|
dgapp = DupeGuru()
|
||||||
install_excepthook("https://github.com/arsenetar/dupeguru/issues")
|
install_excepthook("https://github.com/arsenetar/dupeguru/issues")
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ install_requires =
|
|||||||
Send2Trash>=1.3.0
|
Send2Trash>=1.3.0
|
||||||
mutagen>=1.45.1
|
mutagen>=1.45.1
|
||||||
distro>=1.5.0
|
distro>=1.5.0
|
||||||
PyQt5 >=5.14.1,<6.0; sys_platform != 'linux'
|
PyQt6 >=6.3.0,<7.0; sys_platform != 'linux'
|
||||||
pywin32>=228; sys_platform == 'win32'
|
pywin32>=228; sys_platform == 'win32'
|
||||||
semantic-version>=2.0.0,<3.0.0
|
semantic-version>=2.0.0,<3.0.0
|
||||||
xxhash>=3.0.0,<4.0.0
|
xxhash>=3.0.0,<4.0.0
|
||||||
setup_requires =
|
setup_requires =
|
||||||
sphinx>=3.0.0
|
sphinx>=5.0.0
|
||||||
polib>=1.1.0
|
polib>=1.1.0
|
||||||
tests_require =
|
tests_require =
|
||||||
pytest >=6,<7
|
pytest >=7,<8
|
||||||
include_package_data = true
|
include_package_data = true
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ Section "Uninstall"
|
|||||||
; Remove Files & Folders in Install Folder
|
; Remove Files & Folders in Install Folder
|
||||||
RMDir /r "$INSTDIR\core"
|
RMDir /r "$INSTDIR\core"
|
||||||
RMDir /r "$INSTDIR\help"
|
RMDir /r "$INSTDIR\help"
|
||||||
RMDir /r "$INSTDIR\PyQt5"
|
RMDir /r "$INSTDIR\PyQt6"
|
||||||
RMDir /r "$INSTDIR\qt"
|
RMDir /r "$INSTDIR\qt"
|
||||||
RMDir /r "$INSTDIR\locale"
|
RMDir /r "$INSTDIR\locale"
|
||||||
Delete "$INSTDIR\*.exe"
|
Delete "$INSTDIR\*.exe"
|
||||||
|
|||||||
Reference in New Issue
Block a user