mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-24 23:51:38 +00:00
Compare commits
2 Commits
feature/gu
...
4.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
360dceca7b
|
|||
|
92b27801c3
|
@@ -1,2 +1,2 @@
|
||||
__version__ = "4.2.1"
|
||||
__version__ = "4.3.0"
|
||||
__appname__ = "dupeGuru"
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2014-03-15
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import plistlib
|
||||
|
||||
|
||||
class IPhotoPlistParser(plistlib._PlistParser):
|
||||
"""A parser for iPhoto plists.
|
||||
|
||||
iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more
|
||||
lenient.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
plistlib._PlistParser.__init__(self, use_builtin_types=True, dict_type=dict)
|
||||
# For debugging purposes, we remember the last bit of data to be analyzed so that we can
|
||||
# log it in case of an exception
|
||||
self.lastdata = ""
|
||||
|
||||
def get_data(self):
|
||||
self.lastdata = plistlib._PlistParser.get_data(self)
|
||||
return self.lastdata
|
||||
|
||||
def end_integer(self):
|
||||
try:
|
||||
self.add_object(int(self.get_data()))
|
||||
except ValueError:
|
||||
self.add_object(0)
|
||||
@@ -1,3 +1,16 @@
|
||||
=== 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)
|
||||
|
||||
@@ -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 "最古"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user