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

Compare commits

..

7 Commits

Author SHA1 Message Date
1f1dfa88dc Update version & changelog for 4.3.1 release 2022-07-07 22:06:06 -05:00
916c5204cf Update translations from transifex 2022-07-07 21:57:59 -05:00
71af825b37 Move try/except of cache db to get() and put()
- Move the try/except of cache db calls to the calls themselves.
- Add some additional information to logging statements on cache db
  exception to improve troubleshooting.
2022-07-07 21:52:22 -05:00
97f490b8b7 Fix typo in engine.py 2022-07-07 19:06:35 -05:00
d369bcddd7 Updates from investigation of #1015
- Add protection for empty hash digests in comparison of non-zero size
  files
- Bump version to 4.3.1-dev for identification
2022-07-07 19:00:09 -05:00
360dceca7b Update to version 4.3.0, update changelog 2022-06-30 23:27:14 -05:00
92b27801c3 Update translations, remove iphoto_plist.py 2022-06-30 23:03:40 -05:00
13 changed files with 261 additions and 286 deletions

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -2,15 +2,16 @@
# Andrew Senetar <arsenetar@gmail.com>, 2022
# Emanuele, 2022
# Fuan <jcfrt@posteo.net>, 2022
# Giovanni, 2022
#
msgid ""
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: it\n"
"Content-Type: text/plain; charset=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
msgid "Quit"
@@ -979,37 +980,40 @@ msgstr "Ignora file più grandi di"
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
msgstr "Svuota cache"
#: 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 ""
msgstr "Cache svuotata"
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""
msgstr "Usa stile scuro"
#: qt\preferences_dialog.py:241
msgid "Profile scan operation"
msgstr ""
msgstr "Profila l'operazione di scansione"
#: 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 ""
msgstr "I log si trovano in: <a href=\"{}\">{}</a>"
#: qt\preferences_dialog.py:291
msgid "Debug"
msgstr ""
msgstr "Debug"
#: qt\about_box.py:31
msgid "About {}"
@@ -1021,7 +1025,7 @@ msgstr "Versione {}"
#: qt\about_box.py:49 qt\about_box.py:75
msgid "Checking for updates..."
msgstr ""
msgstr "Controllo degli aggiornamenti..."
#: qt\about_box.py:54
msgid "Licensed under GPLv3"
@@ -1029,11 +1033,11 @@ msgstr "Distribuito sotto licenza GPLv3"
#: qt\about_box.py:68
msgid "No update available."
msgstr ""
msgstr "Nessun aggiornamento disponibile."
#: qt\about_box.py:71
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
msgid "Error Report"

View File

