1# Copyright (C) 2017-2020 Damon Lynch <damonlynch@gmail.com> 2 3# This file is part of Rapid Photo Downloader. 4# 5# Rapid Photo Downloader is free software: you can redistribute it and/or 6# modify it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Rapid Photo Downloader is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Rapid Photo Downloader. If not, 17# see <http://www.gnu.org/licenses/>. 18 19""" 20Dialog window to show and manipulate selected user preferences 21""" 22 23__author__ = 'Damon Lynch' 24__copyright__ = "Copyright 2017-2020, Damon Lynch" 25 26import webbrowser 27from typing import List 28import logging 29 30 31from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal, QObject, QThread, QTimer, QSize) 32from PyQt5.QtWidgets import ( 33 QWidget, QSizePolicy, QComboBox, QVBoxLayout, QLabel, QLineEdit, QSpinBox, QGridLayout, 34 QAbstractItemView, QListWidgetItem, QHBoxLayout, QDialog, QDialogButtonBox, QCheckBox, 35 QStyle, QStackedWidget, QApplication, QPushButton, QGroupBox, QFormLayout, QMessageBox, 36 QButtonGroup, QRadioButton, QAbstractButton 37) 38from PyQt5.QtGui import ( 39 QShowEvent, QCloseEvent, QMouseEvent, QIcon, QFont, QFontMetrics, QPixmap, QPalette 40) 41 42from raphodo.preferences import Preferences 43from raphodo.constants import ( 44 KnownDeviceType, CompletedDownloads, TreatRawJpeg, MarkRawJpeg 45) 46from raphodo.viewutils import QNarrowListWidget, translateDialogBoxButtons, standardMessageBox 47from raphodo.utilities import available_cpu_count, format_size_for_user, thousands 48from raphodo.cache import ThumbnailCacheSql 49from raphodo.constants import ConflictResolution 50from raphodo.utilities import ( 51 current_version_is_dev_version, make_internationalized_list, version_check_disabled, 52 available_languages 53) 54from raphodo.fileformats import ( 55 PHOTO_EXTENSIONS, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, VIDEO_THUMBNAIL_EXTENSIONS, 56 ALL_KNOWN_EXTENSIONS 57) 58import raphodo.qrc_resources as qrc_resources 59 60 61class ClickableLabel(QLabel): 62 clicked = pyqtSignal() 63 64 def mousePressEvent(self, event: QMouseEvent) -> None: 65 self.clicked.emit() 66 67 68consolidation_implemented = False 69# consolidation_implemented = True 70 71system_language = 'SYSTEM' 72 73 74class PreferencesDialog(QDialog): 75 """ 76 Preferences dialog for those preferences that are not adjusted via the main window 77 78 Note: 79 80 When pref value generate_thumbnails is made False, pref values use_thumbnail_cache and 81 generate_thumbnails are not changed, even though the preference value shown to the user 82 shows False (to indicate that the activity will not occur). 83 """ 84 85 getCacheSize = pyqtSignal() 86 87 def __init__(self, prefs: Preferences, parent=None) -> None: 88 super().__init__(parent=parent) 89 90 self.rapidApp = parent 91 92 self.setWindowTitle(_('Preferences')) 93 94 self.prefs = prefs 95 96 self.is_prerelease = current_version_is_dev_version() 97 98 self.panels = QStackedWidget() 99 100 self.chooser = QNarrowListWidget(no_focus_recentangle=True) 101 102 font = QFont() 103 fontMetrics = QFontMetrics(font) 104 icon_padding = 6 105 icon_height = max(fontMetrics.height(), 16) 106 icon_width = icon_height + icon_padding 107 self.chooser.setIconSize(QSize(icon_width, icon_height)) 108 109 palette = QPalette() 110 selectedColour = palette.color(palette.HighlightedText) 111 112 if consolidation_implemented: 113 self.chooser_items = ( 114 _('Devices'), _('Language'), _('Automation'), _('Thumbnails'), _('Error Handling'), 115 _('Warnings'), _('Consolidation'), _('Miscellaneous') 116 ) 117 icons = ( 118 ":/prefs/devices.svg", ":/prefs/language.svg", ":/prefs/automation.svg", 119 ":/prefs/thumbnails.svg", ":/prefs/error-handling.svg", ":/prefs/warnings.svg", 120 ":/prefs/consolidation.svg", ":/prefs/miscellaneous.svg" 121 ) 122 else: 123 self.chooser_items = ( 124 _('Devices'), _('Language'), _('Automation'), _('Thumbnails'), _('Error Handling'), 125 _('Warnings'), _('Miscellaneous') 126 ) 127 icons = ( 128 ":/prefs/devices.svg", ":/prefs/language.svg", ":/prefs/automation.svg", 129 ":/prefs/thumbnails.svg", ":/prefs/error-handling.svg", ":/prefs/warnings.svg", 130 ":/prefs/miscellaneous.svg" 131 ) 132 133 for prefIcon, label in zip(icons, self.chooser_items): 134 # make the selected icons be the same colour as the selected text 135 icon = QIcon() 136 pixmap = QPixmap(prefIcon) 137 selected = QPixmap(pixmap.size()) 138 selected.fill(selectedColour) 139 selected.setMask(pixmap.createMaskFromColor(Qt.transparent)) 140 icon.addPixmap(pixmap, QIcon.Normal) 141 icon.addPixmap(selected, QIcon.Selected) 142 143 item = QListWidgetItem(icon, label, self.chooser) 144 item.setFont(QFont()) 145 width = fontMetrics.width(label) + icon_width + icon_padding * 2 146 item.setSizeHint(QSize(width, icon_height * 2)) 147 148 self.chooser.currentRowChanged.connect(self.rowChanged) 149 self.chooser.setSelectionMode(QAbstractItemView.SingleSelection) 150 self.chooser.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) 151 152 self.devices = QWidget() 153 154 self.scanBox = QGroupBox(_('Device Scanning')) 155 self.onlyExternal = QCheckBox(_('Scan only external devices')) 156 self.onlyExternal.setToolTip(_( 157 'Scan for photos and videos only on devices that are external to the computer,\n' 158 'including cameras, memory cards, external hard drives, and USB flash drives.' 159 )) 160 self.scanSpecificFolders = QCheckBox(_('Scan only specific folders on devices')) 161 tip = _( 162 'Scan for photos and videos only in the folders specified below (except paths\n' 163 'specified in Ignored Paths).\n\n' 164 'Changing this setting causes all devices to be scanned again.' 165 ) 166 self.scanSpecificFolders.setToolTip(tip) 167 168 self.foldersToScanLabel = QLabel(_('Folders to scan:')) 169 self.foldersToScan = QNarrowListWidget(minimum_rows=5) 170 self.foldersToScan.setToolTip(_( 171 'Folders at the base level of device file systems that will be scanned\n' 172 'for photos and videos.' 173 )) 174 self.addFolderToScan = QPushButton(_('Add...')) 175 self.addFolderToScan.setToolTip(_( 176 'Add a folder to the list of folders to scan for photos and videos.\n\n' 177 'Changing this setting causes all devices to be scanned again.' 178 )) 179 self.removeFolderToScan = QPushButton(_('Remove')) 180 self.removeFolderToScan.setToolTip(_( 181 'Remove a folder from the list of folders to scan for photos and videos.\n\n' 182 'Changing this setting causes all devices to be scanned again.' 183 )) 184 185 self.addFolderToScan.clicked.connect(self.addFolderToScanClicked) 186 self.removeFolderToScan.clicked.connect(self.removeFolderToScanClicked) 187 188 scanLayout = QGridLayout() 189 scanLayout.setHorizontalSpacing(18) 190 scanLayout.addWidget(self.onlyExternal, 0, 0, 1, 3) 191 scanLayout.addWidget(self.scanSpecificFolders, 1, 0, 1, 3) 192 scanLayout.addWidget(self.foldersToScanLabel, 2, 1, 1, 2) 193 scanLayout.addWidget(self.foldersToScan, 3, 1, 3, 1) 194 scanLayout.addWidget(self.addFolderToScan, 3, 2, 1, 1) 195 scanLayout.addWidget(self.removeFolderToScan, 4, 2, 1, 1) 196 self.scanBox.setLayout(scanLayout) 197 198 tip = _('Devices that have been set to automatically ignore or download from.') 199 self.knownDevicesBox = QGroupBox(_('Remembered Devices')) 200 self.knownDevices = QNarrowListWidget(minimum_rows=5) 201 self.knownDevices.setToolTip(tip) 202 tip = _( 203 'Remove a device from the list of devices to automatically ignore or download from.' 204 ) 205 self.removeDevice = QPushButton(_('Remove')) 206 self.removeDevice.setToolTip(tip) 207 self.removeAllDevice = QPushButton(_('Remove All')) 208 tip = _( 209 'Clear the list of devices from which to automatically ignore or download from.\n\n' 210 'Note: Changes take effect when the computer is next scanned for devices.' 211 ) 212 self.removeAllDevice.setToolTip(tip) 213 self.removeDevice.clicked.connect(self.removeDeviceClicked) 214 self.removeAllDevice.clicked.connect(self.removeAllDeviceClicked) 215 knownDevicesLayout = QGridLayout() 216 knownDevicesLayout.setHorizontalSpacing(18) 217 knownDevicesLayout.addWidget(self.knownDevices, 0, 0, 3, 1) 218 knownDevicesLayout.addWidget(self.removeDevice, 0, 1, 1, 1) 219 knownDevicesLayout.addWidget(self.removeAllDevice, 1, 1, 1, 1) 220 self.knownDevicesBox.setLayout(knownDevicesLayout) 221 222 self.ignoredPathsBox = QGroupBox(_('Ignored Paths')) 223 tip = _('The end part of a path that should never be scanned for photos or videos.') 224 self.ignoredPaths = QNarrowListWidget(minimum_rows=4) 225 self.ignoredPaths.setToolTip(tip) 226 self.addPath = QPushButton(_('Add...')) 227 self.addPath.setToolTip(_( 228 'Add a path to the list of paths to ignore.\n\n' 229 'Changing this setting causes all devices to be scanned again.' 230 )) 231 self.removePath = QPushButton(_('Remove')) 232 self.removePath.setToolTip(_( 233 'Remove a path from the list of paths to ignore.\n\n' 234 'Changing this setting causes all devices to be scanned again.' 235 )) 236 self.removeAllPath = QPushButton(_('Remove All')) 237 self.removeAllPath.setToolTip(_( 238 'Clear the list of paths to ignore.\n\n' 239 'Changing this setting causes all devices to be scanned again.' 240 )) 241 self.addPath.clicked.connect(self.addPathClicked) 242 self.removePath.clicked.connect(self.removePathClicked) 243 self.removeAllPath.clicked.connect(self.removeAllPathClicked) 244 self.ignoredPathsRe = QCheckBox() 245 self.ignorePathsReLabel = ClickableLabel( 246 _('Use python-style ' 247 '<a href="http://damonlynch.net/rapid/documentation/#regularexpressions">regular ' 248 'expressions</a>')) 249 self.ignorePathsReLabel.setToolTip(_( 250 'Use regular expressions in the list of ignored paths.\n\n' 251 'Changing this setting causes all devices to be scanned again.' 252 )) 253 self.ignorePathsReLabel.setTextInteractionFlags(Qt.TextBrowserInteraction) 254 self.ignorePathsReLabel.setOpenExternalLinks(True) 255 self.ignorePathsReLabel.clicked.connect(self.ignorePathsReLabelClicked) 256 reLayout = QHBoxLayout() 257 reLayout.setSpacing(5) 258 reLayout.addWidget(self.ignoredPathsRe) 259 reLayout.addWidget(self.ignorePathsReLabel) 260 reLayout.addStretch() 261 ignoredPathsLayout = QGridLayout() 262 ignoredPathsLayout.setHorizontalSpacing(18) 263 ignoredPathsLayout.addWidget(self.ignoredPaths, 0, 0, 4, 1) 264 ignoredPathsLayout.addWidget(self.addPath, 0, 1, 1, 1) 265 ignoredPathsLayout.addWidget(self.removePath, 1, 1, 1, 1) 266 ignoredPathsLayout.addWidget(self.removeAllPath, 2, 1, 1, 1) 267 ignoredPathsLayout.addLayout(reLayout, 4, 0, 1, 2) 268 self.ignoredPathsBox.setLayout(ignoredPathsLayout) 269 270 self.setDeviceWidgetValues() 271 272 # connect these next 3 only after having set their values, so rescan / search again 273 # in rapidApp is not triggered 274 self.onlyExternal.stateChanged.connect(self.onlyExternalChanged) 275 self.scanSpecificFolders.stateChanged.connect(self.noDcimChanged) 276 self.ignoredPathsRe.stateChanged.connect(self.ignoredPathsReChanged) 277 278 devicesLayout = QVBoxLayout() 279 devicesLayout.addWidget(self.scanBox) 280 devicesLayout.addWidget(self.ignoredPathsBox) 281 devicesLayout.addWidget(self.knownDevicesBox) 282 devicesLayout.addStretch() 283 devicesLayout.setSpacing(18) 284 285 self.devices.setLayout(devicesLayout) 286 devicesLayout.setContentsMargins(0, 0, 0, 0) 287 288 self.language = QWidget() 289 self.languages = QComboBox() 290 self.languages.setEditable(False) 291 self.languagesLabel = QLabel(_('Language: ')) 292 self.languages.setSizeAdjustPolicy(QComboBox.AdjustToContents) 293 # self.languages.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 294 295 self.setLanguageWidgetValues() 296 297 self.languages.currentIndexChanged.connect(self.languagesChanged) 298 299 languageWidgetsLayout = QHBoxLayout() 300 languageWidgetsLayout.addWidget(self.languagesLabel) 301 languageWidgetsLayout.addWidget(self.languages) 302 # Translators: the * acts as an asterisk to denote a reference to an annotation 303 # such as '* Takes effect upon program restart' 304 languageWidgetsLayout.addWidget(QLabel(_('*'))) 305 languageWidgetsLayout.addStretch() 306 languageWidgetsLayout.setSpacing(5) 307 308 languageLayout = QVBoxLayout() 309 languageLayout.addLayout(languageWidgetsLayout) 310 # Translators: the * acts as an asterisk to denote a reference to this annotation 311 languageLayout.addWidget(QLabel(_('* Takes effect upon program restart'))) 312 languageLayout.addStretch() 313 languageLayout.setContentsMargins(0, 0, 0, 0) 314 languageLayout.setSpacing(18) 315 self.language.setLayout(languageLayout) 316 317 self.automation = QWidget() 318 319 self.automationBox = QGroupBox(_('Program Automation')) 320 self.autoDownloadStartup = QCheckBox(_('Start downloading at program startup')) 321 self.autoDownloadInsertion = QCheckBox(_('Start downloading upon device insertion')) 322 self.autoEject = QCheckBox(_('Unmount (eject) device upon download completion')) 323 self.autoExit = QCheckBox(_('Exit program when download completes')) 324 self.autoExitError = QCheckBox(_('Exit program even if download had warnings or errors')) 325 self.setAutomationWidgetValues() 326 self.autoDownloadStartup.stateChanged.connect(self.autoDownloadStartupChanged) 327 self.autoDownloadInsertion.stateChanged.connect(self.autoDownloadInsertionChanged) 328 self.autoEject.stateChanged.connect(self.autoEjectChanged) 329 self.autoExit.stateChanged.connect(self.autoExitChanged) 330 self.autoExitError.stateChanged.connect(self.autoExitErrorChanged) 331 332 automationBoxLayout = QGridLayout() 333 automationBoxLayout.addWidget(self.autoDownloadStartup, 0, 0, 1, 2) 334 automationBoxLayout.addWidget(self.autoDownloadInsertion, 1, 0, 1, 2) 335 automationBoxLayout.addWidget(self.autoEject, 2, 0, 1, 2) 336 automationBoxLayout.addWidget(self.autoExit, 3, 0, 1, 2) 337 automationBoxLayout.addWidget(self.autoExitError, 4, 1, 1, 1) 338 checkbox_width = self.autoExit.style().pixelMetric(QStyle.PM_IndicatorWidth) 339 automationBoxLayout.setColumnMinimumWidth(0, checkbox_width) 340 self.automationBox.setLayout(automationBoxLayout) 341 342 automationLayout = QVBoxLayout() 343 automationLayout.addWidget(self.automationBox) 344 automationLayout.addStretch() 345 automationLayout.setContentsMargins(0, 0, 0, 0) 346 347 self.automation.setLayout(automationLayout) 348 349 self.performance = QWidget() 350 351 self.performanceBox = QGroupBox(_('Thumbnail Generation')) 352 self.generateThumbnails = QCheckBox(_('Generate thumbnails')) 353 self.generateThumbnails.setToolTip( 354 _('Generate thumbnails to show in the main program window') 355 ) 356 self.useThumbnailCache = QCheckBox(_('Cache thumbnails')) 357 self.useThumbnailCache.setToolTip( 358 _( 359 "Save thumbnails shown in the main program window in a thumbnail cache unique to " 360 "Rapid Photo Downloader" 361 ) 362 ) 363 self.fdoThumbnails = QCheckBox(_('Generate system thumbnails')) 364 self.fdoThumbnails.setToolTip( 365 _( 366 'While downloading, save thumbnails that can be used by desktop file managers ' 367 'and other programs' 368 ) 369 ) 370 self.generateThumbnails.stateChanged.connect(self.generateThumbnailsChanged) 371 self.useThumbnailCache.stateChanged.connect(self.useThumbnailCacheChanged) 372 self.fdoThumbnails.stateChanged.connect(self.fdoThumbnailsChanged) 373 self.maxCores = QComboBox() 374 self.maxCores.setEditable(False) 375 tip = _('Number of CPU cores used to generate thumbnails.') 376 self.coresLabel = QLabel(_('CPU cores:')) 377 self.coresLabel.setToolTip(tip) 378 self.maxCores.setSizeAdjustPolicy(QComboBox.AdjustToContents) 379 self.maxCores.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 380 self.maxCores.setToolTip(tip) 381 382 self.setPerformanceValues() 383 384 self.maxCores.currentIndexChanged.connect(self.maxCoresChanged) 385 386 coresLayout = QHBoxLayout() 387 coresLayout.addWidget(self.coresLabel) 388 coresLayout.addWidget(self.maxCores) 389 # Translators: the * acts as an asterisk to denote a reference to an annotation 390 # such as '* Takes effect upon program restart' 391 coresLayout.addWidget(QLabel(_('*'))) 392 coresLayout.addStretch() 393 394 performanceBoxLayout = QVBoxLayout() 395 performanceBoxLayout.addWidget(self.generateThumbnails) 396 performanceBoxLayout.addWidget(self.useThumbnailCache) 397 performanceBoxLayout.addWidget(self.fdoThumbnails) 398 performanceBoxLayout.addLayout(coresLayout) 399 self.performanceBox.setLayout(performanceBoxLayout) 400 401 self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False) 402 403 self.cacheSize = CacheSize() 404 self.cacheSizeThread = QThread() 405 self.cacheSizeThread.started.connect(self.cacheSize.start) 406 self.getCacheSize.connect(self.cacheSize.getCacheSize) 407 self.cacheSize.size.connect(self.setCacheSize) 408 self.cacheSize.moveToThread(self.cacheSizeThread) 409 410 QTimer.singleShot(0, self.cacheSizeThread.start) 411 412 self.getCacheSize.emit() 413 414 self.cacheBox = QGroupBox(_('Thumbnail Cache')) 415 self.thumbnailCacheSize = QLabel() 416 self.thumbnailCacheSize.setText(_('Calculating...')) 417 self.thumbnailNumber = QLabel() 418 self.thumbnailSqlSize = QLabel() 419 self.thumbnailCacheDaysKeep = QSpinBox() 420 self.thumbnailCacheDaysKeep.setMinimum(0) 421 self.thumbnailCacheDaysKeep.setMaximum(360*3) 422 self.thumbnailCacheDaysKeep.setSuffix(' ' + _('days')) 423 self.thumbnailCacheDaysKeep.setSpecialValueText(_('forever')) 424 self.thumbnailCacheDaysKeep.valueChanged.connect(self.thumbnailCacheDaysKeepChanged) 425 426 cacheBoxLayout = QVBoxLayout() 427 cacheLayout = QGridLayout() 428 cacheLayout.addWidget(QLabel(_('Cache size:')), 0, 0, 1, 1) 429 cacheLayout.addWidget(self.thumbnailCacheSize, 0, 1, 1, 1) 430 cacheLayout.addWidget(QLabel(_('Number of thumbnails:')), 1, 0, 1, 1) 431 cacheLayout.addWidget(self.thumbnailNumber, 1, 1, 1, 1) 432 cacheLayout.addWidget(QLabel(_('Database size:')), 2, 0, 1, 1) 433 cacheLayout.addWidget(self.thumbnailSqlSize, 2, 1, 1, 1) 434 cacheLayout.addWidget(QLabel(_('Cache unaccessed thumbnails for:')), 3, 0, 1, 1) 435 cacheDays = QHBoxLayout() 436 cacheDays.addWidget(self.thumbnailCacheDaysKeep) 437 cacheDays.addWidget(QLabel(_('*'))) 438 cacheLayout.addLayout(cacheDays, 3, 1, 1, 1) 439 cacheBoxLayout.addLayout(cacheLayout) 440 441 cacheButtons = QDialogButtonBox() 442 self.purgeCache = cacheButtons.addButton(_('Purge Cache...'), QDialogButtonBox.ResetRole) 443 self.optimizeCache = cacheButtons.addButton( 444 _('Optimize Cache...'), QDialogButtonBox.ResetRole 445 ) 446 self.purgeCache.clicked.connect(self.purgeCacheClicked) 447 self.optimizeCache.clicked.connect(self.optimizeCacheClicked) 448 449 cacheBoxLayout.addWidget(cacheButtons) 450 451 self.cacheBox.setLayout(cacheBoxLayout) 452 self.setCacheValues() 453 454 performanceLayout = QVBoxLayout() 455 performanceLayout.addWidget(self.performanceBox) 456 performanceLayout.addWidget(self.cacheBox) 457 performanceLayout.addWidget(QLabel(_('* Takes effect upon program restart'))) 458 performanceLayout.addStretch() 459 performanceLayout.setContentsMargins(0, 0, 0, 0) 460 performanceLayout.setSpacing(18) 461 462 self.performance.setLayout(performanceLayout) 463 464 self.errorBox = QGroupBox(_('Error Handling')) 465 466 self.downloadErrorGroup = QButtonGroup() 467 self.skipDownload = QRadioButton(_('Skip download')) 468 self.skipDownload.setToolTip(_("Don't download the file, and issue an error message")) 469 self.addIdentifier = QRadioButton(_('Add unique identifier')) 470 self.addIdentifier.setToolTip( 471 _( 472 "Add an identifier like _1 or _2 to the end of the filename, immediately before " 473 "the file's extension" 474 ) 475 ) 476 self.downloadErrorGroup.addButton(self.skipDownload) 477 self.downloadErrorGroup.addButton(self.addIdentifier) 478 479 self.backupErrorGroup = QButtonGroup() 480 self.overwriteBackup = QRadioButton(_('Overwrite')) 481 self.overwriteBackup.setToolTip(_("Overwrite the previously backed up file")) 482 self.skipBackup = QRadioButton(_('Skip')) 483 self.skipBackup.setToolTip( 484 _("Don't overwrite the backup file, and issue an error message") 485 ) 486 self.backupErrorGroup.addButton(self.overwriteBackup) 487 self.backupErrorGroup.addButton(self.skipBackup) 488 489 errorBoxLayout = QVBoxLayout() 490 lbl = _( 491 'When a photo or video of the same name has already been downloaded, choose ' 492 'whether to skip downloading the file, or to add a unique identifier:' 493 ) 494 self.downloadError = QLabel(lbl) 495 self.downloadError.setWordWrap(True) 496 errorBoxLayout.addWidget(self.downloadError) 497 errorBoxLayout.addWidget(self.skipDownload) 498 errorBoxLayout.addWidget(self.addIdentifier) 499 lbl = '<i>' + _( 500 'Using sequence numbers to automatically generate unique filenames is ' 501 'strongly recommended. Configure file renaming in the Rename panel in the ' 502 'main window.' 503 ) + '</i>' 504 self.recommended = QLabel(lbl) 505 self.recommended.setWordWrap(True) 506 errorBoxLayout.addWidget(self.recommended) 507 errorBoxLayout.addSpacing(18) 508 lbl = _( 509 'When backing up, choose whether to overwrite a file on the backup device that ' 510 'has the same name, or skip backing it up:' 511 ) 512 self.backupError = QLabel(lbl) 513 self.backupError.setWordWrap(True) 514 errorBoxLayout.addWidget(self.backupError) 515 errorBoxLayout.addWidget(self.overwriteBackup) 516 errorBoxLayout.addWidget(self.skipBackup) 517 self.errorBox.setLayout(errorBoxLayout) 518 519 self.setErrorHandingValues() 520 self.downloadErrorGroup.buttonClicked.connect(self.downloadErrorGroupClicked) 521 self.backupErrorGroup.buttonClicked.connect(self.backupErrorGroupClicked) 522 523 self.errorWidget = QWidget() 524 errorLayout = QVBoxLayout() 525 self.errorWidget.setLayout(errorLayout) 526 errorLayout.addWidget(self.errorBox) 527 errorLayout.addStretch() 528 errorLayout.setContentsMargins(0, 0, 0, 0) 529 530 self.warningBox = QGroupBox(_('Program Warnings')) 531 lbl = _('Show a warning when:') 532 self.warningLabel = QLabel(lbl) 533 self.warningLabel.setWordWrap(True) 534 self.warnDownloadingAll = QCheckBox(_('Downloading files currently not displayed')) 535 tip = _('Warn when about to download files that are not displayed in the main window.') 536 self.warnDownloadingAll.setToolTip(tip) 537 self.warnBackupProblem = QCheckBox(_('Backup destinations are missing')) 538 tip = _("Warn before starting a download if it is not possible to back up files.") 539 self.warnBackupProblem.setToolTip(tip) 540 self.warnMissingLibraries = QCheckBox(_('Program libraries are missing or broken')) 541 tip = _('Warn if a software library used by Rapid Photo Downloader is missing or not ' 542 'functioning.') 543 self.warnMissingLibraries.setToolTip(tip) 544 self.warnMetadata = QCheckBox(_('Filesystem metadata cannot be set')) 545 tip = _("Warn if there is an error setting a file's filesystem metadata, " 546 "such as its modification time.") 547 self.warnMetadata.setToolTip(tip) 548 self.warnUnhandledFiles = QCheckBox(_('Encountering unhandled files')) 549 tip = _('Warn after scanning a device or this computer if there are unrecognized files ' 550 'that will not be included in the download.') 551 self.warnUnhandledFiles.setToolTip(tip) 552 self.exceptTheseFilesLabel = QLabel( 553 _('Do not warn about unhandled files with extensions:') 554 ) 555 self.exceptTheseFilesLabel.setWordWrap(True) 556 self.exceptTheseFiles = QNarrowListWidget(minimum_rows=4) 557 tip = _( 558 'File extensions are case insensitive and do not need to include the leading dot.' 559 ) 560 self.exceptTheseFiles.setToolTip(tip) 561 self.addExceptFiles = QPushButton(_('Add')) 562 tip = _('Add a file extension to the list of unhandled file types to not warn about.') 563 self.addExceptFiles.setToolTip(tip) 564 tip = _('Remove a file extension from the list of unhandled file types to not warn about.') 565 self.removeExceptFiles = QPushButton(_('Remove')) 566 self.removeExceptFiles.setToolTip(tip) 567 self.removeAllExceptFiles = QPushButton(_('Remove All')) 568 tip = _('Clear the list of file extensions of unhandled file types to not warn about.') 569 self.removeAllExceptFiles.setToolTip(tip) 570 self.addExceptFiles.clicked.connect(self.addExceptFilesClicked) 571 self.removeExceptFiles.clicked.connect(self.removeExceptFilesClicked) 572 self.removeAllExceptFiles.clicked.connect(self.removeAllExceptFilesClicked) 573 574 self.setWarningValues() 575 self.warnDownloadingAll.stateChanged.connect(self.warnDownloadingAllChanged) 576 self.warnBackupProblem.stateChanged.connect(self.warnBackupProblemChanged) 577 self.warnMissingLibraries.stateChanged.connect(self.warnMissingLibrariesChanged) 578 self.warnMetadata.stateChanged.connect(self.warnMetadataChanged) 579 self.warnUnhandledFiles.stateChanged.connect(self.warnUnhandledFilesChanged) 580 581 warningBoxLayout = QGridLayout() 582 warningBoxLayout.addWidget(self.warningLabel, 0, 0, 1, 3) 583 warningBoxLayout.addWidget(self.warnDownloadingAll, 1, 0, 1, 3) 584 warningBoxLayout.addWidget(self.warnBackupProblem, 2, 0, 1, 3) 585 warningBoxLayout.addWidget(self.warnMissingLibraries, 3, 0, 1, 3) 586 warningBoxLayout.addWidget(self.warnMetadata, 4, 0, 1, 3) 587 warningBoxLayout.addWidget(self.warnUnhandledFiles, 5, 0, 1, 3) 588 warningBoxLayout.addWidget(self.exceptTheseFilesLabel, 6, 1, 1, 2) 589 warningBoxLayout.addWidget(self.exceptTheseFiles, 7, 1, 4, 1) 590 warningBoxLayout.addWidget(self.addExceptFiles, 7, 2, 1, 1) 591 warningBoxLayout.addWidget(self.removeExceptFiles, 8, 2, 1, 1) 592 warningBoxLayout.addWidget(self.removeAllExceptFiles, 9, 2, 1, 1) 593 warningBoxLayout.setColumnMinimumWidth(0, checkbox_width) 594 self.warningBox.setLayout(warningBoxLayout) 595 596 self.warnings = QWidget() 597 warningLayout = QVBoxLayout() 598 self.warnings.setLayout(warningLayout) 599 warningLayout.addWidget(self.warningBox) 600 warningLayout.addStretch() 601 warningLayout.setContentsMargins(0, 0, 0, 0) 602 603 if consolidation_implemented: 604 self.consolidationBox = QGroupBox(_('Photo and Video Consolidation')) 605 606 self.consolidateIdentical = QCheckBox( 607 _('Consolidate files across devices and downloads') 608 ) 609 tip = _( 610 "Analyze the results of device scans looking for duplicate files and matching " 611 "RAW and JPEG pairs,\ncomparing them across multiple devices and download " 612 "sessions." 613 ) 614 self.consolidateIdentical.setToolTip(tip) 615 616 self.treatRawJpegLabel = QLabel(_('Treat matching RAW and JPEG files as:')) 617 self.oneRawJpeg = QRadioButton(_('One photo')) 618 self.twoRawJpeg = QRadioButton(_('Two photos')) 619 tip = _( 620 "Display matching pairs of RAW and JPEG photos as one photo, and if marked, " 621 "download both." 622 ) 623 self.oneRawJpeg.setToolTip(tip) 624 tip = _( 625 "Display matching pairs of RAW and JPEG photos as two different photos. You can " 626 "still synchronize their sequence numbers." 627 ) 628 self.twoRawJpeg.setToolTip(tip) 629 630 self.treatRawJpegGroup = QButtonGroup() 631 self.treatRawJpegGroup.addButton(self.oneRawJpeg) 632 self.treatRawJpegGroup.addButton(self.twoRawJpeg) 633 634 self.markRawJpegLabel = QLabel(_('With matching RAW and JPEG photos:')) 635 636 self.noJpegWhenRaw = QRadioButton(_('Do not mark JPEG for download')) 637 self.noRawWhenJpeg = QRadioButton(_('Do not mark RAW for download')) 638 self.markRawJpeg = QRadioButton(_('Mark both for download')) 639 640 self.markRawJpegGroup = QButtonGroup() 641 for widget in (self.noJpegWhenRaw, self.noRawWhenJpeg, self.markRawJpeg): 642 self.markRawJpegGroup.addButton(widget) 643 644 tip = _( 645 "When matching RAW and JPEG photos are found, do not automatically mark the " 646 "JPEG for\ndownload. You can still mark it for download yourself." 647 ) 648 self.noJpegWhenRaw.setToolTip(tip) 649 tip = _( 650 "When matching RAW and JPEG photos are found, do not automatically mark the " 651 "RAW for\ndownload. You can still mark it for download yourself." 652 ) 653 self.noRawWhenJpeg.setToolTip(tip) 654 tip = _( 655 "When matching RAW and JPEG photos are found, automatically mark both " 656 "for download." 657 ) 658 self.markRawJpeg.setToolTip(tip) 659 660 explanation = _( 661 'If you disable file consolidation, choose what to do when a download device is ' 662 'inserted while completed downloads are displayed:' 663 ) 664 665 else: 666 explanation = _( 667 'When a download device is inserted while completed downloads are displayed:' 668 ) 669 self.noconsolidationLabel = QLabel(explanation) 670 self.noconsolidationLabel.setWordWrap(True) 671 self.noconsolidationLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) 672 # Unless this next call is made, for some reason the widget is far too high! :-( 673 self.noconsolidationLabel.setContentsMargins(0, 0, 1, 0) 674 675 self.noConsolidationGroup = QButtonGroup() 676 self.noConsolidationGroup.buttonClicked.connect(self.noConsolidationGroupClicked) 677 678 self.clearCompletedDownloads = QRadioButton(_('Clear completed downloads')) 679 self.keepCompletedDownloads = QRadioButton(_('Keep displaying completed downloads')) 680 self.promptCompletedDownloads = QRadioButton(_('Prompt for what to do')) 681 self.noConsolidationGroup.addButton(self.clearCompletedDownloads) 682 self.noConsolidationGroup.addButton(self.keepCompletedDownloads) 683 self.noConsolidationGroup.addButton(self.promptCompletedDownloads) 684 tip = _( 685 "Automatically clear the display of completed downloads whenever a new download " 686 "device is inserted." 687 ) 688 self.clearCompletedDownloads.setToolTip(tip) 689 tip = _( 690 "Keep displaying completed downloads whenever a new download device is inserted." 691 ) 692 self.keepCompletedDownloads.setToolTip(tip) 693 tip = _( 694 "Prompt whether to keep displaying completed downloads or clear them whenever a new " 695 "download device is inserted." 696 ) 697 self.promptCompletedDownloads.setToolTip(tip) 698 699 if consolidation_implemented: 700 consolidationBoxLayout = QGridLayout() 701 consolidationBoxLayout.addWidget(self.consolidateIdentical, 0, 0, 1, 3) 702 703 consolidationBoxLayout.addWidget(self.treatRawJpegLabel, 1, 1, 1, 2) 704 consolidationBoxLayout.addWidget(self.oneRawJpeg, 2, 1, 1, 2) 705 consolidationBoxLayout.addWidget(self.twoRawJpeg, 3, 1, 1, 2) 706 707 consolidationBoxLayout.addWidget(self.markRawJpegLabel, 4, 2, 1, 1) 708 consolidationBoxLayout.addWidget(self.noJpegWhenRaw, 5, 2, 1, 1) 709 consolidationBoxLayout.addWidget(self.noRawWhenJpeg, 6, 2, 1, 1) 710 consolidationBoxLayout.addWidget(self.markRawJpeg, 7, 2, 1, 1, Qt.AlignTop) 711 712 consolidationBoxLayout.addWidget(self.noconsolidationLabel, 8, 0, 1, 3) 713 consolidationBoxLayout.addWidget(self.keepCompletedDownloads, 9, 0, 1, 3) 714 consolidationBoxLayout.addWidget(self.clearCompletedDownloads, 10, 0, 1, 3) 715 consolidationBoxLayout.addWidget(self.promptCompletedDownloads, 11, 0, 1, 3) 716 717 consolidationBoxLayout.setColumnMinimumWidth(0, checkbox_width) 718 consolidationBoxLayout.setColumnMinimumWidth(1, checkbox_width) 719 720 consolidationBoxLayout.setRowMinimumHeight(7, checkbox_width * 2) 721 722 self.consolidationBox.setLayout(consolidationBoxLayout) 723 724 self.consolidation = QWidget() 725 consolidationLayout = QVBoxLayout() 726 consolidationLayout.addWidget(self.consolidationBox) 727 consolidationLayout.addStretch() 728 consolidationLayout.setContentsMargins(0, 0, 0, 0) 729 consolidationLayout.setSpacing(18) 730 self.consolidation.setLayout(consolidationLayout) 731 732 self.setCompletedDownloadsValues() 733 self.setConsolidatedValues() 734 self.consolidateIdentical.stateChanged.connect(self.consolidateIdenticalChanged) 735 self.treatRawJpegGroup.buttonClicked.connect(self.treatRawJpegGroupClicked) 736 self.markRawJpegGroup.buttonClicked.connect(self.markRawJpegGroupClicked) 737 738 if not version_check_disabled(): 739 self.newVersionBox = QGroupBox(_('Version Check')) 740 self.checkNewVersion = QCheckBox(_('Check for new version at startup')) 741 self.checkNewVersion.setToolTip( 742 _('Check for a new version of the program each time the program starts.') 743 ) 744 self.includeDevRelease = QCheckBox(_('Include development releases')) 745 tip = _( 746 'Include alpha, beta and other development releases when checking for a new ' 747 'version of the program.\n\nIf you are currently running a development version, ' 748 'the check will always occur.' 749 ) 750 self.includeDevRelease.setToolTip(tip) 751 self.setVersionCheckValues() 752 self.checkNewVersion.stateChanged.connect(self.checkNewVersionChanged) 753 self.includeDevRelease.stateChanged.connect(self.includeDevReleaseChanged) 754 755 newVersionLayout = QGridLayout() 756 newVersionLayout.addWidget(self.checkNewVersion, 0, 0, 1, 2) 757 newVersionLayout.addWidget(self.includeDevRelease, 1, 1, 1, 1) 758 newVersionLayout.setColumnMinimumWidth(0, checkbox_width) 759 self.newVersionBox.setLayout(newVersionLayout) 760 761 self.metadataBox = QGroupBox(_('Metadata')) 762 self.ignoreMdatatimeMtpDng = QCheckBox(_('Ignore DNG date/time metadata on MTP devices')) 763 tip = _( 764 "Ignore date/time metadata in DNG files located on MTP devices, and use the " 765 "file's modification time instead.\n\nUseful for devices like some phones and " 766 "tablets that create incorrect DNG metadata." 767 ) 768 self.ignoreMdatatimeMtpDng.setToolTip(tip) 769 770 self.forceExiftool = QCheckBox(_('Read photo metadata using only ExifTool')) 771 tip = _( 772 'Use ExifTool instead of Exiv2 to read photo metadata and extract thumbnails.\n\n' 773 'The default is to use Exiv2, relying on ExifTool only when Exiv2 does not support\n' 774 'the file format being read.\n\n' 775 'Exiv2 is fast, accurate, and almost always reliable, but it crashes when extracting\n' 776 'metadata from a small number of files, such as DNG files produced by Leica M8\n' 777 'cameras.' 778 ) 779 780 self.forceExiftool.setToolTip(tip) 781 782 self.setMetdataValues() 783 self.ignoreMdatatimeMtpDng.stateChanged.connect(self.ignoreMdatatimeMtpDngChanged) 784 self.forceExiftool.stateChanged.connect(self.forceExiftoolChanged) 785 786 metadataLayout = QVBoxLayout() 787 metadataLayout.addWidget(self.ignoreMdatatimeMtpDng) 788 metadataLayout.addWidget(self.forceExiftool) 789 self.metadataBox.setLayout(metadataLayout) 790 791 if not consolidation_implemented: 792 self.completedDownloadsBox = QGroupBox(_('Completed Downloads')) 793 completedDownloadsLayout = QVBoxLayout() 794 completedDownloadsLayout.addWidget(self.noconsolidationLabel) 795 completedDownloadsLayout.addWidget(self.keepCompletedDownloads) 796 completedDownloadsLayout.addWidget(self.clearCompletedDownloads) 797 completedDownloadsLayout.addWidget(self.promptCompletedDownloads) 798 self.completedDownloadsBox.setLayout(completedDownloadsLayout) 799 self.setCompletedDownloadsValues() 800 801 self.miscWidget = QWidget() 802 miscLayout = QVBoxLayout() 803 if not version_check_disabled(): 804 miscLayout.addWidget(self.newVersionBox) 805 miscLayout.addWidget(self.metadataBox) 806 if not consolidation_implemented: 807 miscLayout.addWidget(self.completedDownloadsBox) 808 miscLayout.addStretch() 809 miscLayout.setContentsMargins(0, 0, 0, 0) 810 miscLayout.setSpacing(18) 811 self.miscWidget.setLayout(miscLayout) 812 813 self.panels.addWidget(self.devices) 814 self.panels.addWidget(self.language) 815 self.panels.addWidget(self.automation) 816 self.panels.addWidget(self.performance) 817 self.panels.addWidget(self.errorWidget) 818 self.panels.addWidget(self.warnings) 819 if consolidation_implemented: 820 self.panels.addWidget(self.consolidation) 821 self.panels.addWidget(self.miscWidget) 822 823 layout = QVBoxLayout() 824 self.setLayout(layout) 825 layout.setSpacing(layout.contentsMargins().left() * 2) 826 layout.setContentsMargins(18, 18, 18, 18) 827 828 buttons = QDialogButtonBox( 829 QDialogButtonBox.RestoreDefaults | QDialogButtonBox.Close | QDialogButtonBox.Help 830 ) 831 translateDialogBoxButtons(buttons) 832 self.restoreButton = buttons.button(QDialogButtonBox.RestoreDefaults) # type: QPushButton 833 self.restoreButton.clicked.connect(self.restoreDefaultsClicked) 834 self.helpButton = buttons.button(QDialogButtonBox.Help) # type: QPushButton 835 self.helpButton.clicked.connect(self.helpButtonClicked) 836 self.helpButton.setToolTip(_('Get help online...')) 837 self.closeButton = buttons.button(QDialogButtonBox.Close) # type: QPushButton 838 self.closeButton.clicked.connect(self.close) 839 840 controlsLayout = QHBoxLayout() 841 controlsLayout.addWidget(self.chooser) 842 controlsLayout.addWidget(self.panels) 843 844 controlsLayout.setStretch(0, 0) 845 controlsLayout.setStretch(1, 1) 846 controlsLayout.setSpacing(layout.contentsMargins().left()) 847 848 layout.addLayout(controlsLayout) 849 layout.addWidget(buttons) 850 851 self.device_right_side_buttons = ( 852 self.removeDevice, self.removeAllDevice, self.addPath, self.removePath, 853 self.removeAllPath 854 ) 855 856 self.device_list_widgets = (self.knownDevices, self.ignoredPaths) 857 self.chooser.setCurrentRow(0) 858 859 def reject(self) -> None: 860 # If not called, rejecting this dialog will cause Rapid Photo Downloader to crash 861 self.close() 862 863 def _addItems(self, pref_list: str, pref_type: int) -> None: 864 if self.prefs.list_not_empty(key=pref_list): 865 for value in self.prefs[pref_list]: 866 QListWidgetItem(value, self.knownDevices, pref_type) 867 868 def setDeviceWidgetValues(self) -> None: 869 self.onlyExternal.setChecked(self.prefs.only_external_mounts) 870 self.scanSpecificFolders.setChecked(self.prefs.scan_specific_folders) 871 self.setFoldersToScanWidgetValues() 872 self.knownDevices.clear() 873 self._addItems('volume_whitelist', KnownDeviceType.volume_whitelist) 874 self._addItems('volume_blacklist', KnownDeviceType.volume_blacklist) 875 self._addItems('camera_blacklist', KnownDeviceType.camera_blacklist) 876 if self.knownDevices.count(): 877 self.knownDevices.setCurrentRow(0) 878 self.removeDevice.setEnabled(self.knownDevices.count()) 879 self.removeAllDevice.setEnabled(self.knownDevices.count()) 880 self.setIgnorePathWidgetValues() 881 882 def setLanguageWidgetValues(self) -> None: 883 # Translators: this is an option when the user chooses the language to use for 884 # Rapid Photo Downloader and it allows them to reset it back to whatever their 885 # system language settings are. The < and > are not HTML codes. They are there 886 # simply to set this choice apart from all the other choices in the drop down list. 887 # You can keep the < > if you like, or replace them with whatever you typically use 888 # in your language. 889 self.languages.addItem(_('<System Language>'), system_language) 890 for code, language in available_languages(display_locale_code=self.prefs.language): 891 self.languages.addItem(language, code) 892 value = self.prefs.language 893 if value: 894 index = self.languages.findData(value) 895 self.languages.setCurrentIndex(index) 896 897 def setFoldersToScanWidgetValues(self) -> None: 898 self.foldersToScan.clear() 899 if self.prefs.list_not_empty('folders_to_scan'): 900 self.foldersToScan.addItems(self.prefs.folders_to_scan) 901 self.foldersToScan.setCurrentRow(0) 902 self.setFoldersToScanState() 903 904 def setFoldersToScanState(self) -> None: 905 scan_specific = self.prefs.scan_specific_folders 906 self.foldersToScanLabel.setEnabled(scan_specific) 907 self.foldersToScan.setEnabled(scan_specific) 908 self.addFolderToScan.setEnabled(scan_specific) 909 self.removeFolderToScan.setEnabled(scan_specific and self.foldersToScan.count() > 1) 910 911 def setIgnorePathWidgetValues(self) -> None: 912 self.ignoredPaths.clear() 913 if self.prefs.list_not_empty('ignored_paths'): 914 self.ignoredPaths.addItems(self.prefs.ignored_paths) 915 self.ignoredPaths.setCurrentRow(0) 916 self.removePath.setEnabled(self.ignoredPaths.count()) 917 self.removeAllPath.setEnabled(self.ignoredPaths.count()) 918 self.ignoredPathsRe.setChecked(self.prefs.use_re_ignored_paths) 919 920 def setAutomationWidgetValues(self) -> None: 921 self.autoDownloadStartup.setChecked(self.prefs.auto_download_at_startup) 922 self.autoDownloadInsertion.setChecked(self.prefs.auto_download_upon_device_insertion) 923 self.autoEject.setChecked(self.prefs.auto_unmount) 924 self.autoExit.setChecked(self.prefs.auto_exit) 925 self.setAutoExitErrorState() 926 927 def setAutoExitErrorState(self) -> None: 928 if self.prefs.auto_exit: 929 self.autoExitError.setChecked(self.prefs.auto_exit_force) 930 self.autoExitError.setEnabled(True) 931 else: 932 self.autoExitError.setChecked(False) 933 self.autoExitError.setEnabled(False) 934 935 def setPerformanceValues(self, check_boxes_only: bool=False) -> None: 936 self.generateThumbnails.setChecked(self.prefs.generate_thumbnails) 937 self.useThumbnailCache.setChecked( 938 self.prefs.use_thumbnail_cache and self.prefs.generate_thumbnails 939 ) 940 self.fdoThumbnails.setChecked( 941 self.prefs.save_fdo_thumbnails and self.prefs.generate_thumbnails 942 ) 943 944 if not check_boxes_only: 945 available = available_cpu_count(physical_only=True) 946 self.maxCores.addItems(str(i + 1) for i in range(0, available)) 947 self.maxCores.setCurrentText(str(self.prefs.max_cpu_cores)) 948 949 def setPerfomanceEnabled(self) -> None: 950 enable = self.prefs.generate_thumbnails 951 self.useThumbnailCache.setEnabled(enable) 952 self.fdoThumbnails.setEnabled(enable) 953 self.maxCores.setEnabled(enable) 954 self.coresLabel.setEnabled(enable) 955 956 def setCacheValues(self) -> None: 957 self.thumbnailNumber.setText(thousands(self.thumbnail_cache.no_thumbnails())) 958 self.thumbnailSqlSize.setText(format_size_for_user(self.thumbnail_cache.db_size())) 959 self.thumbnailCacheDaysKeep.setValue(self.prefs.keep_thumbnails_days) 960 961 @pyqtSlot('PyQt_PyObject') 962 def setCacheSize(self, size: int) -> None: 963 self.thumbnailCacheSize.setText(format_size_for_user(size)) 964 965 def setErrorHandingValues(self) -> None: 966 if self.prefs.conflict_resolution == int(ConflictResolution.skip): 967 self.skipDownload.setChecked(True) 968 else: 969 self.addIdentifier.setChecked(True) 970 if self.prefs.backup_duplicate_overwrite: 971 self.overwriteBackup.setChecked(True) 972 else: 973 self.skipBackup.setChecked(True) 974 975 def setWarningValues(self) -> None: 976 self.warnDownloadingAll.setChecked(self.prefs.warn_downloading_all) 977 if self.prefs.backup_files: 978 self.warnBackupProblem.setChecked(self.prefs.warn_backup_problem) 979 else: 980 self.warnBackupProblem.setChecked(False) 981 self.warnMissingLibraries.setChecked(self.prefs.warn_broken_or_missing_libraries) 982 self.warnMetadata.setChecked(self.prefs.warn_fs_metadata_error) 983 self.warnUnhandledFiles.setChecked(self.prefs.warn_unhandled_files) 984 self.setAddExceptFilesValues() 985 986 self.setBackupWarningEnabled() 987 self.setUnhandledWarningEnabled() 988 989 def setAddExceptFilesValues(self) -> None: 990 self.exceptTheseFiles.clear() 991 if self.prefs.list_not_empty('ignore_unhandled_file_exts'): 992 self.exceptTheseFiles.addItems(self.prefs.ignore_unhandled_file_exts) 993 self.exceptTheseFiles.setCurrentRow(0) 994 995 def setBackupWarningEnabled(self) -> None: 996 self.warnBackupProblem.setEnabled(self.prefs.backup_files) 997 998 def setUnhandledWarningEnabled(self) -> None: 999 enabled = self.prefs.warn_unhandled_files 1000 for widget in (self.exceptTheseFilesLabel, self.exceptTheseFiles, self.addExceptFiles): 1001 widget.setEnabled(enabled) 1002 count = bool(self.exceptTheseFiles.count()) 1003 for widget in (self.removeExceptFiles, self.removeAllExceptFiles): 1004 widget.setEnabled(enabled and count) 1005 1006 def setConsolidatedValues(self) -> None: 1007 enabled = self.prefs.consolidate_identical 1008 self.consolidateIdentical.setChecked(enabled) 1009 1010 self.setTreatRawJpeg() 1011 self.setMarkRawJpeg() 1012 1013 if enabled: 1014 # Must turn off the exclusive button group feature, or else 1015 # it's impossible to set all the radio buttons to False 1016 self.noConsolidationGroup.setExclusive(False) 1017 for widget in ( 1018 self.clearCompletedDownloads, 1019 self.keepCompletedDownloads, self.promptCompletedDownloads): 1020 widget.setChecked(False) 1021 # Now turn it back on again 1022 self.noConsolidationGroup.setExclusive(True) 1023 else: 1024 self.setCompletedDownloadsValues() 1025 1026 self.setConsolidatedEnabled() 1027 1028 def setTreatRawJpeg(self) -> None: 1029 if self.prefs.consolidate_identical: 1030 if self.prefs.treat_raw_jpeg == int(TreatRawJpeg.one_photo): 1031 self.oneRawJpeg.setChecked(True) 1032 else: 1033 self.twoRawJpeg.setChecked(True) 1034 else: 1035 # Must turn off the exclusive button group feature, or else 1036 # it's impossible to set all the radio buttons to False 1037 self.treatRawJpegGroup.setExclusive(False) 1038 self.oneRawJpeg.setChecked(False) 1039 self.twoRawJpeg.setChecked(False) 1040 # Now turn it back on again 1041 self.treatRawJpegGroup.setExclusive(True) 1042 1043 def setMarkRawJpeg(self) -> None: 1044 if self.prefs.consolidate_identical and self.twoRawJpeg.isChecked(): 1045 v = self.prefs.mark_raw_jpeg 1046 if v == int(MarkRawJpeg.no_jpeg): 1047 self.noJpegWhenRaw.setChecked(True) 1048 elif v == int(MarkRawJpeg.no_raw): 1049 self.noRawWhenJpeg.setChecked(True) 1050 else: 1051 self.markRawJpeg.setChecked(True) 1052 else: 1053 # Must turn off the exclusive button group feature, or else 1054 # it's impossible to set all the radio buttons to False 1055 self.markRawJpegGroup.setExclusive(False) 1056 for widget in (self.noJpegWhenRaw, self.noRawWhenJpeg, self.markRawJpeg): 1057 widget.setChecked(False) 1058 # Now turn it back on again 1059 self.markRawJpegGroup.setExclusive(True) 1060 1061 def setConsolidatedEnabled(self) -> None: 1062 enabled = self.prefs.consolidate_identical 1063 1064 for widget in self.treatRawJpegGroup.buttons(): 1065 widget.setEnabled(enabled) 1066 self.treatRawJpegLabel.setEnabled(enabled) 1067 1068 self.setMarkRawJpegEnabled() 1069 1070 for widget in ( 1071 self.noconsolidationLabel, self.clearCompletedDownloads, 1072 self.keepCompletedDownloads, self.promptCompletedDownloads): 1073 widget.setEnabled(not enabled) 1074 1075 def setMarkRawJpegEnabled(self) -> None: 1076 mark_enabled = self.prefs.consolidate_identical and self.twoRawJpeg.isChecked() 1077 for widget in self.markRawJpegGroup.buttons(): 1078 widget.setEnabled(mark_enabled) 1079 self.markRawJpegLabel.setEnabled(mark_enabled) 1080 1081 def setVersionCheckValues(self) -> None: 1082 self.checkNewVersion.setChecked(self.prefs.check_for_new_versions) 1083 self.includeDevRelease.setChecked( 1084 self.prefs.include_development_release or self.is_prerelease 1085 ) 1086 self.setVersionCheckEnabled() 1087 1088 def setVersionCheckEnabled(self) -> None: 1089 self.includeDevRelease.setEnabled( 1090 not(self.is_prerelease or not self.prefs.check_for_new_versions) 1091 ) 1092 1093 def setMetdataValues(self) -> None: 1094 self.ignoreMdatatimeMtpDng.setChecked(self.prefs.ignore_mdatatime_for_mtp_dng) 1095 self.forceExiftool.setChecked(self.prefs.force_exiftool) 1096 1097 def setCompletedDownloadsValues(self) -> None: 1098 s = self.prefs.completed_downloads 1099 if s == int(CompletedDownloads.keep): 1100 self.keepCompletedDownloads.setChecked(True) 1101 elif s == int(CompletedDownloads.clear): 1102 self.clearCompletedDownloads.setChecked(True) 1103 else: 1104 self.promptCompletedDownloads.setChecked(True) 1105 1106 @pyqtSlot(int) 1107 def onlyExternalChanged(self, state: int) -> None: 1108 self.prefs.only_external_mounts = state == Qt.Checked 1109 if self.rapidApp is not None: 1110 self.rapidApp.search_for_devices_again = True 1111 1112 @pyqtSlot(int) 1113 def noDcimChanged(self, state: int) -> None: 1114 self.prefs.scan_specific_folders = state == Qt.Checked 1115 self.setFoldersToScanState() 1116 if self.rapidApp is not None: 1117 self.rapidApp.scan_non_cameras_again = True 1118 1119 @pyqtSlot(int) 1120 def ignoredPathsReChanged(self, state: int) -> None: 1121 self.prefs.use_re_ignored_paths = state == Qt.Checked 1122 if self.rapidApp is not None: 1123 self.rapidApp.scan_all_again = True 1124 1125 def _equalizeWidgetWidth(self, widget_list) -> None: 1126 max_width = max(widget.width() for widget in widget_list) 1127 for widget in widget_list: 1128 widget.setFixedWidth(max_width) 1129 1130 def showEvent(self, e: QShowEvent): 1131 self.chooser.minimum_width = self.restoreButton.width() 1132 self._equalizeWidgetWidth(self.device_right_side_buttons) 1133 self._equalizeWidgetWidth(self.device_list_widgets) 1134 super().showEvent(e) 1135 1136 @pyqtSlot(int) 1137 def rowChanged(self, row: int) -> None: 1138 self.panels.setCurrentIndex(row) 1139 # Translators: substituted value is a description for the set of preferences 1140 # shown in the preference dialog window, e.g. Devices, Automation, etc. 1141 # This string is shown in a tooltip for the "Restore Defaults" button 1142 self.restoreButton.setToolTip(_('Restores default %s preference values') % 1143 self.chooser_items[row]) 1144 1145 @pyqtSlot() 1146 def removeDeviceClicked(self) -> None: 1147 row = self.knownDevices.currentRow() 1148 item = self.knownDevices.takeItem(row) # type: QListWidgetItem 1149 known_device_type = item.type() 1150 if known_device_type == KnownDeviceType.volume_whitelist: 1151 self.prefs.del_list_value('volume_whitelist', item.text()) 1152 elif known_device_type == KnownDeviceType.volume_blacklist: 1153 self.prefs.del_list_value('volume_blacklist', item.text()) 1154 else: 1155 assert known_device_type == KnownDeviceType.camera_blacklist 1156 self.prefs.del_list_value('camera_blacklist', item.text()) 1157 1158 self.removeDevice.setEnabled(self.knownDevices.count()) 1159 self.removeAllDevice.setEnabled(self.knownDevices.count()) 1160 1161 if self.rapidApp is not None: 1162 self.rapidApp.search_for_devices_again = True 1163 1164 @pyqtSlot() 1165 def removeAllDeviceClicked(self) -> None: 1166 self.knownDevices.clear() 1167 self.prefs.volume_whitelist = [''] 1168 self.prefs.volume_blacklist = [''] 1169 self.prefs.camera_blacklist = [''] 1170 self.removeDevice.setEnabled(False) 1171 self.removeAllDevice.setEnabled(False) 1172 1173 if self.rapidApp is not None: 1174 self.rapidApp.search_for_devices_again = True 1175 1176 @pyqtSlot() 1177 def removeFolderToScanClicked(self) -> None: 1178 row = self.foldersToScan.currentRow() 1179 if row >= 0 and self.foldersToScan.count() > 1: 1180 item = self.foldersToScan.takeItem(row) 1181 self.prefs.del_list_value('folders_to_scan', item.text()) 1182 self.removeFolderToScan.setEnabled(self.foldersToScan.count() > 1) 1183 1184 if self.rapidApp is not None: 1185 self.rapidApp.scan_all_again = True 1186 1187 @pyqtSlot() 1188 def addFolderToScanClicked(self) -> None: 1189 dlg = FoldersToScanDialog(prefs=self.prefs, parent=self) 1190 if dlg.exec(): 1191 self.setFoldersToScanWidgetValues() 1192 1193 if self.rapidApp is not None: 1194 self.rapidApp.scan_all_again = True 1195 1196 @pyqtSlot() 1197 def removePathClicked(self) -> None: 1198 row = self.ignoredPaths.currentRow() 1199 if row >= 0: 1200 item = self.ignoredPaths.takeItem(row) 1201 self.prefs.del_list_value('ignored_paths', item.text()) 1202 self.removePath.setEnabled(self.ignoredPaths.count()) 1203 self.removeAllPath.setEnabled(self.ignoredPaths.count()) 1204 1205 if self.rapidApp is not None: 1206 self.rapidApp.scan_all_again = True 1207 1208 @pyqtSlot() 1209 def removeAllPathClicked(self) -> None: 1210 self.ignoredPaths.clear() 1211 self.prefs.ignored_paths = [''] 1212 self.removePath.setEnabled(False) 1213 self.removeAllPath.setEnabled(False) 1214 1215 if self.rapidApp is not None: 1216 self.rapidApp.scan_all_again = True 1217 1218 @pyqtSlot() 1219 def addPathClicked(self) -> None: 1220 dlg = IgnorePathDialog(prefs=self.prefs, parent=self) 1221 if dlg.exec(): 1222 self.setIgnorePathWidgetValues() 1223 1224 if self.rapidApp is not None: 1225 self.rapidApp.scan_all_again = True 1226 1227 @pyqtSlot() 1228 def ignorePathsReLabelClicked(self) -> None: 1229 self.ignoredPathsRe.click() 1230 1231 @pyqtSlot(int) 1232 def languagesChanged(self, index: int) -> None: 1233 if index == 0: 1234 self.prefs.language = '' 1235 logging.info("Resetting user interface language to system default") 1236 elif index > 0: 1237 self.prefs.language = self.languages.currentData() 1238 logging.info("Setting user interface language to %s", self.prefs.language) 1239 1240 @pyqtSlot(int) 1241 def autoDownloadStartupChanged(self, state: int) -> None: 1242 self.prefs.auto_download_at_startup = state == Qt.Checked 1243 1244 @pyqtSlot(int) 1245 def autoDownloadInsertionChanged(self, state: int) -> None: 1246 self.prefs.auto_download_upon_device_insertion = state == Qt.Checked 1247 1248 @pyqtSlot(int) 1249 def autoEjectChanged(self, state: int) -> None: 1250 self.prefs.auto_unmount = state == Qt.Checked 1251 1252 @pyqtSlot(int) 1253 def autoExitChanged(self, state: int) -> None: 1254 auto_exit = state == Qt.Checked 1255 self.prefs.auto_exit = auto_exit 1256 self.setAutoExitErrorState() 1257 if not auto_exit: 1258 self.prefs.auto_exit_force = False 1259 1260 @pyqtSlot(int) 1261 def autoExitErrorChanged(self, state: int) -> None: 1262 self.prefs.auto_exit_force = state == Qt.Checked 1263 1264 @pyqtSlot(int) 1265 def generateThumbnailsChanged(self, state: int) -> None: 1266 self.prefs.generate_thumbnails = state == Qt.Checked 1267 self.setPerformanceValues(check_boxes_only=True) 1268 self.setPerfomanceEnabled() 1269 1270 @pyqtSlot(int) 1271 def useThumbnailCacheChanged(self, state: int) -> None: 1272 if self.prefs.generate_thumbnails: 1273 self.prefs.use_thumbnail_cache = state == Qt.Checked 1274 1275 @pyqtSlot(int) 1276 def fdoThumbnailsChanged(self, state: int) -> None: 1277 if self.prefs.generate_thumbnails: 1278 self.prefs.save_fdo_thumbnails = state == Qt.Checked 1279 1280 @pyqtSlot(int) 1281 def thumbnailCacheDaysKeepChanged(self, value: int) -> None: 1282 self.prefs.keep_thumbnails_days = value 1283 1284 @pyqtSlot(int) 1285 def maxCoresChanged(self, index: int) -> None: 1286 if index >= 0: 1287 self.prefs.max_cpu_cores = int(self.maxCores.currentText()) 1288 1289 @pyqtSlot() 1290 def purgeCacheClicked(self) -> None: 1291 message = _( 1292 'Do you want to purge the thumbnail cache? The cache will be purged when the ' 1293 'program is next started.' 1294 ) 1295 msgBox = standardMessageBox( 1296 parent=self, title=_('Purge Thumbnail Cache'), message=message, 1297 standardButtons=QMessageBox.Yes | QMessageBox.No, rich_text=False 1298 ) 1299 1300 if msgBox.exec_() == QMessageBox.Yes: 1301 self.prefs.purge_thumbnails = True 1302 self.prefs.optimize_thumbnail_db = False 1303 else: 1304 self.prefs.purge_thumbnails = False 1305 1306 @pyqtSlot() 1307 def optimizeCacheClicked(self) -> None: 1308 message = _( 1309 'Do you want to optimize the thumbnail cache? The cache will be optimized when ' 1310 'the program is next started.' 1311 ) 1312 msgBox = standardMessageBox( 1313 parent=self, title=_('Optimize Thumbnail Cache'), message=message, 1314 standardButtons=QMessageBox.Yes|QMessageBox.No, rich_text=False 1315 ) 1316 if msgBox.exec_() == QMessageBox.Yes: 1317 self.prefs.purge_thumbnails = False 1318 self.prefs.optimize_thumbnail_db = True 1319 else: 1320 self.prefs.optimize_thumbnail_db = False 1321 1322 @pyqtSlot(QAbstractButton) 1323 def downloadErrorGroupClicked(self, button: QRadioButton) -> None: 1324 if self.downloadErrorGroup.checkedButton() == self.skipDownload: 1325 self.prefs.conflict_resolution = int(ConflictResolution.skip) 1326 else: 1327 self.prefs.conflict_resolution = int(ConflictResolution.add_identifier) 1328 1329 @pyqtSlot(QAbstractButton) 1330 def backupErrorGroupClicked(self, button: QRadioButton) -> None: 1331 self.prefs.backup_duplicate_overwrite = self.backupErrorGroup.checkedButton() == \ 1332 self.overwriteBackup 1333 1334 @pyqtSlot(int) 1335 def warnDownloadingAllChanged(self, state: int) -> None: 1336 self.prefs.warn_downloading_all = state == Qt.Checked 1337 1338 @pyqtSlot(int) 1339 def warnBackupProblemChanged(self, state: int) -> None: 1340 self.prefs.warn_backup_problem = state == Qt.Checked 1341 1342 @pyqtSlot(int) 1343 def warnMissingLibrariesChanged(self, state: int) -> None: 1344 self.prefs.warn_broken_or_missing_libraries = state == Qt.Checked 1345 1346 @pyqtSlot(int) 1347 def warnMetadataChanged(self, state: int) -> None: 1348 self.prefs.warn_fs_metadata_error = state == Qt.Checked 1349 1350 @pyqtSlot(int) 1351 def warnUnhandledFilesChanged(self, state: int) -> None: 1352 self.prefs.warn_unhandled_files = state == Qt.Checked 1353 self.setUnhandledWarningEnabled() 1354 1355 @pyqtSlot() 1356 def addExceptFilesClicked(self) -> None: 1357 dlg = ExceptFileExtDialog(prefs=self.prefs, parent=self) 1358 if dlg.exec(): 1359 self.setAddExceptFilesValues() 1360 1361 @pyqtSlot() 1362 def removeExceptFilesClicked(self) -> None: 1363 row = self.exceptTheseFiles.currentRow() 1364 if row >= 0: 1365 item = self.exceptTheseFiles.takeItem(row) 1366 self.prefs.del_list_value('ignore_unhandled_file_exts', item.text()) 1367 self.removeExceptFiles.setEnabled(self.exceptTheseFiles.count()) 1368 self.removeAllExceptFiles.setEnabled(self.exceptTheseFiles.count()) 1369 1370 @pyqtSlot() 1371 def removeAllExceptFilesClicked(self) -> None: 1372 self.exceptTheseFiles.clear() 1373 self.prefs.ignore_unhandled_file_exts = [''] 1374 self.removeExceptFiles.setEnabled(False) 1375 self.removeAllExceptFiles.setEnabled(False) 1376 1377 @pyqtSlot(int) 1378 def consolidateIdenticalChanged(self, state: int) -> None: 1379 self.prefs.consolidate_identical = state == Qt.Checked 1380 self.setConsolidatedValues() 1381 self.setConsolidatedEnabled() 1382 1383 @pyqtSlot(QAbstractButton) 1384 def treatRawJpegGroupClicked(self, button: QRadioButton) -> None: 1385 if button == self.oneRawJpeg: 1386 self.prefs.treat_raw_jpeg = int(TreatRawJpeg.one_photo) 1387 else: 1388 self.prefs.treat_raw_jpeg = int(TreatRawJpeg.two_photos) 1389 self.setMarkRawJpeg() 1390 self.setMarkRawJpegEnabled() 1391 1392 @pyqtSlot(QAbstractButton) 1393 def markRawJpegGroupClicked(self, button: QRadioButton) -> None: 1394 if button == self.noJpegWhenRaw: 1395 self.prefs.mark_raw_jpeg = int(MarkRawJpeg.no_jpeg) 1396 elif button == self.noRawWhenJpeg: 1397 self.prefs.mark_raw_jpeg = int(MarkRawJpeg.no_raw) 1398 else: 1399 self.prefs.mark_raw_jpeg = int(MarkRawJpeg.both) 1400 1401 @pyqtSlot(int) 1402 def noJpegWhenRawChanged(self, state: int) -> None: 1403 self.prefs.do_not_mark_jpeg = state == Qt.Checked 1404 1405 @pyqtSlot(int) 1406 def noRawWhenJpegChanged(self, state: int) -> None: 1407 self.prefs.do_not_mark_raw = state == Qt.Checked 1408 1409 @pyqtSlot(int) 1410 def checkNewVersionChanged(self, state: int) -> None: 1411 do_check = state == Qt.Checked 1412 self.prefs.check_for_new_versions = do_check 1413 self.setVersionCheckEnabled() 1414 1415 @pyqtSlot(int) 1416 def includeDevReleaseChanged(self, state: int) -> None: 1417 self.prefs.include_development_release = state == Qt.Checked 1418 1419 @pyqtSlot(int) 1420 def ignoreMdatatimeMtpDngChanged(self, state: int) -> None: 1421 self.prefs.ignore_mdatatime_for_mtp_dng = state == Qt.Checked 1422 1423 @pyqtSlot(int) 1424 def forceExiftoolChanged(self, state: int) -> None: 1425 self.prefs.force_exiftool = state == Qt.Checked 1426 1427 @pyqtSlot(QAbstractButton) 1428 def noConsolidationGroupClicked(self, button: QRadioButton) -> None: 1429 if button == self.keepCompletedDownloads: 1430 self.prefs.completed_downloads = int(CompletedDownloads.keep) 1431 elif button == self.clearCompletedDownloads: 1432 self.prefs.completed_downloads = int(CompletedDownloads.clear) 1433 else: 1434 self.prefs.completed_downloads = int(CompletedDownloads.prompt) 1435 1436 @pyqtSlot() 1437 def restoreDefaultsClicked(self) -> None: 1438 row = self.chooser.currentRow() 1439 if row == 0: 1440 for value in ('only_external_mounts', 'scan_specific_folders', 'folders_to_scan', 1441 'ignored_paths', 'use_re_ignored_paths'): 1442 self.prefs.restore(value) 1443 self.removeAllDeviceClicked() 1444 self.setDeviceWidgetValues() 1445 elif row == 1: 1446 self.prefs.restore('language') 1447 self.languages.setCurrentIndex(0) 1448 elif row == 2: 1449 for value in ('auto_download_at_startup', 'auto_download_upon_device_insertion', 1450 'auto_unmount', 'auto_exit', 'auto_exit_force'): 1451 self.prefs.restore(value) 1452 self.setAutomationWidgetValues() 1453 elif row == 3: 1454 for value in ('generate_thumbnails', 'use_thumbnail_cache', 'save_fdo_thumbnails', 1455 'max_cpu_cores', 'keep_thumbnails_days'): 1456 self.prefs.restore(value) 1457 self.setPerformanceValues(check_boxes_only=True) 1458 self.maxCores.setCurrentText(str(self.prefs.max_cpu_cores)) 1459 self.setPerfomanceEnabled() 1460 self.thumbnailCacheDaysKeep.setValue(self.prefs.keep_thumbnails_days) 1461 elif row == 4: 1462 for value in ('conflict_resolution', 'backup_duplicate_overwrite'): 1463 self.prefs.restore(value) 1464 self.setErrorHandingValues() 1465 elif row == 5: 1466 for value in ( 1467 'warn_downloading_all', 'warn_backup_problem', 1468 'warn_broken_or_missing_libraries', 'warn_fs_metadata_error', 1469 'warn_unhandled_files', 'ignore_unhandled_file_exts'): 1470 self.prefs.restore(value) 1471 self.setWarningValues() 1472 elif row == 6 and consolidation_implemented: 1473 for value in ( 1474 'completed_downloads', 'consolidate_identical', 'one_raw_jpeg', 1475 'do_not_mark_jpeg', 'do_not_mark_raw'): 1476 self.prefs.restore(value) 1477 self.setConsolidatedValues() 1478 elif (row == 7 and consolidation_implemented) or (row == 6 and not 1479 consolidation_implemented): 1480 if not version_check_disabled(): 1481 self.prefs.restore('check_for_new_versions') 1482 for value in ('include_development_release', 'ignore_mdatatime_for_mtp_dng', 1483 'force_exiftool'): 1484 self.prefs.restore(value) 1485 if not consolidation_implemented: 1486 self.prefs.restore('completed_downloads') 1487 if not version_check_disabled(): 1488 self.setVersionCheckValues() 1489 self.setMetdataValues() 1490 if not consolidation_implemented: 1491 self.setCompletedDownloadsValues() 1492 1493 @pyqtSlot() 1494 def helpButtonClicked(self) -> None: 1495 row = self.chooser.currentRow() 1496 if row == 0: 1497 location = '#devicepreferences' 1498 elif row == 1: 1499 location = '#languagepreferences' 1500 elif row == 2: 1501 location = '#automationpreferences' 1502 elif row == 3: 1503 location = '#thumbnailpreferences' 1504 elif row == 4: 1505 location = '#errorhandlingpreferences' 1506 elif row == 5: 1507 location = '#warningpreferences' 1508 elif row == 6: 1509 if consolidation_implemented: 1510 location = '#consolidationpreferences' 1511 else: 1512 location = '#miscellaneousnpreferences' 1513 elif row == 7: 1514 location = '#miscellaneousnpreferences' 1515 else: 1516 location = '' 1517 1518 webbrowser.open_new_tab( 1519 "https://www.damonlynch.net/rapid/documentation/{}".format(location) 1520 ) 1521 1522 def closeEvent(self, event: QCloseEvent) -> None: 1523 self.cacheSizeThread.quit() 1524 self.cacheSizeThread.wait(1000) 1525 event.accept() 1526 1527 1528class PreferenceAddDialog(QDialog): 1529 """ 1530 Base class for adding value to pref list 1531 """ 1532 def __init__(self, prefs: Preferences, 1533 title: str, 1534 instruction: str, 1535 label: str, 1536 pref_value: str, 1537 parent=None) -> None: 1538 super().__init__(parent=parent) 1539 1540 self.prefs = prefs 1541 self.pref_value = pref_value 1542 1543 self.setWindowTitle(title) 1544 1545 self.instructionLabel = QLabel(instruction) 1546 self.instructionLabel.setWordWrap(False) 1547 layout = QVBoxLayout() 1548 self.setLayout(layout) 1549 1550 self.valueEdit = QLineEdit() 1551 formLayout = QFormLayout() 1552 formLayout.addRow(label, self.valueEdit) 1553 1554 buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) 1555 translateDialogBoxButtons(buttons) 1556 buttons.rejected.connect(self.reject) 1557 buttons.accepted.connect(self.accept) 1558 1559 layout.addWidget(self.instructionLabel) 1560 layout.addLayout(formLayout) 1561 layout.addWidget(buttons) 1562 1563 def accept(self): 1564 value = self.valueEdit.text() 1565 if value: 1566 self.prefs.add_list_value(self.pref_value, value) 1567 super().accept() 1568 1569 1570class FoldersToScanDialog(PreferenceAddDialog): 1571 """ 1572 Dialog prompting for a folder on devices to scan for photos and videos 1573 """ 1574 def __init__(self, prefs: Preferences, parent=None) -> None: 1575 super().__init__( 1576 prefs=prefs, 1577 title=_('Enter a Folder to Scan'), 1578 instruction=_('Specify a folder that will be scanned for photos and videos'), 1579 label=_('Folder:'), 1580 pref_value='folders_to_scan', 1581 parent=parent 1582 ) 1583 1584 1585class IgnorePathDialog(PreferenceAddDialog): 1586 """ 1587 Dialog prompting for a path to ignore when scanning devices 1588 """ 1589 1590 def __init__(self, prefs: Preferences, parent=None) -> None: 1591 super().__init__( 1592 prefs=prefs, 1593 title=_('Enter a Path to Ignore'), 1594 instruction=_('Specify a path that will never be scanned for photos or videos'), 1595 label=_('Path:'), 1596 pref_value='ignored_paths', 1597 parent=parent 1598 ) 1599 1600 1601class ExceptFileExtDialog(PreferenceAddDialog): 1602 """ 1603 Dialog prompting for file extensions never to warn about 1604 """ 1605 1606 def __init__(self, prefs: Preferences, parent=None) -> None: 1607 super().__init__( 1608 prefs=prefs, 1609 title=_('Enter a File Extension'), 1610 instruction=_('Specify a file extension (without the leading dot)'), 1611 label=_('Extension:'), 1612 pref_value='ignore_unhandled_file_exts', 1613 parent=parent 1614 ) 1615 1616 def exts(self, exts: List[str]) -> str: 1617 return make_internationalized_list([ext.upper() for ext in exts]) 1618 1619 def accept(self): 1620 value = self.valueEdit.text() 1621 if value: 1622 while value.startswith('.'): 1623 value = value[1:] 1624 value = value.upper() 1625 if value.lower() in ALL_KNOWN_EXTENSIONS: 1626 title = _('Invalid File Extension') 1627 # Translators: please do not change HTML codes like <br>, <i>, </i>, or <b>, </b> 1628 # etc. 1629 message = _( 1630 "The file extension <b>%s</b> is recognized by Rapid Photo Downloader, so it " 1631 "makes no sense to warn about its presence." 1632 ) % value 1633 # Translators: %(variable)s represents Python code, not a plural of the term 1634 # variable. You must keep the %(variable)s untranslated, or the program will 1635 # crash. 1636 details = _( 1637 'Recognized file types:\n\n' 1638 'Photos:\n%(photos)s\n\nVideos:\n%(videos)s\n\n' 1639 'Audio:\n%(audio)s\n\nOther:\n%(other)s' 1640 ) % dict( 1641 photos=self.exts(PHOTO_EXTENSIONS), 1642 videos=self.exts(VIDEO_EXTENSIONS + VIDEO_THUMBNAIL_EXTENSIONS), 1643 audio=self.exts(AUDIO_EXTENSIONS), 1644 other=self.exts(['xmp']) 1645 ) 1646 msgBox = standardMessageBox( 1647 parent=self, title=title, message=message, rich_text=True, 1648 standardButtons=QMessageBox.Ok, iconType=QMessageBox.Information 1649 ) 1650 msgBox.setDetailedText(details) 1651 msgBox.exec() 1652 self.valueEdit.setText(value) 1653 self.valueEdit.selectAll() 1654 return 1655 else: 1656 self.prefs.add_list_value(self.pref_value, value) 1657 QDialog.accept(self) 1658 1659 1660class CacheSize(QObject): 1661 size = pyqtSignal('PyQt_PyObject') # don't convert python int to C++ int 1662 1663 @pyqtSlot() 1664 def start(self) -> None: 1665 self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False) 1666 1667 @pyqtSlot() 1668 def getCacheSize(self) -> None: 1669 self.size.emit(self.thumbnail_cache.cache_size()) 1670 1671 1672if __name__ == '__main__': 1673 1674 # Application development test code: 1675 1676 app = QApplication([]) 1677 1678 app.setOrganizationName("Rapid Photo Downloader") 1679 app.setOrganizationDomain("damonlynch.net") 1680 app.setApplicationName("Rapid Photo Downloader") 1681 1682 prefs = Preferences() 1683 1684 prefDialog = PreferencesDialog(prefs) 1685 prefDialog.show() 1686 app.exec_() 1687