1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-24 23:51:38 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
ade5d7f8c1 Some type-hinting for various qt base objects 2022-06-30 22:52:54 -05:00
36 changed files with 505 additions and 514 deletions

View File

@@ -40,7 +40,7 @@ jobs:
name: Build Cpp
run: |
sudo apt-get update
sudo apt-get install python3-pyqt6
sudo apt-get install python3-pyqt5
make modules
- if: matrix.language == 'python'
name: Autobuild

View File

@@ -60,8 +60,8 @@ ifndef NO_VENV
@${PYTHON} -m venv -h > /dev/null || \
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
endif
@${PYTHON} -c 'import PyQt6' >/dev/null 2>&1 || \
{ echo "PyQt 6.3+ required. Install it and try again. Aborting"; exit 1; }
@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \
{ echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
env: | reqs
ifndef NO_VENV

View File

@@ -32,15 +32,18 @@ For macos instructions (qt version) see the [macOS Instructions](macos.md).
### Prerequisites
* [Python 3.7+][python]
* PyQt6
* PyQt5
### System Setup
When running in a linux based environment the following system packages or equivalents are needed to build:
* python3-pyqt6
* python3-pyqt5
* pyqt5-dev-tools (on some systems, see note)
* python3-venv (only if using a virtual environment)
* python3-dev
* 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:
* python3-setuptools
* debhelper

View File

@@ -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
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
2. Install PyQt6 globally via pip
2. Install PyQt5 globally via pip
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.

View File

@@ -1,2 +1,2 @@
__version__ = "4.3.1"
__version__ = "4.2.1"
__appname__ = "dupeGuru"

View File

@@ -555,9 +555,7 @@ class DupeGuru(Broadcaster):
# a workaround to make the damn thing work.
exepath, args = match.groups()
path, exename = op.split(exepath)
p = subprocess.Popen(
exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
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:

View File

@@ -303,13 +303,12 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
# skip hashing for zero length files
result.append(Match(first, second, 100))
continue
# 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 first.digest_partial == second.digest_partial:
if bigsize > 0 and first.size > bigsize:
if first.digest_samples == second.digest_samples and first.digest_samples is not None:
if first.digest_samples == second.digest_samples:
result.append(Match(first, second, 100))
else:
if first.digest == second.digest and first.digest is not None:
if first.digest == second.digest:
result.append(Match(first, second, 100))
group_count += 1
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))

View File