@@ -1,138 +1,139 @@
# Translators:
# Fuan <jcfrt@posteo.net>, 2021
# Yuji Sasaki, 2022
# Fuan <jcfrt@posteo.net>, 2022
#
msgid ""
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: 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:42
#: core\app.py:44
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."
msgstr "選択された重複はありません。 何も行われていません。"
#: core\app.py:44
#: core\app.py:46
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:71
#: core\app.py:73
msgid "Scanning for duplicates"
msgstr "重複のスキャン"
#: core\app.py:72
#: core\app.py:74
msgid "Loading"
msgstr "読み込み中"
#: core\app.py:73
#: core\app.py:75
msgid "Moving"
msgstr "移動します"
#: core\app.py:74
#: core\app.py:76
msgid "Copying"
msgstr "コピー中"
#: core\app.py:75
#: core\app.py:77
msgid "Sending to Trash"
msgstr "ごみ箱に送信します"
#: core\app.py:289
#: core\app.py:291
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:300
#: core\app.py:302
msgid "No duplicates found."
msgstr "重複は見つかりませんでした。"
#: core\app.py:315
msgid "All marked files were copied successfully."
msgstr "マークされたファイルはすべて正常にコピーされました。"
#: core\app.py:317
msgid "All marked files were moved successfully."
msgstr "マークされたファイルすべて正常に移動されました。"
msgid "All marked files were copied successfully."
msgstr "チェックを入れたファイルすべてコピーしました。"
#: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
msgid "All marked files were moved successfully."
msgstr "チェックを入れたファイルをすべて移動しました。"
#: core\app.py:321
msgid "All marked files were successfully sent to Trash."
msgstr "マークされたファイルすべてごみ箱に正常に送信されました。"
msgid "All marked files were deleted successfully."
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: {}"
msgstr "ファイルを読み込めませんでした:{}"
#: core\app.py:382
#: core\app.py:384
msgid "'{}' already is in the list."
msgstr "「{}」既にリストに含まれています。"
#: core\app.py:384
#: core\app.py:386
msgid "'{}' does not exist."
msgstr "'{}' 存在しません。"
#: core\app.py:392
#: core\app.py:394
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
#: core\app.py:469
#: core\app.py:471
msgid "Select a directory to copy marked files to"
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
#: core\app.py:471
#: core\app.py:473
msgid "Select a directory to move marked files to"
msgstr "マークされたファイルを移動するディレクトリを選択してください"
#: core\app.py:510
#: core\app.py:512
msgid "Select a destination for your exported 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: {}"
msgstr "ファイルに書き込めませんでした:{}"
#: core\app.py:539
#: core\app.py:541
msgid "You have no custom command set up. Set it up in your preferences."
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?"
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
#: core\app.py:743
#: core\app.py:745
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
#: core\app.py:790
#: core\app.py:792
msgid "The selected directories contain no scannable file."
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
#: core\app.py:803
#: core\app.py:808
msgid "Collecting files to scan"
msgstr "スキャンするファイルを収集しています"
#: core\app.py:850
#: core\app.py:858
msgid "%s (%d discarded)"
msgstr "%s (%d 廃棄)"
#: core\directories.py:191
#: core\directories.py:190
msgid "Collected {} files to scan"
msgstr ""
#: core\directories.py:207
#: core\directories.py:206
msgid "Collected {} folders to scan"
msgstr ""
@@ -200,35 +201,35 @@ msgstr "EXIFタイムスタンプ"
msgid "None"
msgstr "無し"
#: core\prioritize.py:100
#: core\prioritize.py:102
msgid "Ends with number"
msgstr "番号で終わっている"
#: core\prioritize.py:101
#: core\prioritize.py:103
msgid "Doesn't end with number"
msgstr "数字で終わっていない"
#: core\prioritize.py:102
#: core\prioritize.py:104
msgid "Longest"
msgstr "最長"
#: core\prioritize.py:103
#: core\prioritize.py:105
msgid "Shortest"
msgstr "最短"
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Highest"
msgstr "最高"
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Lowest"
msgstr "最低"
#: core\prioritize.py:169
#: core\prioritize.py:171
msgid "Newest"
msgstr "最新"
#: core\prioritize.py:169
#: core\prioritize.py:171
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

@@ -15,7 +15,7 @@ tr = trget("ui")
class DetailsDialog(DetailsDialogBase):
def _setupUi(self) -> None:
def _setupUi(self):
self.setWindowTitle(tr("Details"))
self.resize(502, 295)
self.setMinimumSize(QSize(250, 250))

View File

@@ -4,22 +4,27 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
from PyQt5.QtWidgets import (
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)
@@ -65,7 +70,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
self._setupBottomPart()
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
def _load(self, prefs, setchecked, section):
setchecked(self.tagTrackBox, prefs.scan_tag_track)
setchecked(self.tagArtistBox, prefs.scan_tag_artist)
setchecked(self.tagAlbumBox, prefs.scan_tag_album)
@@ -94,7 +99,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.tagGenreBox.setEnabled(tag_based)
self.tagYearBox.setEnabled(tag_based)
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
def _save(self, prefs, ischecked):
prefs.scan_tag_track = ischecked(self.tagTrackBox)
prefs.scan_tag_artist = ischecked(self.tagArtistBox)
prefs.scan_tag_album = ischecked(self.tagAlbumBox)

View File

@@ -4,12 +4,10 @@
# 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 PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtWidgets import QWidget, QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import 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
@@ -18,15 +16,15 @@ tr = trget("ui")
class DetailsDialog(DetailsDialogBase):
def __init__(self, parent: QWidget, app: "app.DupeGuru") -> None:
self.vController: Union[ScrollAreaController, None] = None
def __init__(self, parent, app):
self.vController = None
super().__init__(parent, app)
def _setupUi(self) -> None:
def _setupUi(self):
self.setWindowTitle(tr("Details"))
self.resize(502, 502)
self.setMinimumSize(QSize(250, 250))
self.splitter = QSplitter(Qt.Orientation.Vertical)
self.splitter = QSplitter(Qt.Vertical)
self.topFrame = EmittingFrame()
self.topFrame.setFrameShape(QFrame.StyledPanel)
self.horizontalLayout = QGridLayout()
@@ -49,8 +47,8 @@ class DetailsDialog(DetailsDialogBase):
self.vController = ScrollAreaController(self)
self.verticalToolBar = ViewerToolBar(self, self.vController)
self.verticalToolBar.setOrientation(Qt.Orientation.Vertical)
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignmentFlag.AlignCenter)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
@@ -75,7 +73,7 @@ class DetailsDialog(DetailsDialogBase):
self.topFrame.resized.connect(self.resizeEvent)
def _update(self) -> None:
def _update(self):
if self.vController is None: # Not yet constructed!
return
if not self.app.model.selected_dupes:
@@ -89,14 +87,15 @@ class DetailsDialog(DetailsDialogBase):
self.vController.updateView(ref, dupe, group)
# --- Override
def resizeEvent(self, event: QResizeEvent) -> None:
@pyqtSlot(QResizeEvent)
def resizeEvent(self, event):
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) -> None:
def show(self):
# Give the splitter a maximum height to reach. This is assuming that
# all rows below their headers have the same height
self.tableView.setMaximumHeight(
@@ -109,7 +108,7 @@ class DetailsDialog(DetailsDialogBase):
self.ensure_same_sizes()
self._update()
def ensure_same_sizes(self) -> None:
def ensure_same_sizes(self):
# 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,
@@ -127,7 +126,7 @@ class DetailsDialog(DetailsDialogBase):
self.selectedImageViewer.resize(self.referenceImageViewer.size())
# model --> view
def refresh(self) -> None:
def refresh(self):
DetailsDialogBase.refresh(self)
if self.isVisible():
self._update()
@@ -138,5 +137,5 @@ class EmittingFrame(QFrame):
resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event: QResizeEvent) -> None:
def resizeEvent(self, event):
self.resized.emit(event)

View File

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

View File