@@ -144,17 +144,13 @@ class FilesDB:
stat = path.stat()
size = stat.st_size
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()
if result:
return result[0]
except Exception as ex:
logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}")
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()
if result:
return result[0]
return None
@@ -162,14 +158,12 @@ class FilesDB:
stat = path.stat()
size = stat.st_size
mtime_ns = stat.st_mtime_ns
try:
with self.lock:
self.cur.execute(
self.insert_query.format(key=key),
{"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}")
with self.lock:
self.cur.execute(
self.insert_query.format(key=key),
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
)
def commit(self) -> None:
with self.lock:
@@ -271,25 +265,34 @@ class File:
self.size = nonone(stats.st_size, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field == "digest_partial":
self.digest_partial = filesdb.get(self.path, "digest_partial")
if self.digest_partial is None:
self.digest_partial = self._calc_digest_partial()
filesdb.put(self.path, "digest_partial", self.digest_partial)
try:
self.digest_partial = filesdb.get(self.path, "digest_partial")
if self.digest_partial is None:
self.digest_partial = self._calc_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":
self.digest = filesdb.get(self.path, "digest")
if self.digest is None:
self.digest = self._calc_digest()
filesdb.put(self.path, "digest", self.digest)
try:
self.digest = filesdb.get(self.path, "digest")
if self.digest is None:
self.digest = self._calc_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":
size = self.size
# Might as well hash such small files entirely.
if size <= MIN_FILE_SIZE:
setattr(self, field, self.digest)
return
self.digest_samples = filesdb.get(self.path, "digest_samples")
if self.digest_samples is None:
self.digest_samples = self._calc_digest_samples()
filesdb.put(self.path, "digest_samples", self.digest_samples)
try:
self.digest_samples = filesdb.get(self.path, "digest_samples")
if self.digest_samples is None:
self.digest_samples = self._calc_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):
"""Cache all possible info.

33
core/pe/iphoto_plist.py Normal file
View File

@@ -0,0 +1,33 @@
# 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)

View File

@@ -1,21 +1,3 @@
=== 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)
* Default to English on unsupported system language (#976)
* Fix image viewer zoom datatype issue (#978)

View File

@@ -44,8 +44,8 @@ def special_folder_path(special_folder: SpecialFolder, portable: bool = False) -
try:
from PyQt6.QtCore import QUrl, QStandardPaths
from PyQt6.QtGui import QDesktopServices
from PyQt5.QtCore import QUrl, QStandardPaths
from PyQt5.QtGui import QDesktopServices
from qt.util import get_appdata
from core.util import executable_folder
from hscommon.plat import ISWINDOWS, ISOSX
@@ -71,7 +71,7 @@ try:
if ISWINDOWS and portable:
folder = op.join(executable_folder(), "cache")
else:
folder = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.CacheLocation)[0]
folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
else:
folder = get_appdata(portable)
return folder

View File

@@ -82,7 +82,7 @@ def get_locale_name(lang: str) -> Union[str, None]:
# --- Qt
def install_qt_trans(lang: str = None) -> None:
from PyQt6.QtCore import QCoreApplication, QTranslator, QLocale
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale
if not lang:
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
# available so that strings that are inside Qt itself over which I have no control are in the
# right language.
from PyQt6.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo
if not lang:
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:
# 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.
qmpath = op.join(QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath), qmname)
qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname)
else:
qmpath = op.join(base_folder, qmname)
qtr = QTranslator(QCoreApplication.instance())

View File

@@ -2,16 +2,15 @@
# Andrew Senetar <arsenetar@gmail.com>, 2022
# Emanuele, 2022
# Fuan <jcfrt@posteo.net>, 2022
# Giovanni, 2022
#
msgid ""
msgstr ""
"Last-Translator: Giovanni, 2022\n"
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
"Language: it\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: qt/app.py:81
msgid "Quit"
@@ -980,40 +979,37 @@ msgstr "Ignora file più grandi di"
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr "Svuota cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
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
msgid "Cache cleared."
msgstr "Cache svuotata"
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr "Usa stile scuro"
msgstr ""
#: qt\preferences_dialog.py:241
msgid "Profile scan operation"
msgstr "Profila l'operazione di scansione"
msgstr ""
#: qt\preferences_dialog.py:242
msgid "Profile the scan operation and save logs for optimization."
msgstr ""
"Profila l'operazione di scansione e salva i registri per l'ottimizzazione."
#: qt\preferences_dialog.py:246
msgid "Logs located in: <a href=\"{}\">{}</a>"
msgstr "I log si trovano in: <a href=\"{}\">{}</a>"
msgstr ""
#: qt\preferences_dialog.py:291
msgid "Debug"
msgstr "Debug"
msgstr ""
#: qt\about_box.py:31
msgid "About {}"
@@ -1025,7 +1021,7 @@ msgstr "Versione {}"
#: qt\about_box.py:49 qt\about_box.py:75
msgid "Checking for updates..."
msgstr "Controllo degli aggiornamenti..."
msgstr ""
#: qt\about_box.py:54
msgid "Licensed under GPLv3"
@@ -1033,11 +1029,11 @@ msgstr "Distribuito sotto licenza GPLv3"
#: qt\about_box.py:68
msgid "No update available."
msgstr "Nessun aggiornamento disponibile."
msgstr ""
#: qt\about_box.py:71
msgid "New version {} available, download <a href=\"{}\">here</a>."
msgstr "È disponibile la nuova versione {}, scaricabile <a href=\"{}\">qui</a>."
msgstr ""
#: qt\error_report_dialog.py:50
msgid "Error Report"

View File

@@ -1,139 +1,138 @@
# Translators:
# Yuji Sasaki, 2022
# Fuan <jcfrt@posteo.net>, 2022
# Fuan <jcfrt@posteo.net>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: core\app.py:44
#: core\app.py:42
msgid "There are no marked duplicates. Nothing has been done."
msgstr "チェックを入れた重複はありません。 何も行われませんでした。"
msgstr "マークされた重複はありません。 何も行われていません。"
#: core\app.py:45
#: core\app.py:43
msgid "There are no selected duplicates. Nothing has been done."
msgstr "選択された重複はありません。 何も行われていません。"
#: core\app.py:46
#: core\app.py:44
msgid ""
"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?"
msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?"
#: core\app.py:73
#: core\app.py:71
msgid "Scanning for duplicates"
msgstr "重複のスキャン"
#: core\app.py:74
#: core\app.py:72
msgid "Loading"
msgstr "読み込み中"
#: core\app.py:75
#: core\app.py:73
msgid "Moving"
msgstr "移動します"
#: core\app.py:76
#: core\app.py:74
msgid "Copying"
msgstr "コピー中"
#: core\app.py:77
#: core\app.py:75
msgid "Sending to Trash"
msgstr "ごみ箱に送信します"
#: core\app.py:291
#: core\app.py:289
msgid ""
"A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again."
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
#: core\app.py:302
#: core\app.py:300
msgid "No duplicates found."
msgstr "重複は見つかりませんでした。"
#: core\app.py:317
#: core\app.py:315
msgid "All marked files were copied successfully."
msgstr "チェックを入れたファイルすべてコピーました。"
msgstr "マークされたファイルすべて正常にコピーされました。"
#: core\app.py:317
msgid "All marked files were moved successfully."
msgstr "マークされたファイルはすべて正常に移動されました。"
#: core\app.py:319
msgid "All marked files were moved successfully."
msgstr "チェックを入れたファイルをすべて移動しました。"
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were deleted successfully."
msgstr "チェックを入れたファイルをすべて削除しました。"
#: core\app.py:323
msgid "All marked files were successfully sent to Trash."
msgstr "チェックを入れたファイルすべてごみ箱に移動しました。"
msgstr "マークされたファイルすべてごみ箱に正常に送信されました。"
#: core\app.py:328
#: core\app.py:326
msgid "Could not load file: {}"
msgstr "ファイルを読み込めませんでした:{}"
#: core\app.py:384
#: core\app.py:382
msgid "'{}' already is in the list."
msgstr "「{}」既にリストに含まれています。"
#: core\app.py:386
#: core\app.py:384
msgid "'{}' does not exist."
msgstr "'{}' 存在しません。"
#: core\app.py:394
#: core\app.py:392
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
#: core\app.py:471
#: core\app.py:469
msgid "Select a directory to copy marked files to"
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
#: core\app.py:473
#: core\app.py:471
msgid "Select a directory to move marked files to"
msgstr "マークされたファイルを移動するディレクトリを選択してください"
#: core\app.py:512
#: core\app.py:510
msgid "Select a destination for your exported CSV"
msgstr "エクスポートしたCSVの宛先を選択します。"
#: core\app.py:518 core\app.py:773 core\app.py:783
#: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}"
msgstr "ファイルに書き込めませんでした:{}"
#: core\app.py:541
#: core\app.py:539
msgid "You have no custom command set up. Set it up in your preferences."
msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。"
#: core\app.py:697 core\app.py:709
#: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?"
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
#: core\app.py:745
#: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
#: core\app.py:792
#: core\app.py:790
msgid "The selected directories contain no scannable file."
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
#: core\app.py:808
#: core\app.py:803
msgid "Collecting files to scan"
msgstr "スキャンするファイルを収集しています"
#: core\app.py:858
#: core\app.py:850
msgid "%s (%d discarded)"
msgstr "%s (%d 廃棄)"
#: core\directories.py:190
#: core\directories.py:191
msgid "Collected {} files to scan"
msgstr ""
#: core\directories.py:206
#: core\directories.py:207
msgid "Collected {} folders to scan"
msgstr ""
@@ -201,35 +200,35 @@ msgstr "EXIFタイムスタンプ"
msgid "None"
msgstr "無し"
#: core\prioritize.py:102
#: core\prioritize.py:100
msgid "Ends with number"
msgstr "番号で終わっている"
#: core\prioritize.py:103
#: core\prioritize.py:101
msgid "Doesn't end with number"
msgstr "数字で終わっていない"
#: core\prioritize.py:104
#: core\prioritize.py:102
msgid "Longest"
msgstr "最長"
#: core\prioritize.py:105
#: core\prioritize.py:103
msgid "Shortest"
msgstr "最短"
#: core\prioritize.py:142
#: core\prioritize.py:140
msgid "Highest"
msgstr "最高"
#: core\prioritize.py:142
#: core\prioritize.py:140
msgid "Lowest"
msgstr "最低"
#: core\prioritize.py:171
#: core\prioritize.py:169
msgid "Newest"
msgstr "最新"
#: core\prioritize.py:171
#: core\prioritize.py:169
msgid "Oldest"
msgstr "最古"

View File

@@ -81,7 +81,7 @@ msgstr "(非対応)"
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
msgid "Directly delete files"
msgstr "ファイルを完全に削除"
msgstr "ファイルを直接削除する"
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
msgid ""
@@ -100,7 +100,7 @@ msgstr "キャンセル"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Attribute"
msgstr "属性"
msgstr "アトリビュート"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Selected"
@@ -163,7 +163,7 @@ msgstr "スキャンの種類:"
#: qt/directories_dialog.py:135
msgid "More Options"
msgstr "詳細設定"
msgstr "もっとオプション"
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
msgid "Select folders to scan and press \"Scan\"."
@@ -179,7 +179,7 @@ msgstr "スキャン"
#: qt/directories_dialog.py:230
msgid "Unsaved results"
msgstr "保存結果"
msgstr "保存されていない結果"
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
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
#: cocoa/en.lproj/Localizable.strings:0
msgid "Match similar words"
msgstr "類似の単語一致"
msgstr "類似の単語一致する"
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
msgid "Can mix file kind"
msgstr "ファイルの種類を混在"
msgstr "ファイルの種類を混在させることができる"
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
msgid "Use regular expressions when filtering"
msgstr "フィルタに正規表現を使用"
msgstr "フィルタリング時に正規表現を使用する"
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Remove empty folders on delete or move"
msgstr "削除や移動で空になったフォルダを削除"
msgstr "削除または移動時に空のフォルダを削除する"
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file"
msgstr "同じファイルへの重複ハードリンクを無視"
msgstr "同じファイルへの重複ハードリンクを無視する"
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
#: 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
msgid "Match pictures of different dimensions"
msgstr "異なるサイズの写真を一致"
msgstr "異なる寸法の写真を一致させる"
#: qt/preferences_dialog.py:43
msgid "Filter Hardness:"
msgstr "フィルタの強さ:"
msgstr "フィルター硬度:"
#: qt/preferences_dialog.py:69
msgid "More Results"
@@ -325,7 +325,7 @@ msgstr "より少ない結果"
#: qt/preferences_dialog.py:81
msgid "Font size:"
msgstr "文字サイズ:"
msgstr "フォントサイズ:"
#: qt/preferences_dialog.py:85
msgid "Language:"
@@ -333,7 +333,7 @@ msgstr "言語:"
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
msgid "Copy and Move:"
msgstr "コピーと移動:"
msgstr "コピーと移動"
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
msgid "Right in destination"
@@ -349,7 +349,7 @@ msgstr "絶対パスを再作成"
#: qt/preferences_dialog.py:99
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
msgstr "カスタムコマンド (引数: dは重複・rは参照:"
msgstr "カスタムコマンド (引数重複の場合はd、参照の場合はr:"
#: qt/preferences_dialog.py:174
msgid "dupeGuru has to restart for language changes to take effect."
@@ -719,7 +719,7 @@ msgstr "ウィンドウ"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Zoom"
msgstr "拡大"
msgstr "ズーム"
#: qt\app.py:158
msgid "Exclusion Filters"
@@ -909,15 +909,15 @@ msgstr "結果"
#: qt\preferences_dialog.py:150
msgid "General Interface"
msgstr "一般"
msgstr "一般的なインターフェイス"
#: qt\preferences_dialog.py:176
msgid "Result Table"
msgstr "結果"
msgstr "結果"
#: qt\preferences_dialog.py:205
msgid "Details Window"
msgstr "詳細画面"
msgstr "詳細ウィンドウ"
#: qt\preferences_dialog.py:285
msgid "General"
@@ -985,7 +985,7 @@ msgstr ""
#: qt\about_box.py:31
msgid "About {}"
msgstr "{}について"
msgstr "{}について"
#: qt\about_box.py:47
msgid "Version {}"
@@ -997,7 +997,7 @@ msgstr ""
#: qt\about_box.py:54
msgid "Licensed under GPLv3"
msgstr "GPLv3のもとでライセンスされています"
msgstr "GPLv3としてライセンス供与。"
#: qt\about_box.py:68
msgid "No update available."
@@ -1013,7 +1013,7 @@ msgstr "エラーレポート"
#: qt\error_report_dialog.py:54
msgid "Something went wrong. How about reporting the error?"
msgstr "不明な理由により失敗しました。問題を報告しませんか?"
msgstr "何かがうまくいかなかった。 エラーを報告するのはどうですか?"
#: qt\error_report_dialog.py:60
msgid ""
@@ -1035,7 +1035,7 @@ msgstr ""
#: qt\error_report_dialog.py:80
msgid "Go to Github"
msgstr "Githubに移動"
msgstr "Githubに移動する"
#: qt\preferences.py:24
msgid "Czech"

View File

@@ -7,19 +7,19 @@ These instructions are for the Qt version of the UI on macOS.
- [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)
- [Homebrew][homebrew]
- [qt6](https://www.qt.io/)
- [qt5](https://www.qt.io/)
#### Prerequisite setup
1. Install Xcode if desired
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
effect.
3. Install qt6 with `brew`. If you are using a version of macos without system python 3.7+ then you will
3. Install qt5 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.
$ brew install qt6
$ brew install qt5
NOTE: Using `brew` to install qt6 is to allow pyqt6 to build without a native wheel
NOTE: Using `brew` to install qt5 is to allow pyqt5 to build without a native wheel
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.
@@ -27,7 +27,7 @@ also need to install that via brew or with pyenv.
### With build.py
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
build pyqt6 from source then the first line below is needed, else it may be omitted. (Path shown is
build pyqt5 from source then the first line below is needed, else it may be omitted. (Path shown is
for an arm mac.)
$ export PATH="/opt/homebrew/opt/qt/bin:$PATH"

View File

@@ -10,7 +10,7 @@ Vcs-Git: https://github.com/arsenetar/dupeguru.git
Package: {pkgname}
Architecture: {arch}
Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt6, python3-mutagen, python3-semantic-version
Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt5, python3-mutagen, python3-semantic-version
Provides: dupeguru-se, dupeguru-me, dupeguru-pe
Replaces: dupeguru-se, dupeguru-me, dupeguru-pe
Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe

View File

@@ -6,46 +6,36 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt6.QtCore import Qt, QCoreApplication, QTimer
from PyQt6.QtGui import QPixmap, QFont, QShowEvent
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel, QWidget
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel
from core.util import check_for_update
from qt.util import move_to_screen_center
from hscommon.trans import trget
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from qt.app import DupeGuru
tr = trget("ui")
class AboutBox(QDialog):
def __init__(self, parent: QWidget, app: "DupeGuru", **kwargs) -> None:
flags = (
Qt.WindowType.CustomizeWindowHint
| Qt.WindowType.WindowTitleHint
| Qt.WindowType.WindowSystemMenuHint
| Qt.WindowType.MSWindowsFixedSizeDialogHint
)
def __init__(self, parent, app, **kwargs):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
super().__init__(parent, flags, **kwargs)
self.app = app
self._setupUi()
def _setupUi(self) -> None:
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
def _setupUi(self):
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
size_policy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setSizePolicy(size_policy)
main_layout = QHBoxLayout(self)
logo_label = QLabel()
logo_label.setPixmap(QPixmap(f"images:{self.app.LOGO_NAME}_128.png"))
logo_label.setPixmap(QPixmap(":/%s_big" % self.app.LOGO_NAME))
main_layout.addWidget(logo_label)
detail_layout = QVBoxLayout()
name_label = QLabel()
font = QFont()
font.setWeight(75)
@@ -53,35 +43,26 @@ class AboutBox(QDialog):
name_label.setFont(font)
name_label.setText(QCoreApplication.instance().applicationName())
detail_layout.addWidget(name_label)
version_label = QLabel()
version_label.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
detail_layout.addWidget(version_label)
self.update_label = QLabel(tr("Checking for updates..."))
self.update_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
self.update_label.setOpenExternalLinks(True)
detail_layout.addWidget(self.update_label)
license_label = QLabel()
license_label.setText(tr("Licensed under GPLv3"))
detail_layout.addWidget(license_label)
spacer_label = QLabel()
spacer_label.setFont(font)
detail_layout.addWidget(spacer_label)
button_box = QDialogButtonBox()
button_box.setOrientation(Qt.Orientation.Horizontal)
button_box.setStandardButtons(QDialogButtonBox.StandardButton.Ok)
detail_layout.addWidget(button_box)
self.button_box = QDialogButtonBox()
self.button_box.setOrientation(Qt.Horizontal)
self.button_box.setStandardButtons(QDialogButtonBox.Ok)
detail_layout.addWidget(self.button_box)
main_layout.addLayout(detail_layout)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
def _check_for_update(self) -> None:
def _check_for_update(self):
update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False)
if update is None:
self.update_label.setText(tr("No update available."))
@@ -90,7 +71,7 @@ class AboutBox(QDialog):
tr('New version {} available, download <a href="{}">here</a>.').format(update["version"], update["url"])
)
def showEvent(self, event: QShowEvent) -> None:
def showEvent(self, event):
self.update_label.setText(tr("Checking for updates..."))
# have to do this here as the frameGeometry is not correct until shown
move_to_screen_center(self)

106
qt/app.py
View File

@@ -6,18 +6,15 @@
import sys
import os.path as op
from typing import Type
from PyQt6.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt
from PyQt6.QtGui import QColor, QDesktopServices, QPalette
from PyQt6.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip
from PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt
from PyQt5.QtGui import QColor, QDesktopServices, QPalette
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip
from hscommon.trans import trget
from hscommon import desktop, plat
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.util import create_actions
from qt.progress_window import ProgressWindow
@@ -45,10 +42,10 @@ tr = trget("ui")
class DupeGuru(QObject):
LOGO_NAME = "dgse_logo"
LOGO_NAME = "logo_se"
NAME = "dupeGuru"
def __init__(self, **kwargs) -> None:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.prefs = Preferences()
self.prefs.load()
@@ -59,7 +56,7 @@ class DupeGuru(QObject):
self._setup()
# --- Private
def _setup(self) -> None:
def _setup(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
self._setupActions()
self.details_dialog = None
@@ -111,7 +108,7 @@ class DupeGuru(QObject):
# that the application haven't launched.
QTimer.singleShot(0, self.finishedLaunching)
def _setupActions(self) -> None:
def _setupActions(self):
# Setup actions that are common to both the directory dialog and the results window.
# (name, shortcut, icon, desc, func)
ACTIONS = [
@@ -157,7 +154,7 @@ class DupeGuru(QObject):
]
create_actions(ACTIONS, self)
def _update_options(self) -> None:
def _update_options(self):
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["clean_empty_dirs"] = self.prefs.remove_empty_folders
@@ -203,7 +200,7 @@ class DupeGuru(QObject):
self._set_style("dark" if self.prefs.use_dark_style else "light")
# --- Private
def _get_details_dialog_class(self) -> Type[DetailsDialog]:
def _get_details_dialog_class(self):
if self.model.app_mode == AppMode.PICTURE:
return DetailsDialogPicture
elif self.model.app_mode == AppMode.MUSIC:
@@ -211,7 +208,7 @@ class DupeGuru(QObject):
else:
return DetailsDialogStandard
def _get_preferences_dialog_class(self) -> Type[PreferencesDialogBase]:
def _get_preferences_dialog_class(self):
if self.model.app_mode == AppMode.PICTURE:
return PreferencesDialogPicture
elif self.model.app_mode == AppMode.MUSIC:
@@ -219,7 +216,7 @@ class DupeGuru(QObject):
else:
return PreferencesDialogStandard
def _set_style(self, style: str = "light") -> None:
def _set_style(self, style="light"):
# Only support this feature on windows for now
if not plat.ISWINDOWS:
return
@@ -227,18 +224,18 @@ class DupeGuru(QObject):
QApplication.setStyle(QStyleFactory.create("Fusion"))
palette = QApplication.style().standardPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.WindowText, Qt.white)
palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.white)
palette.setColor(QPalette.ColorRole.Text, Qt.white)
palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
palette.setColor(QPalette.ColorRole.ButtonText, Qt.white)
palette.setColor(QPalette.ColorRole.BrightText, Qt.red)
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.black)
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.ButtonText, QColor(164, 166, 168))
@@ -253,31 +250,29 @@ class DupeGuru(QObject):
QApplication.setPalette(palette)
# --- Public
def add_selected_to_ignore_list(self) -> None:
def add_selected_to_ignore_list(self):
self.model.add_selected_to_ignore_list()
def remove_selected(self) -> None:
self.model.remove_selected()
def remove_selected(self):
self.model.remove_selected(self)
def confirm(
self, title: str, msg: str, default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.Yes
) -> bool:
def confirm(self, title, msg, default_button=QMessageBox.Yes):
active = QApplication.activeWindow()
buttons = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
buttons = QMessageBox.Yes | QMessageBox.No
answer = QMessageBox.question(active, title, msg, buttons, default_button)
return answer == QMessageBox.StandardButton.Yes
return answer == QMessageBox.Yes
def invokeCustomCommand(self) -> None:
def invokeCustomCommand(self):
self.model.invoke_custom_command()
def show_details(self) -> None:
def show_details(self):
if self.details_dialog is not None:
if not self.details_dialog.isVisible():
self.details_dialog.show()
else:
self.details_dialog.hide()
def showResultsWindow(self) -> None:
def showResultsWindow(self):
if self.resultWindow is not None:
if self.use_tabs:
if self.main_window.indexOfWidget(self.resultWindow) < 0:
@@ -287,14 +282,14 @@ class DupeGuru(QObject):
else:
self.resultWindow.show()
def showDirectoriesWindow(self) -> None:
def showDirectoriesWindow(self):
if self.directories_dialog is not None:
if self.use_tabs:
self.main_window.showTab(self.directories_dialog)
else:
self.directories_dialog.show()
def shutdown(self) -> None:
def shutdown(self):
self.willSavePrefs.emit()
self.prefs.save()
self.model.save()
@@ -309,7 +304,7 @@ class DupeGuru(QObject):
SIGTERM = pyqtSignal()
# --- Events
def finishedLaunching(self) -> None:
def finishedLaunching(self):
if sys.getfilesystemencoding() == "ascii":
# No need to localize this, it's a debugging message.
msg = (
@@ -329,28 +324,28 @@ class DupeGuru(QObject):
self.model.load_from(results)
self.recentResults.insertItem(results)
def clearCacheTriggered(self) -> None:
def clearCacheTriggered(self):
title = tr("Clear Cache")
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.StandardButton.No):
if self.confirm(title, msg, QMessageBox.No):
self.model.clear_picture_cache()
self.model.clear_hash_cache()
active = QApplication.activeWindow()
QMessageBox.information(active, title, tr("Cache cleared."))
def ignoreListTriggered(self) -> None:
def ignoreListTriggered(self):
if self.use_tabs:
self.showTriggeredTabbedDialog(self.ignoreListDialog, tr("Ignore List"))
else: # floating windows
self.model.ignore_list_dialog.show()
def excludeListTriggered(self) -> None:
def excludeListTriggered(self):
if self.use_tabs:
self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
else: # floating windows
self.model.exclude_list_dialog.show()
def showTriggeredTabbedDialog(self, dialog, desc_string: str) -> None:
def showTriggeredTabbedDialog(self, dialog, desc_string):
"""Add tab for dialog, name the tab with desc_string, then show it."""
index = self.main_window.indexOfWidget(dialog)
# Create the tab if it doesn't exist already
@@ -359,22 +354,23 @@ class DupeGuru(QObject):
# Show the tab for that widget
self.main_window.setCurrentIndex(index)
def openDebugLogTriggered(self) -> None:
def openDebugLogTriggered(self):
debug_log_path = op.join(self.model.appdata, "debug.log")
desktop.open_path(debug_log_path)
def preferencesTriggered(self) -> None:
def preferencesTriggered(self):
preferences_dialog = self._get_preferences_dialog_class()(
self.main_window if self.main_window else self.directories_dialog, self
)
preferences_dialog.load()
result = preferences_dialog.exec()
if result == QDialog.DialogCode.Accepted:
if result == QDialog.Accepted:
preferences_dialog.save()
self.prefs.save()
self._update_options()
preferences_dialog.setParent(None)
def quitTriggered(self) -> None:
def quitTriggered(self):
if self.details_dialog is not None:
self.details_dialog.close()
@@ -383,10 +379,10 @@ class DupeGuru(QObject):
else:
self.directories_dialog.close()
def showAboutBoxTriggered(self) -> None:
def showAboutBoxTriggered(self):
self.about_box.show()
def showHelpTriggered(self) -> None:
def showHelpTriggered(self):
base_path = platform.HELP_PATH
help_path = op.abspath(op.join(base_path, "index.html"))
if op.exists(help_path):
@@ -395,7 +391,7 @@ class DupeGuru(QObject):
url = QUrl("https://dupeguru.voltaicideas.net/help/en/")
QDesktopServices.openUrl(url)
def handleSIGTERM(self) -> None:
def handleSIGTERM(self):
self.shutdown()
# --- model --> view
@@ -405,20 +401,20 @@ class DupeGuru(QObject):
def set_default(self, key, value):
self.prefs.set_value(key, value)
def show_message(self, msg: str) -> None:
def show_message(self, msg):
window = QApplication.activeWindow()
QMessageBox.information(window, "", msg)
def ask_yes_no(self, prompt: str) -> bool:
def ask_yes_no(self, prompt):
return self.confirm("", prompt)
def create_results_window(self) -> None:
def create_results_window(self):
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``."""
if self.details_dialog is not None:
# The object is not deleted entirely, avoid saving its geometry in the future
# self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)
# or simply delete it on close which is probably cleaner:
self.details_dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.details_dialog.setAttribute(Qt.WA_DeleteOnClose)
self.details_dialog.close()
# if we don't do the following, Qt will crash when we recreate the Results dialog
self.details_dialog.setParent(None)
@@ -433,17 +429,17 @@ class DupeGuru(QObject):
self.directories_dialog._updateActionsState()
self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self)
def show_results_window(self) -> None:
def show_results_window(self):
self.showResultsWindow()
def show_problem_dialog(self) -> None:
def show_problem_dialog(self):
self.problemDialog.show()
def select_dest_folder(self, prompt: str) -> str:
flags = QFileDialog.Option.ShowDirsOnly
def select_dest_folder(self, prompt):
flags = QFileDialog.ShowDirsOnly
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, "", flags)
def select_dest_file(self, prompt: str, extension: str) -> str:
def select_dest_file(self, prompt, extension):
files = tr("{} file (*.{})").format(extension.upper(), extension)
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
if not destination.endswith(f".{extension}"):

View File

@@ -11,8 +11,8 @@ import sys
import os
import platform
from PyQt6.QtCore import Qt, QCoreApplication, QSize
from PyQt6.QtWidgets import (
from PyQt5.QtCore import Qt, QCoreApplication, QSize
from PyQt5.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
@@ -30,7 +30,7 @@ tr = trget("ui")
class ErrorReportDialog(QDialog):
def __init__(self, parent, github_url, error, **kwargs):
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
super().__init__(parent, flags, **kwargs)
self._setupUi()
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
error_text = error_text.replace("\n", os.linesep)
self.error_text_edit.setPlainText(error_text)
self.errorTextEdit.setPlainText(error_text)
self.github_url = github_url
self.sendButton.clicked.connect(self.goToGithub)
self.dontSendButton.clicked.connect(self.reject)
def _setupUi(self):
self.setWindowTitle(tr("Error Report"))
self.resize(553, 349)
main_layout = QVBoxLayout(self)
title_label = QLabel(self)
title_label.setText(tr("Something went wrong. How about reporting the error?"))
title_label.setWordWrap(True)
main_layout.addWidget(title_label)
self.error_text_edit = QPlainTextEdit(self)
self.error_text_edit.setReadOnly(True)
main_layout.addWidget(self.error_text_edit)
self.verticalLayout = QVBoxLayout(self)
self.label = QLabel(self)
self.label.setText(tr("Something went wrong. How about reporting the error?"))
self.label.setWordWrap(True)
self.verticalLayout.addWidget(self.label)
self.errorTextEdit = QPlainTextEdit(self)
self.errorTextEdit.setReadOnly(True)
self.verticalLayout.addWidget(self.errorTextEdit)
msg = tr(
"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 "
@@ -67,28 +67,21 @@ class ErrorReportDialog(QDialog):
"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."
)
instructions_label = QLabel(msg)
instructions_label.setWordWrap(True)
main_layout.addWidget(instructions_label)
button_layout = QHBoxLayout()
button_layout.addItem(horizontal_spacer())
close_button = QPushButton(self)
close_button.setText(tr("Close"))
close_button.setMinimumSize(QSize(110, 0))
button_layout.addWidget(close_button)
report_button = QPushButton(self)
report_button.setText(tr("Go to Github"))
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)
self.label2 = QLabel(msg)
self.label2.setWordWrap(True)
self.verticalLayout.addWidget(self.label2)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.addItem(horizontal_spacer())
self.dontSendButton = QPushButton(self)
self.dontSendButton.setText(tr("Close"))
self.dontSendButton.setMinimumSize(QSize(110, 0))
self.horizontalLayout.addWidget(self.dontSendButton)
self.sendButton = QPushButton(self)
self.sendButton.setText(tr("Go to Github"))
self.sendButton.setMinimumSize(QSize(110, 0))
self.sendButton.setDefault(True)
self.horizontalLayout.addWidget(self.sendButton)
self.verticalLayout.addLayout(self.horizontalLayout)
def goToGithub(self):
open_url(self.github_url)
@@ -98,6 +91,6 @@ def install_excepthook(github_url):
def my_excepthook(exctype, value, tb):
s = "".join(traceback.format_exception(exctype, value, tb))
dialog = ErrorReportDialog(None, github_url, s)
dialog.exec()
dialog.exec_()
sys.excepthook = my_excepthook

View File

@@ -4,8 +4,8 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt6.QtCore import QSize
from PyQt6.QtWidgets import QAbstractItemView
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QAbstractItemView
from hscommon.trans import trget
from qt.details_dialog import DetailsDialog as DetailsDialogBase
@@ -21,6 +21,6 @@ class DetailsDialog(DetailsDialogBase):
self.setMinimumSize(QSize(250, 250))
self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.setWidget(self.tableView)

View File

@@ -5,8 +5,8 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable
from PyQt6.QtCore import QSize
from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
from hscommon.trans import trget
from core.app import AppMode
@@ -32,7 +32,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.verticalLayout_4.addWidget(self.label_6)
self.horizontalLayout_2 = QHBoxLayout()
self.horizontalLayout_2.setSpacing(0)
spacer_item = QSpacerItem(15, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
spacer_item = QSpacerItem(15, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacer_item)
self._setupAddCheckbox("tagTrackBox", tr("Track"), self.widget)
self.horizontalLayout_2.addWidget(self.tagTrackBox)

View File

@@ -1,5 +1,5 @@
from typing import Tuple, List, Union
from PyQt6.QtGui import QImage
from PyQt5.QtGui import QImage
_block = Tuple[int, int, int]

View File

@@ -4,10 +4,12 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
from typing import Union
from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtWidgets import QWidget, QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
from PyQt5.QtGui import QResizeEvent
from hscommon.trans import trget
from qt import app
from qt.details_dialog import DetailsDialog as DetailsDialogBase
from qt.details_table import DetailsTable
from qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
@@ -16,15 +18,15 @@ tr = trget("ui")
class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app):
self.vController = None
def __init__(self, parent: QWidget, app: "app.DupeGuru") -> None:
self.vController: Union[ScrollAreaController, None] = None
super().__init__(parent, app)
def _setupUi(self):
def _setupUi(self) -> None:
self.setWindowTitle(tr("Details"))
self.resize(502, 502)
self.setMinimumSize(QSize(250, 250))
self.splitter = QSplitter(Qt.Vertical)
self.splitter = QSplitter(Qt.Orientation.Vertical)
self.topFrame = EmittingFrame()
self.topFrame.setFrameShape(QFrame.StyledPanel)
self.horizontalLayout = QGridLayout()
@@ -47,8 +49,8 @@ class DetailsDialog(DetailsDialogBase):
self.vController = ScrollAreaController(self)
self.verticalToolBar = ViewerToolBar(self, self.vController)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.verticalToolBar.setOrientation(Qt.Orientation.Vertical)
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignmentFlag.AlignCenter)
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
@@ -73,7 +75,7 @@ class DetailsDialog(DetailsDialogBase):
self.topFrame.resized.connect(self.resizeEvent)
def _update(self):
def _update(self) -> None:
if self.vController is None: # Not yet constructed!
return
if not self.app.model.selected_dupes:
@@ -87,15 +89,14 @@ class DetailsDialog(DetailsDialogBase):
self.vController.updateView(ref, dupe, group)
# --- Override
@pyqtSlot(QResizeEvent)
def resizeEvent(self, event):
def resizeEvent(self, event: QResizeEvent) -> None:
self.ensure_same_sizes()
if self.vController is None or not self.vController.bestFit:
return
# Only update the scaled down pixmaps
self.vController.updateBothImages()
def show(self):
def show(self) -> None:
# Give the splitter a maximum height to reach. This is assuming that
# all rows below their headers have the same height
self.tableView.setMaximumHeight(
@@ -108,7 +109,7 @@ class DetailsDialog(DetailsDialogBase):
self.ensure_same_sizes()
self._update()
def ensure_same_sizes(self):
def ensure_same_sizes(self) -> None:
# HACK This ensures same size while shrinking.
# ReferenceViewer might be 1 pixel shorter in width
# due to the toolbar in the middle keeping the same width,
@@ -126,7 +127,7 @@ class DetailsDialog(DetailsDialogBase):
self.selectedImageViewer.resize(self.referenceImageViewer.size())
# model --> view
def refresh(self):
def refresh(self) -> None:
DetailsDialogBase.refresh(self)
if self.isVisible():
self._update()
@@ -137,5 +138,5 @@ class EmittingFrame(QFrame):
resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event):
def resizeEvent(self, event: QResizeEvent) -> None:
self.resized.emit(event)

View File

@@ -2,6 +2,7 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Union, cast
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import (
@@ -17,6 +18,7 @@ from PyQt5.QtWidgets import (
QAbstractScrollArea,
QStyle,
)
from qt.details_dialog import DetailsDialog
from hscommon.trans import trget
from hscommon.plat import ISLINUX
@@ -26,7 +28,7 @@ MAX_SCALE = 12.0
MIN_SCALE = 0.1
def create_actions(actions, target):
def create_actions(actions: list, target: QObject) -> None:
# actions are list of (name, shortcut, icon, desc, func)
for name, shortcut, icon, desc, func in actions:
action = QAction(target)
@@ -40,9 +42,9 @@ def create_actions(actions, target):
class ViewerToolBar(QToolBar):
def __init__(self, parent, controller):
def __init__(self, parent: DetailsDialog, controller: "BaseController") -> None:
super().__init__(parent)
self.parent = parent
self.setParent(parent)
self.controller = controller
self.setupActions(controller)
self.createButtons()
@@ -52,24 +54,21 @@ class ViewerToolBar(QToolBar):
self.buttonNormalSize.setEnabled(False)
self.buttonBestFit.setEnabled(False)
def setupActions(self, controller):
def setupActions(self, controller: "BaseController") -> None:
override_icons = cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
# actions are list of (name, shortcut, icon, desc, func)
ACTIONS = [
(
"actionZoomIn",
QKeySequence.ZoomIn,
QIcon.fromTheme("zoom-in")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_in")),
QIcon.fromTheme("zoom-in") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_in")),
tr("Increase zoom"),
controller.zoomIn,
),
(
"actionZoomOut",
QKeySequence.ZoomOut,
QIcon.fromTheme("zoom-out")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_out")),
QIcon.fromTheme("zoom-out") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_out")),
tr("Decrease zoom"),
controller.zoomOut,
),
@@ -77,7 +76,7 @@ class ViewerToolBar(QToolBar):
"actionNormalSize",
tr("Ctrl+/"),
QIcon.fromTheme("zoom-original")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not override_icons
else QIcon(QPixmap(":/" + "zoom_original")),
tr("Normal size"),
controller.zoomNormalSize,
@@ -86,7 +85,7 @@ class ViewerToolBar(QToolBar):
"actionBestFit",
tr("Ctrl+*"),
QIcon.fromTheme("zoom-best-fit")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not override_icons
else QIcon(QPixmap(":/" + "zoom_best_fit")),
tr("Best fit"),
controller.zoomBestFit,
@@ -96,12 +95,12 @@ class ViewerToolBar(QToolBar):
# the popup menu work in the toolbar (if resized below minimum height)
create_actions(ACTIONS, self)
def createButtons(self):
def createButtons(self) -> None:
self.buttonImgSwap = QToolButton(self)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonImgSwap.setIcon(
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload))
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
if ISLINUX and not cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "exchange"))
)
self.buttonImgSwap.setText("Swap images")
@@ -110,22 +109,22 @@ class ViewerToolBar(QToolBar):
self.buttonImgSwap.released.connect(self.controller.swapImages)
self.buttonZoomIn = QToolButton(self)
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonZoomIn.setDefaultAction(self.actionZoomIn)
self.buttonZoomIn.setEnabled(False)
self.buttonZoomOut = QToolButton(self)
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonZoomOut.setDefaultAction(self.actionZoomOut)
self.buttonZoomOut.setEnabled(False)
self.buttonNormalSize = QToolButton(self)
self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonNormalSize.setDefaultAction(self.actionNormalSize)
self.buttonNormalSize.setEnabled(True)
self.buttonBestFit = QToolButton(self)
self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonBestFit.setDefaultAction(self.actionBestFit)
self.buttonBestFit.setEnabled(False)
@@ -141,10 +140,10 @@ class BaseController(QObject):
Base proxy interface to keep image viewers synchronized.
Relays function calls, keep tracks of things."""
def __init__(self, parent):
def __init__(self, parent: QObject) -> None:
super().__init__()
self.selectedViewer = None
self.referenceViewer = None
self.selectedViewer: Union[ScrollAreaImageViewer, None] = None
self.referenceViewer: Union[ScrollAreaImageViewer, None] = None
# cached pixmaps
self.selectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
@@ -152,22 +151,24 @@ class BaseController(QObject):
self.scaledReferencePixmap = QPixmap()
self.current_scale = 1.0
self.bestFit = True
self.parent = parent # To change buttons' states
self.setParent(parent) # To change buttons' states
self.cached_group = None
self.same_dimensions = True
def setupViewers(self, selected_viewer, reference_viewer):
def setupViewers(self, selected_viewer: "ScrollAreaImageViewer", reference_viewer: "ScrollAreaImageViewer") -> None:
self.selectedViewer = selected_viewer
self.referenceViewer = reference_viewer
self.selectedViewer.controller = self
self.referenceViewer.controller = self
self._setupConnections()
def _setupConnections(self):
self.selectedViewer.connectMouseSignals()
self.referenceViewer.connectMouseSignals()
def _setupConnections(self) -> None:
if self.selectedViewer is not None:
self.selectedViewer.connectMouseSignals()
if self.referenceViewer is not None:
self.referenceViewer.connectMouseSignals()
def updateView(self, ref, dupe, group):
def updateView(self, ref, dupe, group) -> None:
# To keep current scale accross dupes from the same group
previous_same_dimensions = self.same_dimensions
self.same_dimensions = True
@@ -206,13 +207,15 @@ class BaseController(QObject):
if ignore_update:
self.selectedViewer.ignore_signal = False
def _updateImage(self, pixmap, viewer, same_group=False):
def _updateImage(
self, pixmap: QPixmap, viewer: "ScrollAreaImageViewer", same_group: bool = False
) -> Union[QSize, None]:
# WARNING this is called on every resize event, might need to split
# into a separate function depending on the implementation used
if pixmap.isNull():
# This should disable the blank widget
viewer.setImage(pixmap)
return
return None
target_size = viewer.size()
if not viewer.bestFit:
if same_group:
@@ -220,14 +223,18 @@ class BaseController(QObject):
return target_size
# zoomed in state, expand
# only if not same_group, we need full update
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
scaledpixmap = pixmap.scaled(
target_size, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.FastTransformation
)
else:
# best fit, keep ratio always
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
scaledpixmap = pixmap.scaled(
target_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation
)
viewer.setImage(scaledpixmap)
return target_size
def resetState(self):
def resetState(self) -> None:
"""Only called when the group of dupes has changed. We reset our
controller internal state and buttons, center view on viewers."""
self.selectedPixmap = QPixmap()
@@ -248,7 +255,7 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
def resetViewersState(self):
def resetViewersState(self) -> None:
"""No item from the model, disable and clear everything."""
# only called by the details dialog
self.selectedPixmap = QPixmap()
@@ -277,36 +284,40 @@ class BaseController(QObject):
self.referenceViewer.setEnabled(False)
@pyqtSlot()
def zoomIn(self):
def zoomIn(self) -> None:
self.scaleImagesBy(1.25)
@pyqtSlot()
def zoomOut(self):
def zoomOut(self) -> None:
self.scaleImagesBy(0.8)
@pyqtSlot(float)
def scaleImagesBy(self, factor):
def scaleImagesBy(self, factor: float) -> None:
"""Compute new scale from factor and scale."""
self.current_scale *= factor
self.selectedViewer.scaleBy(factor)
self.referenceViewer.scaleBy(factor)
if self.selectedViewer is not None:
self.selectedViewer.scaleBy(factor)
if self.referenceViewer is not None:
self.referenceViewer.scaleBy(factor)
self.updateButtons()
@pyqtSlot(float)
def scaleImagesAt(self, scale):
def scaleImagesAt(self, scale: float) -> None:
"""Scale at a pre-computed scale."""
self.current_scale = scale
self.selectedViewer.scaleAt(scale)
self.referenceViewer.scaleAt(scale)
if self.selectedViewer is not None:
self.selectedViewer.scaleAt(scale)
if self.referenceViewer is not None:
self.referenceViewer.scaleAt(scale)
self.updateButtons()
def updateButtons(self):
def updateButtons(self) -> None:
self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0)
self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)
def updateButtonsAsPerDimensions(self, previous_same_dimensions):
def updateButtonsAsPerDimensions(self, previous_same_dimensions: bool) -> None:
if not self.same_dimensions:
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
@@ -323,7 +334,7 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
@pyqtSlot()
def zoomBestFit(self):
def zoomBestFit(self) -> None:
"""Setup before scaling to bestfit"""
self.setBestFit(True)
self.current_scale = 1.0
@@ -352,7 +363,7 @@ class BaseController(QObject):
self.referenceViewer.bestFit = value
@pyqtSlot()
def zoomNormalSize(self):
def zoomNormalSize(self) -> None:
self.setBestFit(False)
self.current_scale = 1.0
@@ -373,14 +384,14 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(True)
def centerViews(self, only_selected=False):
def centerViews(self, only_selected: bool = False) -> None:
self.selectedViewer.centerViewAndUpdate()
if only_selected:
return
self.referenceViewer.centerViewAndUpdate()
@pyqtSlot()
def swapImages(self):
def swapImages(self) -> None:
# swap the columns in the details table as well
self.parent.tableView.horizontalHeader().swapSections(0, 1)
@@ -388,17 +399,17 @@ class BaseController(QObject):
class QWidgetController(BaseController):
"""Specialized version for QWidget-based viewers."""
def __init__(self, parent):
def __init__(self, parent: QObject) -> None:
super().__init__(parent)
def _updateImage(self, *args):
def _updateImage(self, *args) -> Union[QSize, None]:
ret = super()._updateImage(*args)
# Fix alignment when resizing window
self.centerViews()
return ret
@pyqtSlot(QPointF)
def onDraggedMouse(self, delta):
def onDraggedMouse(self, delta) -> None:
if not self.same_dimensions:
return
if self.sender() is self.referenceViewer:
@@ -407,7 +418,7 @@ class QWidgetController(BaseController):
self.referenceViewer.onDraggedMouse(delta)
@pyqtSlot()
def swapImages(self):
def swapImages(self) -> None:
self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap)
self.selectedViewer.centerViewAndUpdate()
self.referenceViewer.centerViewAndUpdate()
@@ -417,15 +428,15 @@ class QWidgetController(BaseController):
class ScrollAreaController(BaseController):
"""Specialized version fro QLabel-based viewers."""
def __init__(self, parent):
def __init__(self, parent: QObject) -> None:
super().__init__(parent)
def _setupConnections(self):
def _setupConnections(self) -> None:
super()._setupConnections()
self.selectedViewer.connectScrollBars()
self.referenceViewer.connectScrollBars()
def updateBothImages(self, same_group=False):
def updateBothImages(self, same_group: bool = False) -> None:
super().updateBothImages(same_group)
if not self.referenceViewer.isEnabled():
return
@@ -433,7 +444,7 @@ class ScrollAreaController(BaseController):
self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())
@pyqtSlot(QPoint)
def onDraggedMouse(self, delta):
def onDraggedMouse(self, delta) -> None:
self.selectedViewer.ignore_signal = True
self.referenceViewer.ignore_signal = True
@@ -450,21 +461,21 @@ class ScrollAreaController(BaseController):
self.referenceViewer.ignore_signal = False
@pyqtSlot()
def swapImages(self):
def swapImages(self) -> None:
self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)
self.referenceViewer.setCachedPixmap()
self.selectedViewer.setCachedPixmap()
super().swapImages()
@pyqtSlot(float, QPointF)
def onMouseWheel(self, scale, delta):
def onMouseWheel(self, scale: float, delta: QPointF) -> None:
self.scaleImagesAt(scale)
self.selectedViewer.adjustScrollBarsScaled(delta)
# Signal from scrollbars will automatically change the other:
# self.referenceViewer.adjustScrollBarsScaled(delta)
@pyqtSlot(int)
def onVScrollBarChanged(self, value):
def onVScrollBarChanged(self, value: int) -> None:
if not self.same_dimensions:
return
if self.sender() is self.referenceViewer._verticalScrollBar:
@@ -475,7 +486,7 @@ class ScrollAreaController(BaseController):
self.referenceViewer._verticalScrollBar.setValue(value)
@pyqtSlot(int)
def onHScrollBarChanged(self, value):
def onHScrollBarChanged(self, value: int) -> None:
if not self.same_dimensions:
return
if self.sender() is self.referenceViewer._horizontalScrollBar:
@@ -486,13 +497,13 @@ class ScrollAreaController(BaseController):
self.referenceViewer._horizontalScrollBar.setValue(value)
@pyqtSlot(float)
def scaleImagesBy(self, factor):
def scaleImagesBy(self, factor: float) -> None:
super().scaleImagesBy(factor)
# The other is automatically updated via sigals
self.selectedViewer.adjustScrollBarsFactor(factor)
@pyqtSlot()
def zoomBestFit(self):
def zoomBestFit(self) -> None:
# Disable scrollbars to avoid GridLayout size rounding glitch
super().zoomBestFit()
if self.referencePixmap.isNull():

View File

@@ -4,23 +4,21 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable
from PyQt6.QtWidgets import QFormLayout, QCheckBox
from PyQt6.QtCore import Qt
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtCore import Qt
from hscommon.trans import trget
from hscommon.plat import ISLINUX
from qt.preferences import Preferences
from qt.radio_box import RadioBox
from core.scanner import ScanType
from core.app import AppMode
from qt.preferences_dialog import PreferencesDialogBase, Sections
from qt.preferences_dialog import PreferencesDialogBase
tr = trget("ui")
class PreferencesDialog(PreferencesDialogBase):
def _setupPreferenceWidgets(self) -> None:
def _setupPreferenceWidgets(self):
self._setupFilterHardnessBox()
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
@@ -39,12 +37,12 @@ class PreferencesDialog(PreferencesDialogBase):
self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False)
cache_form = QFormLayout()
cache_form.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
cache_form.setLabelAlignment(Qt.AlignLeft)
cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio)
self.widgetsVLayout.addLayout(cache_form)
self._setupBottomPart()
def _setupDisplayPage(self) -> None:
def _setupDisplayPage(self):
super()._setupDisplayPage()
self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
self.details_dialog_override_theme_icons.setToolTip(
@@ -64,7 +62,7 @@ show scrollbars to span the view around"
)
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
def _load(self, prefs, setchecked, section):
setchecked(self.matchScaledBox, prefs.match_scaled)
self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
@@ -75,7 +73,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_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
def _save(self, prefs, ischecked):
prefs.match_scaled = ischecked(self.matchScaledBox)
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)

View File

@@ -4,10 +4,9 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Any, Tuple
from PyQt6.QtWidgets import QApplication, QDockWidget
from PyQt6.QtCore import Qt, QRect, QObject, pyqtSignal
from PyQt6.QtGui import QColor
from PyQt5.QtWidgets import QApplication, QDockWidget
from PyQt5.QtCore import Qt, QRect, QObject, pyqtSignal
from PyQt5.QtGui import QColor
from hscommon import trans
from hscommon.plat import ISLINUX
@@ -127,7 +126,7 @@ class PreferencesBase(QObject):
def set_value(self, name, value):
self._settings.setValue(name, _normalize_for_serialization(value))
def saveGeometry(self, name, widget) -> None:
def saveGeometry(self, name, widget):
# 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
# the widget is docked, third item is a Qt::DockWidgetArea enum value,
@@ -139,12 +138,12 @@ class PreferencesBase(QObject):
rect_as_list = [r.x(), r.y(), r.width(), r.height()]
self.set_value(name, [m, d, area] + rect_as_list)
def restoreGeometry(self, name, widget) -> Tuple[bool, Any]:
def restoreGeometry(self, name, widget):
geometry = self.get_value(name)
if geometry and len(geometry) == 7:
m, d, area, x, y, w, h = geometry
if m:
widget.setWindowState(Qt.WindowState.WindowMaximized)
widget.setWindowState(Qt.WindowMaximized)
else:
r = QRect(x, y, w, h)
widget.setGeometry(r)
@@ -155,7 +154,7 @@ class PreferencesBase(QObject):
class Preferences(PreferencesBase):
def _load_values(self, settings) -> None:
def _load_values(self, settings):
get = self.get_value
self.filter_hardness = get("FilterHardness", self.filter_hardness)
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
@@ -226,7 +225,7 @@ class Preferences(PreferencesBase):
self.match_scaled = get("MatchScaled", self.match_scaled)
self.picture_cache_type = get("PictureCacheType", self.picture_cache_type)
def reset(self) -> None:
def reset(self):
self.filter_hardness = 95
self.mix_file_kind = True
self.use_regexp = False
@@ -248,8 +247,8 @@ class Preferences(PreferencesBase):
# 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_viewers_show_scrollbars = True
self.result_table_ref_foreground_color = QColor(Qt.GlobalColor.blue)
self.result_table_ref_background_color = QColor(Qt.GlobalColor.lightGray)
self.result_table_ref_foreground_color = QColor(Qt.blue)
self.result_table_ref_background_color = QColor(Qt.lightGray)
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
self.resultWindowIsMaximized = False
self.resultWindowRect = None
@@ -277,7 +276,7 @@ class Preferences(PreferencesBase):
self.match_scaled = False
self.picture_cache_type = "sqlite"
def _save_values(self, settings) -> None:
def _save_values(self, settings):
set_ = self.set_value
set_("FilterHardness", self.filter_hardness)
set_("MixFileKind", self.mix_file_kind)

View File

@@ -4,9 +4,9 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Union
from PyQt6.QtCore import Qt, QSize, pyqtSlot
from PyQt6.QtWidgets import (
from typing import Callable, Union, cast
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import (
QDialog,
QDialogButtonBox,
QVBoxLayout,
@@ -29,22 +29,18 @@ from PyQt6.QtWidgets import (
QGroupBox,
QFormLayout,
)
from PyQt6.QtGui import QPixmap, QIcon, QShowEvent
from PyQt5.QtGui import QPixmap, QIcon, QShowEvent
from hscommon import desktop, plat
from hscommon.trans import trget
from hscommon.plat import ISLINUX
from qt import app
from qt.util import horizontal_wrap, move_to_screen_center
from qt.preferences import get_langnames
from enum import Flag, auto
from qt.preferences import Preferences
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from qt.app import DupeGuru
tr = trget("ui")
@@ -58,8 +54,10 @@ class Sections(Flag):
class PreferencesDialogBase(QDialog):
def __init__(self, parent: QWidget, app: "DupeGuru", **kwargs):
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
def __init__(self, parent: QWidget, app: "app.DupeGuru", **kwargs) -> None:
flags = Qt.WindowType(
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
)
super().__init__(parent, flags, **kwargs)
self.app = app
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
@@ -82,7 +80,7 @@ class PreferencesDialogBase(QDialog):
self.filterHardnessHLayoutSub1 = QHBoxLayout()
self.filterHardnessHLayoutSub1.setSpacing(12)
self.filterHardnessSlider = QSlider(self)
size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth())
@@ -102,7 +100,7 @@ class PreferencesDialogBase(QDialog):
self.moreResultsLabel = QLabel(self)
self.moreResultsLabel.setText(tr("More Results"))
self.filterHardnessHLayoutSub2.addWidget(self.moreResultsLabel)
spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.filterHardnessHLayoutSub2.addItem(spacer_item)
self.fewerResultsLabel = QLabel(self)
self.fewerResultsLabel.setText(tr("Fewer Results"))
@@ -227,8 +225,8 @@ use the modifier key to drag the floating window around"
self.debugVLayout.addWidget(self.profile_scan_box)
self.debug_location_label = QLabel(
tr('Logs located in: <a href="{}">{}</a>').format(self.app.model.appdata, self.app.model.appdata),
wordWrap=True,
)
self.debug_location_label.setWordWrap(True)
self.debugVLayout.addWidget(self.debug_location_label)
def _setupAddCheckbox(self, name: str, label: str, parent: Union[QWidget, None] = None) -> None:
@@ -238,7 +236,7 @@ use the modifier key to drag the floating window around"
cb.setText(label)
setattr(self, name, cb)
def _setupPreferenceWidgets(self):
def _setupPreferenceWidgets(self) -> None:
# Edition-specific
pass
@@ -264,9 +262,7 @@ use the modifier key to drag the floating window around"
# self.mainVLayout.addLayout(self.widgetsVLayout)
self.buttonBox = QDialogButtonBox(self)
self.buttonBox.setStandardButtons(
QDialogButtonBox.StandardButton.Cancel
| QDialogButtonBox.StandardButton.Ok
| QDialogButtonBox.StandardButton.RestoreDefaults
QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.RestoreDefaults
)
self.mainVLayout.addWidget(self.tabwidget)
self.mainVLayout.addWidget(self.buttonBox)
@@ -278,15 +274,15 @@ use the modifier key to drag the floating window around"
self.widgetsVLayout.addStretch(0)
self.debugVLayout.addStretch(0)
def _load(self, prefs, setchecked, section) -> None:
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
# Edition-specific
pass
def _save(self, prefs, ischecked) -> None:
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
# Edition-specific
pass
def load(self, prefs: Preferences = None, section: Sections = Sections.ALL) -> None:
def load(self, prefs: Union[Preferences, None] = None, section: Sections = Sections.ALL) -> None:
if prefs is None:
prefs = self.app.prefs
@@ -375,9 +371,9 @@ use the modifier key to drag the floating window around"
self.load(Preferences(), section_to_update)
# --- Events
def buttonClicked(self, button: QPushButton) -> None:
def buttonClicked(self, button: QDialogButtonBox) -> None:
role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ButtonRole.ResetRole:
if role == QDialogButtonBox.ResetRole:
current_tab = self.tabwidget.currentWidget()
section_to_update = Sections.ALL
if current_tab is self.page_general:
@@ -397,13 +393,14 @@ use the modifier key to drag the floating window around"
class ColorPickerButton(QPushButton):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
self.setParent(parent)
self.color = None
self.clicked.connect(self.onClicked)
@pyqtSlot()
def onClicked(self) -> None:
color = QColorDialog.getColor(
self.color if self.color is not None else Qt.GlobalColor.white, self.parentWidget()
self.color if self.color is not None else Qt.GlobalColor.white, cast(QWidget, self.parent())
)
self.setColor(color)

View File

@@ -6,9 +6,8 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QShowEvent
from PyQt6.QtWidgets import (
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
@@ -18,10 +17,8 @@ from PyQt6.QtWidgets import (
QLabel,
QTableView,
QAbstractItemView,
QWidget,
)
from core.gui.problem_dialog import ProblemDialog as ProblemDiaglogModel
from qt.util import move_to_screen_center
from hscommon.trans import trget
from qt.problem_table import ProblemTable
@@ -30,56 +27,52 @@ tr = trget("ui")
class ProblemDialog(QDialog):
def __init__(self, parent: QWidget, model: ProblemDiaglogModel, **kwargs) -> None:
flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
def __init__(self, parent, model, **kwargs):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
super().__init__(parent, flags, **kwargs)
self.model = model
self.table_view = QTableView(self)
self.table = ProblemTable(self.model.problem_table, view=self.table_view)
self._setupUi()
self.model = model
self.model.view = self
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
def _setupUi(self) -> None:
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
self.closeButton.clicked.connect(self.accept)
def _setupUi(self):
self.setWindowTitle(tr("Problems!"))
self.resize(413, 323)
main_layout = QVBoxLayout(self)
notice_label = QLabel(self)
self.verticalLayout = QVBoxLayout(self)
self.label = QLabel(self)
msg = tr(
"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 "
"removed from your results."
)
notice_label.setText(msg)
notice_label.setWordWrap(True)
main_layout.addWidget(notice_label)
self.label.setText(msg)
self.label.setWordWrap(True)
self.verticalLayout.addWidget(self.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)
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:
def showEvent(self, event):
# have to do this here as the frameGeometry is not correct until shown
move_to_screen_center(self)
super().showEvent(event)

View File

@@ -4,23 +4,29 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable
from PyQt6.QtCore import QSize
from PyQt6.QtWidgets import QSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import (
QSpinBox,
QVBoxLayout,
QHBoxLayout,
QLabel,
QSizePolicy,
QSpacerItem,
QWidget,
)
from hscommon.trans import trget
from core.app import AppMode
from core.scanner import ScanType
from qt.preferences import Preferences
from qt.preferences_dialog import PreferencesDialogBase, Sections
from qt.preferences_dialog import PreferencesDialogBase
tr = trget("ui")
class PreferencesDialog(PreferencesDialogBase):
def _setupPreferenceWidgets(self) -> None:
def _setupPreferenceWidgets(self):
self._setupFilterHardnessBox()
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
self.widget = QWidget(self)
@@ -44,7 +50,7 @@ class PreferencesDialog(PreferencesDialogBase):
self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
self.sizeThresholdSpinBox = QSpinBox(self.widget)
size_policy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
@@ -55,14 +61,14 @@ class PreferencesDialog(PreferencesDialogBase):
self.label_6 = QLabel(self.widget)
self.label_6.setText(tr("KB"))
self.horizontalLayout_2.addWidget(self.label_6)
spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacer_item1)
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
self.horizontalLayout_2a = QHBoxLayout()
self._setupAddCheckbox("ignoreLargeFilesBox", tr("Ignore files larger than"), self.widget)
self.horizontalLayout_2a.addWidget(self.ignoreLargeFilesBox)
self.sizeSaturationSpinBox = QSpinBox(self.widget)
size_policy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.sizeSaturationSpinBox.setSizePolicy(size_policy)
self.sizeSaturationSpinBox.setMaximumSize(QSize(300, 16777215))
self.sizeSaturationSpinBox.setRange(0, 1000000)
@@ -70,7 +76,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.label_6a = QLabel(self.widget)
self.label_6a.setText(tr("MB"))
self.horizontalLayout_2a.addWidget(self.label_6a)
spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2a.addItem(spacer_item3)
self.verticalLayout_4.addLayout(self.horizontalLayout_2a)
self.horizontalLayout_2b = QHBoxLayout()
@@ -88,7 +94,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.label_6b = QLabel(self.widget)
self.label_6b.setText(tr("MB"))
self.horizontalLayout_2b.addWidget(self.label_6b)
spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2b.addItem(spacer_item2)
self.verticalLayout_4.addLayout(self.horizontalLayout_2b)
self._setupAddCheckbox(
@@ -100,7 +106,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.widget)
self._setupBottomPart()
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
def _load(self, prefs, setchecked, section):
setchecked(self.matchSimilarBox, prefs.match_similar)
setchecked(self.wordWeightingBox, prefs.word_weighting)
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
@@ -117,7 +123,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.matchSimilarBox.setEnabled(word_based)
self.wordWeightingBox.setEnabled(word_based)
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
def _save(self, prefs, ischecked):
prefs.match_similar = ischecked(self.matchSimilarBox)
prefs.word_weighting = ischecked(self.wordWeightingBox)
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)

View File

@@ -11,18 +11,22 @@ import io
import os.path as op
import os
import logging
from typing import List, Union
from core.util import executable_folder
from hscommon.util import first
from hscommon.plat import ISWINDOWS
from PyQt6.QtCore import QStandardPaths, QSettings
from PyQt6.QtGui import QPixmap, QIcon, QGuiApplication, QAction
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy, QHBoxLayout, QWidget
from PyQt5.QtCore import QStandardPaths, QSettings
from PyQt5.QtGui import QPixmap, QIcon, QGuiApplication
from PyQt5.QtWidgets import (
QSpacerItem,
QSizePolicy,
QAction,
QHBoxLayout,
)
def move_to_screen_center(widget: QWidget) -> None:
def move_to_screen_center(widget):
frame = widget.frameGeometry()
if QGuiApplication.screenAt(frame.center()) is None:
# if center not on any screen use default screen
@@ -39,21 +43,21 @@ def move_to_screen_center(widget: QWidget) -> None:
widget.move(frame.topLeft())
def vertical_spacer(size: Union[int, None] = None) -> QSpacerItem:
def vertical_spacer(size=None):
if size:
return QSpacerItem(1, size, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed)
else:
return QSpacerItem(1, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding)
return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
def horizontal_spacer(size: Union[int, None] = None) -> QSpacerItem:
def horizontal_spacer(size=None):
if size:
return QSpacerItem(size, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed)
else:
return QSpacerItem(1, 1, QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed)
return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
def horizontal_wrap(widgets: List[Union[QWidget, int, None]]) -> QHBoxLayout:
def horizontal_wrap(widgets):
"""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
@@ -73,7 +77,7 @@ def create_actions(actions, target):
for name, shortcut, icon, desc, func in actions:
action = QAction(target)
if icon:
action.setIcon(QIcon(QPixmap(":/" + icon))) # TODO stop using qrc file path
action.setIcon(QIcon(QPixmap(":/" + icon)))
if shortcut:
action.setShortcut(shortcut)
action.setText(desc)
@@ -96,11 +100,11 @@ def set_accel_keys(menu):
action.setText(newtext)
def get_appdata(portable: bool = False) -> str:
def get_appdata(portable=False):
if portable:
return op.join(executable_folder(), "data")
else:
return QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)[0]
return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0]
class SysWrapper(io.IOBase):
@@ -136,18 +140,18 @@ def escape_amp(s):
return s.replace("&", "&&")
def create_qsettings() -> QSettings:
def create_qsettings():
# Create a QSettings instance with the correct arguments.
config_location = op.join(executable_folder(), "settings.ini")
if op.isfile(config_location):
settings = QSettings(config_location, QSettings.Format.IniFormat)
settings = QSettings(config_location, QSettings.IniFormat)
settings.setValue("Portable", True)
elif ISWINDOWS:
# 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.
locations = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)
locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)
if locations:
settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.Format.IniFormat)
settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat)
else:
settings = QSettings()
settings.setValue("Portable", False)

View File

@@ -1,4 +1,4 @@
pytest>=6,<7.2
pytest>=6,<7
flake8
black
pyinstaller>=4.5,<5.0; sys_platform != 'linux'

View File

@@ -1,9 +1,9 @@
distro>=1.5.0, <2.0
mutagen>=1.44.0, <2.0
polib>=1.1.0, <2.0
PyQt6 >=6.3,<7.0; sys_platform != 'linux'
distro>=1.5.0
mutagen>=1.44.0
polib>=1.1.0
PyQt5 >=5.14.1,<6.0; sys_platform != 'linux'
pywin32>=228; sys_platform == 'win32'
semantic-version>=2.0.0,<3.0.0
Send2Trash>=1.3.0
sphinx>=5.0.0, <6.0
sphinx>=3.0.0
xxhash>=3.0.0,<4.0.0

17
run.py
View File

@@ -9,9 +9,9 @@ import sys
import os.path as op
import gc
from PyQt6.QtCore import QDir
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QApplication
from hscommon.trans import install_gettext_trans_under_qt
from qt.error_report_dialog import install_excepthook
@@ -48,10 +48,9 @@ def setup_signals():
def main():
app = QApplication(sys.argv)
QApplication.setOrganizationName("Hardcoded Software")
QApplication.setApplicationName(__appname__)
QApplication.setApplicationVersion(__version__)
QDir.addSearchPath("images", op.join(BASE_PATH, "images"))
QCoreApplication.setOrganizationName("Hardcoded Software")
QCoreApplication.setApplicationName(__appname__)
QCoreApplication.setApplicationVersion(__version__)
setup_qt_logging()
settings = create_qsettings()
lang = settings.value("Language")
@@ -62,7 +61,7 @@ def main():
# Let the Python interpreter runs every 500ms to handle signals. This is
# required because Python cannot handle signals while the Qt event loop is
# running.
from PyQt6.QtCore import QTimer
from PyQt5.QtCore import QTimer
timer = QTimer()
timer.start(500)
@@ -71,7 +70,7 @@ def main():
# has been installed
from qt.app import DupeGuru
app.setWindowIcon(QIcon(f"images:{DupeGuru.LOGO_NAME}_32.png"))
app.setWindowIcon(QIcon(QPixmap(f":/{DupeGuru.LOGO_NAME}")))
global dgapp
dgapp = DupeGuru()
install_excepthook("https://github.com/arsenetar/dupeguru/issues")

View File

@@ -32,15 +32,15 @@ install_requires =
Send2Trash>=1.3.0
mutagen>=1.45.1
distro>=1.5.0
PyQt6 >=6.3.0,<7.0; sys_platform != 'linux'
PyQt5 >=5.14.1,<6.0; sys_platform != 'linux'
pywin32>=228; sys_platform == 'win32'
semantic-version>=2.0.0,<3.0.0
xxhash>=3.0.0,<4.0.0
setup_requires =
sphinx>=5.0.0
sphinx>=3.0.0
polib>=1.1.0
tests_require =
pytest >=7,<8
pytest >=6,<7
include_package_data = true
[options.entry_points]

View File

@@ -245,7 +245,7 @@ Section "Uninstall"
; Remove Files & Folders in Install Folder
RMDir /r "$INSTDIR\core"
RMDir /r "$INSTDIR\help"
RMDir /r "$INSTDIR\PyQt6"
RMDir /r "$INSTDIR\PyQt5"
RMDir /r "$INSTDIR\qt"
RMDir /r "$INSTDIR\locale"
Delete "$INSTDIR\*.exe"