@@ -4,7 +4,6 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable, Union, cast
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import (
QDialog,
@@ -29,12 +28,11 @@ from PyQt5.QtWidgets import (
QGroupBox,
QFormLayout,
)
from PyQt5.QtGui import QPixmap, QIcon, QShowEvent
from PyQt5.QtGui import QPixmap, QIcon
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
@@ -54,10 +52,8 @@ class Sections(Flag):
class PreferencesDialogBase(QDialog):
def __init__(self, parent: QWidget, app: "app.DupeGuru", **kwargs) -> None:
flags = Qt.WindowType(
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
)
def __init__(self, parent, app, **kwargs):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
super().__init__(parent, flags, **kwargs)
self.app = app
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
@@ -69,7 +65,7 @@ class PreferencesDialogBase(QDialog):
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
def _setupFilterHardnessBox(self) -> None:
def _setupFilterHardnessBox(self):
self.filterHardnessHLayout = QHBoxLayout()
self.filterHardnessLabel = QLabel(self)
self.filterHardnessLabel.setText(tr("Filter Hardness:"))
@@ -88,7 +84,7 @@ class PreferencesDialogBase(QDialog):
self.filterHardnessSlider.setMinimum(1)
self.filterHardnessSlider.setMaximum(100)
self.filterHardnessSlider.setTracking(True)
self.filterHardnessSlider.setOrientation(Qt.Orientation.Horizontal)
self.filterHardnessSlider.setOrientation(Qt.Horizontal)
self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)
self.filterHardnessLabel = QLabel(self)
self.filterHardnessLabel.setText("100")
@@ -108,7 +104,7 @@ class PreferencesDialogBase(QDialog):
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
def _setupBottomPart(self) -> None:
def _setupBottomPart(self):
# The bottom part of the pref panel is always the same in all editions.
self.copyMoveLabel = QLabel(self)
self.copyMoveLabel.setText(tr("Copy and Move:"))
@@ -124,7 +120,7 @@ class PreferencesDialogBase(QDialog):
self.customCommandEdit = QLineEdit(self)
self.widgetsVLayout.addWidget(self.customCommandEdit)
def _setupDisplayPage(self) -> None:
def _setupDisplayPage(self):
self.ui_groupbox = QGroupBox("&" + tr("General Interface"))
layout = QVBoxLayout()
self.languageLabel = QLabel(tr("Language:"), self)
@@ -175,7 +171,7 @@ On MacOS, the tab bar will fill up the window's width instead."
formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color)
self.result_table_delta_foreground_color = ColorPickerButton(self)
formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color)
formlayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
formlayout.setLabelAlignment(Qt.AlignLeft)
# Keep same vertical spacing as parent layout for consistency
formlayout.setVerticalSpacing(self.displayVLayout.spacing())
@@ -217,7 +213,7 @@ use the modifier key to drag the floating window around"
details_groupbox.setLayout(self.details_groupbox_layout)
self.displayVLayout.addWidget(details_groupbox)
def _setupDebugPage(self) -> None:
def _setupDebugPage(self):
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization."))
@@ -225,22 +221,22 @@ use the modifier key to drag the floating window around"
self.debugVLayout.addWidget(self.profile_scan_box)
self.debug_location_label = QLabel(
tr('Logs located in: <a href="{}">{}</a>').format(self.app.model.appdata, self.app.model.appdata),
wordWrap=True,
)
self.debug_location_label.setWordWrap(True)
self.debugVLayout.addWidget(self.debug_location_label)
def _setupAddCheckbox(self, name: str, label: str, parent: Union[QWidget, None] = None) -> None:
def _setupAddCheckbox(self, name, label, parent=None):
if parent is None:
parent = self
cb = QCheckBox(parent)
cb.setText(label)
setattr(self, name, cb)
def _setupPreferenceWidgets(self) -> None:
def _setupPreferenceWidgets(self):
# Edition-specific
pass
def _setupUi(self) -> None:
def _setupUi(self):
self.setWindowTitle(tr("Options"))
self.setSizeGripEnabled(False)
self.setModal(True)
@@ -266,7 +262,7 @@ use the modifier key to drag the floating window around"
)
self.mainVLayout.addWidget(self.tabwidget)
self.mainVLayout.addWidget(self.buttonBox)
self.layout().setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
self.layout().setSizeConstraint(QLayout.SetFixedSize)
self.tabwidget.addTab(self.page_general, tr("General"))
self.tabwidget.addTab(self.page_display, tr("Display"))
self.tabwidget.addTab(self.page_debug, tr("Debug"))
@@ -274,20 +270,20 @@ use the modifier key to drag the floating window around"
self.widgetsVLayout.addStretch(0)
self.debugVLayout.addStretch(0)
def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
def _load(self, prefs, setchecked, section):
# Edition-specific
pass
def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
def _save(self, prefs, ischecked):
# Edition-specific
pass
def load(self, prefs: Union[Preferences, None] = None, section: Sections = Sections.ALL) -> None:
def load(self, prefs=None, section=Sections.ALL):
if prefs is None:
prefs = self.app.prefs
def setchecked(cb: QCheckBox, b: bool) -> None:
cb.setCheckState(Qt.CheckState.Checked if b else Qt.CheckState.Unchecked)
def setchecked(cb, b):
cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
if section & Sections.GENERAL:
self.filterHardnessSlider.setValue(prefs.filter_hardness)
@@ -327,12 +323,12 @@ use the modifier key to drag the floating window around"
setchecked(self.profile_scan_box, prefs.profile_scan)
self._load(prefs, setchecked, section)
def save(self) -> None:
def save(self):
prefs = self.app.prefs
prefs.filter_hardness = self.filterHardnessSlider.value()
def ischecked(cb: QCheckBox) -> bool:
return cb.checkState() == Qt.CheckState.Checked
def ischecked(cb):
return cb.checkState() == Qt.Checked
prefs.mix_file_kind = ischecked(self.mixFileKindBox)
prefs.use_regexp = ischecked(self.useRegexpBox)
@@ -367,11 +363,11 @@ use the modifier key to drag the floating window around"
self.app.prefs.language = lang_code
self._save(prefs, ischecked)
def resetToDefaults(self, section_to_update: Sections) -> None:
def resetToDefaults(self, section_to_update):
self.load(Preferences(), section_to_update)
# --- Events
def buttonClicked(self, button: QDialogButtonBox) -> None:
def buttonClicked(self, button):
role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ResetRole:
current_tab = self.tabwidget.currentWidget()
@@ -384,32 +380,30 @@ use the modifier key to drag the floating window around"
section_to_update = Sections.DEBUG
self.resetToDefaults(section_to_update)
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)
class ColorPickerButton(QPushButton):
def __init__(self, parent: QWidget) -> None:
def __init__(self, parent):
super().__init__(parent)
self.setParent(parent)
self.parent = 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, cast(QWidget, self.parent())
)
def onClicked(self):
color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent)
self.setColor(color)
def setColor(self, color) -> None:
def setColor(self, color):
size = QSize(16, 16)
px = QPixmap(size)
if color is None:
size.setWidth(0)
size.setHeight(0)
size.width = 0
size.height = 0
elif not color.isValid():
return
else: