diff options
Diffstat (limited to 'raphodo/preferencedialog.py')
-rw-r--r-- | raphodo/preferencedialog.py | 1580 |
1 files changed, 1580 insertions, 0 deletions
diff --git a/raphodo/preferencedialog.py b/raphodo/preferencedialog.py new file mode 100644 index 0000000..238ecd4 --- /dev/null +++ b/raphodo/preferencedialog.py @@ -0,0 +1,1580 @@ +# Copyright (C) 2017 Damon Lynch <damonlynch@gmail.com> + +# This file is part of Rapid Photo Downloader. +# +# Rapid Photo Downloader is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Rapid Photo Downloader is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Rapid Photo Downloader. If not, +# see <http://www.gnu.org/licenses/>. + +""" +Dialog window to show and manipulate selected user preferences +""" + +__author__ = 'Damon Lynch' +__copyright__ = "Copyright 2017, Damon Lynch" + +import webbrowser +from typing import List +from gettext import gettext as _ + + +from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal, QObject, QThread, QTimer, QSize) +from PyQt5.QtWidgets import ( + QWidget, QSizePolicy, QComboBox, QVBoxLayout, QLabel, QLineEdit, QSpinBox, QGridLayout, + QAbstractItemView, QListWidgetItem, QHBoxLayout, QDialog, QDialogButtonBox, QCheckBox, + QStyle, QStackedWidget, QApplication, QPushButton, QGroupBox, QFormLayout, QMessageBox, + QButtonGroup, QRadioButton, QAbstractButton +) +from PyQt5.QtGui import ( + QShowEvent, QCloseEvent, QMouseEvent, QIcon, QFont, QFontMetrics, QPixmap, QPalette +) + +from raphodo.preferences import Preferences +from raphodo.constants import (KnownDeviceType, CompletedDownloads, TreatRawJpeg, MarkRawJpeg) +from raphodo.viewutils import QNarrowListWidget, translateButtons +from raphodo.utilities import available_cpu_count, format_size_for_user, thousands +from raphodo.cache import ThumbnailCacheSql +from raphodo.constants import ConflictResolution +from raphodo.utilities import current_version_is_dev_version, make_internationalized_list +from raphodo.rpdfile import ( + ALL_KNOWN_EXTENSIONS, PHOTO_EXTENSIONS, VIDEO_EXTENSIONS, VIDEO_THUMBNAIL_EXTENSIONS, + AUDIO_EXTENSIONS +) +import raphodo.qrc_resources as qrc_resources + + +class ClickableLabel(QLabel): + clicked = pyqtSignal() + + def mousePressEvent(self, event: QMouseEvent) -> None: + self.clicked.emit() + + +consolidation_implemented = False +# consolidation_implemented = True + + + +class PreferencesDialog(QDialog): + """ + Preferences dialog for those preferences that are not adjusted via the main window + + Note: + + When pref value generate_thumbnails is made False, pref values use_thumbnail_cache and + generate_thumbnails are not changed, even though the preference value shown to the user + shows False (to indicate that the activity will not occur). + """ + + getCacheSize = pyqtSignal() + + def __init__(self, prefs: Preferences, parent=None) -> None: + super().__init__(parent=parent) + + self.rapidApp = parent + + self.setWindowTitle(_('Preferences')) + + self.prefs = prefs + + self.is_prerelease = current_version_is_dev_version() + + self.panels = QStackedWidget() + + self.chooser = QNarrowListWidget(no_focus_recentangle=True) + + font = QFont() + fontMetrics = QFontMetrics(font) + icon_padding = 6 + icon_height = max(fontMetrics.height(), 16) + icon_width = icon_height + icon_padding + self.chooser.setIconSize(QSize(icon_width, icon_height)) + + palette = QPalette() + selectedColour = palette.color(palette.HighlightedText) + + if consolidation_implemented: + self.chooser_items = ( + _('Devices'), _('Automation'), _('Thumbnails'), _('Error Handling'), _('Warnings'), + _('Consolidation'), _('Miscellaneous') + ) + icons = ( + ":/prefs/devices.svg", ":/prefs/automation.svg", ":/prefs/thumbnails.svg", + ":/prefs/error-handling.svg", ":/prefs/warnings.svg", ":/prefs/consolidation.svg", + ":/prefs/miscellaneous.svg" + ) + else: + self.chooser_items = ( + _('Devices'), _('Automation'), _('Thumbnails'), _('Error Handling'), _('Warnings'), + _('Miscellaneous') + ) + icons = ( + ":/prefs/devices.svg", ":/prefs/automation.svg", ":/prefs/thumbnails.svg", + ":/prefs/error-handling.svg", ":/prefs/warnings.svg", ":/prefs/miscellaneous.svg" + ) + + for prefIcon, label in zip(icons, self.chooser_items): + # make the selected icons be the same colour as the selected text + icon = QIcon() + pixmap = QPixmap(prefIcon) + selected = QPixmap(pixmap.size()) + selected.fill(selectedColour) + selected.setMask(pixmap.createMaskFromColor(Qt.transparent)) + icon.addPixmap(pixmap, QIcon.Normal) + icon.addPixmap(selected, QIcon.Selected) + + item = QListWidgetItem(icon, label, self.chooser) + item.setFont(QFont()) + width = fontMetrics.width(label) + icon_width + icon_padding * 2 + item.setSizeHint(QSize(width, icon_height * 2)) + + self.chooser.currentRowChanged.connect(self.rowChanged) + self.chooser.setSelectionMode(QAbstractItemView.SingleSelection) + self.chooser.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) + + self.devices = QWidget() + + self.scanBox = QGroupBox(_('Device Scanning')) + self.onlyExternal = QCheckBox(_('Scan only external devices')) + self.onlyExternal.setToolTip(_( + 'Scan for photos and videos only on devices that are external to the computer,\n' + 'including cameras, memory cards, external hard drives, and USB flash drives.' + )) + self.scanSpecificFolders = QCheckBox(_('Scan only specific folders on devices')) + tip = _( + 'Scan for photos and videos only in the folders specified below (except paths\n' + 'specified in Ignored Paths).\n\n' + 'Changing this setting causes all devices to be scanned again.' + ) + self.scanSpecificFolders.setToolTip(tip) + + self.foldersToScanLabel = QLabel(_('Folders to scan:')) + self.foldersToScan = QNarrowListWidget(minimum_rows=5) + self.foldersToScan.setToolTip(_( + 'Folders at the base level of device file systems that will be scanned\n' + 'for photos and videos.' + )) + self.addFolderToScan = QPushButton(_('Add...')) + self.addFolderToScan.setToolTip(_( + 'Add a folder to the list of folders to scan for photos and videos.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + self.removeFolderToScan = QPushButton(_('Remove')) + self.removeFolderToScan.setToolTip(_( + 'Remove a folder from the list of folders to scan for photos and videos.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + + self.addFolderToScan.clicked.connect(self.addFolderToScanClicked) + self.removeFolderToScan.clicked.connect(self.removeFolderToScanClicked) + + scanLayout = QGridLayout() + scanLayout.setHorizontalSpacing(18) + scanLayout.addWidget(self.onlyExternal, 0, 0, 1, 3) + scanLayout.addWidget(self.scanSpecificFolders, 1, 0, 1, 3) + scanLayout.addWidget(self.foldersToScanLabel, 2, 1, 1, 2) + scanLayout.addWidget(self.foldersToScan, 3, 1, 3, 1) + scanLayout.addWidget(self.addFolderToScan, 3, 2, 1, 1) + scanLayout.addWidget(self.removeFolderToScan, 4, 2, 1, 1) + self.scanBox.setLayout(scanLayout) + + tip = _('Devices that have been set to automatically ignore or download from.') + self.knownDevicesBox = QGroupBox(_('Remembered Devices')) + self.knownDevices = QNarrowListWidget(minimum_rows=5) + self.knownDevices.setToolTip(tip) + tip = _( + 'Remove a device from the list of devices to automatically ignore or download from.' + ) + self.removeDevice = QPushButton(_('Remove')) + self.removeDevice.setToolTip(tip) + self.removeAllDevice = QPushButton(_('Remove All')) + tip = _( + 'Clear the list of devices from which to automatically ignore or download from.\n\n' + 'Note: Changes take effect when the computer is next scanned for devices.' + ) + self.removeAllDevice.setToolTip(tip) + self.removeDevice.clicked.connect(self.removeDeviceClicked) + self.removeAllDevice.clicked.connect(self.removeAllDeviceClicked) + knownDevicesLayout = QGridLayout() + knownDevicesLayout.setHorizontalSpacing(18) + knownDevicesLayout.addWidget(self.knownDevices, 0, 0, 3, 1) + knownDevicesLayout.addWidget(self.removeDevice, 0, 1, 1, 1) + knownDevicesLayout.addWidget(self.removeAllDevice, 1, 1, 1, 1) + self.knownDevicesBox.setLayout(knownDevicesLayout) + + self.ignoredPathsBox = QGroupBox(_('Ignored Paths')) + tip = _('The end part of a path that should never be scanned for photos or videos.') + self.ignoredPaths = QNarrowListWidget(minimum_rows=4) + self.ignoredPaths.setToolTip(tip) + self.addPath = QPushButton(_('Add...')) + self.addPath.setToolTip(_( + 'Add a path to the list of paths to ignore.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + self.removePath = QPushButton(_('Remove')) + self.removePath.setToolTip(_( + 'Remove a path from the list of paths to ignore.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + self.removeAllPath = QPushButton(_('Remove All')) + self.removeAllPath.setToolTip(_( + 'Clear the list of paths to ignore.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + self.addPath.clicked.connect(self.addPathClicked) + self.removePath.clicked.connect(self.removePathClicked) + self.removeAllPath.clicked.connect(self.removeAllPathClicked) + self.ignoredPathsRe = QCheckBox() + self.ignorePathsReLabel = ClickableLabel( + _('Use python-style ' + '<a href="http://damonlynch.net/rapid/documentation/#regularexpressions">regular ' + 'expressions</a>')) + self.ignorePathsReLabel.setToolTip(_( + 'Use regular expressions in the list of ignored paths.\n\n' + 'Changing this setting causes all devices to be scanned again.' + )) + self.ignorePathsReLabel.setTextInteractionFlags(Qt.TextBrowserInteraction) + self.ignorePathsReLabel.setOpenExternalLinks(True) + self.ignorePathsReLabel.clicked.connect(self.ignorePathsReLabelClicked) + reLayout = QHBoxLayout() + reLayout.setSpacing(5) + reLayout.addWidget(self.ignoredPathsRe) + reLayout.addWidget(self.ignorePathsReLabel) + reLayout.addStretch() + ignoredPathsLayout = QGridLayout() + ignoredPathsLayout.setHorizontalSpacing(18) + ignoredPathsLayout.addWidget(self.ignoredPaths, 0, 0, 4, 1) + ignoredPathsLayout.addWidget(self.addPath, 0, 1, 1, 1) + ignoredPathsLayout.addWidget(self.removePath, 1, 1, 1, 1) + ignoredPathsLayout.addWidget(self.removeAllPath, 2, 1, 1, 1) + ignoredPathsLayout.addLayout(reLayout, 4, 0, 1, 2) + self.ignoredPathsBox.setLayout(ignoredPathsLayout) + + self.setDeviceWidgetValues() + + # connect these next 3 only after having set their values, so rescan / search again + # in rapidApp is not triggered + self.onlyExternal.stateChanged.connect(self.onlyExternalChanged) + self.scanSpecificFolders.stateChanged.connect(self.noDcimChanged) + self.ignoredPathsRe.stateChanged.connect(self.ignoredPathsReChanged) + + devicesLayout = QVBoxLayout() + devicesLayout.addWidget(self.scanBox) + devicesLayout.addWidget(self.ignoredPathsBox) + devicesLayout.addWidget(self.knownDevicesBox) + devicesLayout.addStretch() + devicesLayout.setSpacing(18) + + self.devices.setLayout(devicesLayout) + devicesLayout.setContentsMargins(0, 0, 0, 0) + + self.automation = QWidget() + + self.automationBox = QGroupBox(_('Program Automation')) + self.autoDownloadStartup = QCheckBox(_('Start downloading at program startup')) + self.autoDownloadInsertion = QCheckBox(_('Start downloading upon device insertion')) + self.autoEject = QCheckBox(_('Unmount (eject) device upon download completion')) + self.autoExit = QCheckBox(_('Exit program when download completes')) + self.autoExitError = QCheckBox(_('Exit program even if download had warnings or errors')) + self.setAutomationWidgetValues() + self.autoDownloadStartup.stateChanged.connect(self.autoDownloadStartupChanged) + self.autoDownloadInsertion.stateChanged.connect(self.autoDownloadInsertionChanged) + self.autoEject.stateChanged.connect(self.autoEjectChanged) + self.autoExit.stateChanged.connect(self.autoExitChanged) + self.autoExitError.stateChanged.connect(self.autoExitErrorChanged) + + automationBoxLayout = QGridLayout() + automationBoxLayout.addWidget(self.autoDownloadStartup, 0, 0, 1, 2) + automationBoxLayout.addWidget(self.autoDownloadInsertion, 1, 0, 1, 2) + automationBoxLayout.addWidget(self.autoEject, 2, 0, 1, 2) + automationBoxLayout.addWidget(self.autoExit, 3, 0, 1, 2) + automationBoxLayout.addWidget(self.autoExitError, 4, 1, 1, 1) + checkbox_width = self.autoExit.style().pixelMetric(QStyle.PM_IndicatorWidth) + automationBoxLayout.setColumnMinimumWidth(0, checkbox_width) + self.automationBox.setLayout(automationBoxLayout) + + automationLayout = QVBoxLayout() + automationLayout.addWidget(self.automationBox) + automationLayout.addStretch() + automationLayout.setContentsMargins(0, 0, 0, 0) + + self.automation.setLayout(automationLayout) + + self.performance = QWidget() + + self.performanceBox = QGroupBox(_('Thumbnail Generation')) + self.generateThumbnails = QCheckBox(_('Generate thumbnails')) + self.generateThumbnails.setToolTip( + _('Generate thumbnails to show in the main program window') + ) + self.useThumbnailCache = QCheckBox(_('Cache thumbnails')) + self.useThumbnailCache.setToolTip( + _( + "Save thumbnails shown in the main program window in a thumbnail cache unique to " + "Rapid Photo Downloader" + ) + ) + self.fdoThumbnails = QCheckBox(_('Generate system thumbnails')) + self.fdoThumbnails.setToolTip( + _( + 'While downloading, save thumbnails that can be used by desktop file managers ' + 'and other programs' + ) + ) + self.generateThumbnails.stateChanged.connect(self.generateThumbnailsChanged) + self.useThumbnailCache.stateChanged.connect(self.useThumbnailCacheChanged) + self.fdoThumbnails.stateChanged.connect(self.fdoThumbnailsChanged) + self.maxCores = QComboBox() + self.maxCores.setEditable(False) + tip = _('Number of CPU cores used to generate thumbnails.') + self.coresLabel = QLabel(_('CPU cores:')) + self.coresLabel.setToolTip(tip) + self.maxCores.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.maxCores.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.maxCores.setToolTip(tip) + + self.setPerformanceValues() + + self.maxCores.currentIndexChanged.connect(self.maxCoresChanged) + + coresLayout = QHBoxLayout() + coresLayout.addWidget(self.coresLabel) + coresLayout.addWidget(self.maxCores) + # Translators: the * acts as an asterisk to denote a reference to an annotation + # such as '* Takes effect upon program restart' + coresLayout.addWidget(QLabel(_('*'))) + coresLayout.addStretch() + + performanceBoxLayout = QVBoxLayout() + performanceBoxLayout.addWidget(self.generateThumbnails) + performanceBoxLayout.addWidget(self.useThumbnailCache) + performanceBoxLayout.addWidget(self.fdoThumbnails) + performanceBoxLayout.addLayout(coresLayout) + self.performanceBox.setLayout(performanceBoxLayout) + + self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False) + + self.cacheSize = CacheSize() + self.cacheSizeThread = QThread() + self.cacheSizeThread.started.connect(self.cacheSize.start) + self.getCacheSize.connect(self.cacheSize.getCacheSize) + self.cacheSize.size.connect(self.setCacheSize) + self.cacheSize.moveToThread(self.cacheSizeThread) + + QTimer.singleShot(0, self.cacheSizeThread.start) + + self.getCacheSize.emit() + + self.cacheBox = QGroupBox(_('Thumbnail Cache')) + self.thumbnailCacheSize = QLabel() + self.thumbnailCacheSize.setText(_('Calculating...')) + self.thumbnailNumber = QLabel() + self.thumbnailSqlSize = QLabel() + self.thumbnailCacheDaysKeep = QSpinBox() + self.thumbnailCacheDaysKeep.setMinimum(0) + self.thumbnailCacheDaysKeep.setMaximum(360*3) + self.thumbnailCacheDaysKeep.setSuffix(' ' + _('days')) + self.thumbnailCacheDaysKeep.setSpecialValueText(_('forever')) + self.thumbnailCacheDaysKeep.valueChanged.connect(self.thumbnailCacheDaysKeepChanged) + + cacheBoxLayout = QVBoxLayout() + cacheLayout = QGridLayout() + cacheLayout.addWidget(QLabel(_('Cache size:')), 0, 0, 1, 1) + cacheLayout.addWidget(self.thumbnailCacheSize, 0, 1, 1, 1) + cacheLayout.addWidget(QLabel(_('Number of thumbnails:')), 1, 0, 1, 1) + cacheLayout.addWidget(self.thumbnailNumber, 1, 1, 1, 1) + cacheLayout.addWidget(QLabel(_('Database size:')), 2, 0, 1, 1) + cacheLayout.addWidget(self.thumbnailSqlSize, 2, 1, 1, 1) + cacheLayout.addWidget(QLabel(_('Cache unaccessed thumbnails for:')), 3, 0, 1, 1) + cacheDays = QHBoxLayout() + cacheDays.addWidget(self.thumbnailCacheDaysKeep) + cacheDays.addWidget(QLabel(_('*'))) + cacheLayout.addLayout(cacheDays, 3, 1, 1, 1) + cacheBoxLayout.addLayout(cacheLayout) + + cacheButtons = QDialogButtonBox() + self.purgeCache = cacheButtons.addButton(_('Purge Cache...'), QDialogButtonBox.ResetRole) + self.optimizeCache = cacheButtons.addButton( + _('Optimize Cache...'), QDialogButtonBox.ResetRole + ) + self.purgeCache.clicked.connect(self.purgeCacheClicked) + self.optimizeCache.clicked.connect(self.optimizeCacheClicked) + + cacheBoxLayout.addWidget(cacheButtons) + + self.cacheBox.setLayout(cacheBoxLayout) + self.setCacheValues() + + performanceLayout = QVBoxLayout() + performanceLayout.addWidget(self.performanceBox) + performanceLayout.addWidget(self.cacheBox) + performanceLayout.addWidget(QLabel(_('* Takes effect upon program restart'))) + performanceLayout.addStretch() + performanceLayout.setContentsMargins(0, 0, 0, 0) + performanceLayout.setSpacing(18) + + self.performance.setLayout(performanceLayout) + + self.errorBox = QGroupBox(_('Error Handling')) + + self.downloadErrorGroup = QButtonGroup() + self.skipDownload = QRadioButton(_('Skip download')) + self.skipDownload.setToolTip(_("Don't download the file, and issue an error message")) + self.addIdentifier = QRadioButton(_('Add unique identifier')) + self.addIdentifier.setToolTip( + _( + "Add an identifier like _1 or _2 to the end of the filename, immediately before " + "the file's extension" + ) + ) + self.downloadErrorGroup.addButton(self.skipDownload) + self.downloadErrorGroup.addButton(self.addIdentifier) + + self.backupErrorGroup = QButtonGroup() + self.overwriteBackup = QRadioButton(_('Overwrite')) + self.overwriteBackup.setToolTip(_("Overwrite the previously backed up file")) + self.skipBackup = QRadioButton(_('Skip')) + self.skipBackup.setToolTip( + _("Don't overwrite the backup file, and issue an error message") + ) + self.backupErrorGroup.addButton(self.overwriteBackup) + self.backupErrorGroup.addButton(self.skipBackup) + + errorBoxLayout = QVBoxLayout() + lbl = _( + 'When a photo or video of the same name has already been downloaded, choose ' + 'whether to skip downloading the file, or to add a unique identifier:' + ) + self.downloadError = QLabel(lbl) + self.downloadError.setWordWrap(True) + errorBoxLayout.addWidget(self.downloadError) + errorBoxLayout.addWidget(self.skipDownload) + errorBoxLayout.addWidget(self.addIdentifier) + lbl = '<i>' + _( + 'Using sequence numbers to automatically generate unique filenames is ' + 'strongly recommended. Configure file renaming in the Rename panel in the ' + 'main window.' + ) + '</i>' + self.recommended = QLabel(lbl) + self.recommended.setWordWrap(True) + errorBoxLayout.addWidget(self.recommended) + errorBoxLayout.addSpacing(18) + lbl = _( + 'When backing up, choose whether to overwrite a file on the backup device that ' + 'has the same name, or skip backing it up:' + ) + self.backupError = QLabel(lbl) + self.backupError.setWordWrap(True) + errorBoxLayout.addWidget(self.backupError) + errorBoxLayout.addWidget(self.overwriteBackup) + errorBoxLayout.addWidget(self.skipBackup) + self.errorBox.setLayout(errorBoxLayout) + + self.setErrorHandingValues() + self.downloadErrorGroup.buttonClicked.connect(self.downloadErrorGroupClicked) + self.backupErrorGroup.buttonClicked.connect(self.backupErrorGroupClicked) + + self.errorWidget = QWidget() + errorLayout = QVBoxLayout() + self.errorWidget.setLayout(errorLayout) + errorLayout.addWidget(self.errorBox) + errorLayout.addStretch() + errorLayout.setContentsMargins(0, 0, 0, 0) + + self.warningBox = QGroupBox(_('Program Warnings')) + lbl = _('Show a warning when:') + self.warningLabel = QLabel(lbl) + self.warningLabel.setWordWrap(True) + self.warnDownloadingAll = QCheckBox(_('Downloading files currently not displayed')) + tip = _('Warn when about to download files that are not displayed in the main window.') + self.warnDownloadingAll.setToolTip(tip) + self.warnBackupProblem = QCheckBox(_('Backup destinations are missing')) + tip = _("Warn before starting a download if it is not possible to back up files.") + self.warnBackupProblem.setToolTip(tip) + self.warnMissingLibraries = QCheckBox(_('Program libraries are missing or broken')) + tip = _('Warn if a software library used by Rapid Photo Downloader is missing or not ' + 'functioning.') + self.warnMissingLibraries.setToolTip(tip) + self.warnMetadata = QCheckBox(_('Filesystem metadata cannot be set')) + tip = _("Warn if there is an error setting a file's filesystem metadata, " + "such as its modification time.") + self.warnMetadata.setToolTip(tip) + self.warnUnhandledFiles = QCheckBox(_('Encountering unhandled files')) + tip = _('Warn after scanning a device or this computer if there are unrecognized files ' + 'that will not be included in the download.') + self.warnUnhandledFiles.setToolTip(tip) + self.exceptTheseFilesLabel = QLabel( + _('Do not warn about unhandled files with extensions:') + ) + self.exceptTheseFilesLabel.setWordWrap(True) + self.exceptTheseFiles = QNarrowListWidget(minimum_rows=4) + tip = _( + 'File extensions are case insensitive and do not need to include the leading dot.' + ) + self.exceptTheseFiles.setToolTip(tip) + self.addExceptFiles = QPushButton(_('Add')) + tip = _('Add a file extension to the list of unhandled file types to not warn about.') + self.addExceptFiles.setToolTip(tip) + tip = _('Remove a file extension from the list of unhandled file types to not warn about.') + self.removeExceptFiles = QPushButton(_('Remove')) + self.removeExceptFiles.setToolTip(tip) + self.removeAllExceptFiles = QPushButton(_('Remove All')) + tip = _('Clear the list of file extensions of unhandled file types to not warn about.') + self.removeAllExceptFiles.setToolTip(tip) + self.addExceptFiles.clicked.connect(self.addExceptFilesClicked) + self.removeExceptFiles.clicked.connect(self.removeExceptFilesClicked) + self.removeAllExceptFiles.clicked.connect(self.removeAllExceptFilesClicked) + + self.setWarningValues() + self.warnDownloadingAll.stateChanged.connect(self.warnDownloadingAllChanged) + self.warnBackupProblem.stateChanged.connect(self.warnBackupProblemChanged) + self.warnMissingLibraries.stateChanged.connect(self.warnMissingLibrariesChanged) + self.warnMetadata.stateChanged.connect(self.warnMetadataChanged) + self.warnUnhandledFiles.stateChanged.connect(self.warnUnhandledFilesChanged) + + warningBoxLayout = QGridLayout() + warningBoxLayout.addWidget(self.warningLabel, 0, 0, 1, 3) + warningBoxLayout.addWidget(self.warnDownloadingAll, 1, 0, 1, 3) + warningBoxLayout.addWidget(self.warnBackupProblem, 2, 0, 1, 3) + warningBoxLayout.addWidget(self.warnMissingLibraries, 3, 0, 1, 3) + warningBoxLayout.addWidget(self.warnMetadata, 4, 0, 1, 3) + warningBoxLayout.addWidget(self.warnUnhandledFiles, 5, 0, 1, 3) + warningBoxLayout.addWidget(self.exceptTheseFilesLabel, 6, 1, 1, 2) + warningBoxLayout.addWidget(self.exceptTheseFiles, 7, 1, 4, 1) + warningBoxLayout.addWidget(self.addExceptFiles, 7, 2, 1, 1) + warningBoxLayout.addWidget(self.removeExceptFiles, 8, 2, 1, 1) + warningBoxLayout.addWidget(self.removeAllExceptFiles, 9, 2, 1, 1) + warningBoxLayout.setColumnMinimumWidth(0, checkbox_width) + self.warningBox.setLayout(warningBoxLayout) + + self.warnings = QWidget() + warningLayout = QVBoxLayout() + self.warnings.setLayout(warningLayout) + warningLayout.addWidget(self.warningBox) + warningLayout.addStretch() + warningLayout.setContentsMargins(0, 0, 0, 0) + + if consolidation_implemented: + self.consolidationBox = QGroupBox(_('Photo and Video Consolidation')) + + self.consolidateIdentical = QCheckBox( + _('Consolidate files across devices and downloads') + ) + tip = _( + "Analyze the results of device scans looking for duplicate files and matching " + "RAW and JPEG pairs,\ncomparing them across multiple devices and download " + "sessions." + ) + self.consolidateIdentical.setToolTip(tip) + + self.treatRawJpegLabel = QLabel(_('Treat matching RAW and JPEG files as:')) + self.oneRawJpeg = QRadioButton(_('One photo')) + self.twoRawJpeg = QRadioButton(_('Two photos')) + tip = _( + "Display matching pairs of RAW and JPEG photos as one photo, and if marked, " + "download both." + ) + self.oneRawJpeg.setToolTip(tip) + tip = _( + "Display matching pairs of RAW and JPEG photos as two different photos. You can " + "still synchronize their sequence numbers." + ) + self.twoRawJpeg.setToolTip(tip) + + self.treatRawJpegGroup = QButtonGroup() + self.treatRawJpegGroup.addButton(self.oneRawJpeg) + self.treatRawJpegGroup.addButton(self.twoRawJpeg) + + self.markRawJpegLabel = QLabel(_('With matching RAW and JPEG photos:')) + + self.noJpegWhenRaw = QRadioButton(_('Do not mark JPEG for download')) + self.noRawWhenJpeg = QRadioButton(_('Do not mark RAW for download')) + self.markRawJpeg = QRadioButton(_('Mark both for download')) + + self.markRawJpegGroup = QButtonGroup() + for widget in (self.noJpegWhenRaw, self.noRawWhenJpeg, self.markRawJpeg): + self.markRawJpegGroup.addButton(widget) + + tip = _( + "When matching RAW and JPEG photos are found, do not automatically mark the " + "JPEG for\ndownload. You can still mark it for download yourself." + ) + self.noJpegWhenRaw.setToolTip(tip) + tip = _( + "When matching RAW and JPEG photos are found, do not automatically mark the " + "RAW for\ndownload. You can still mark it for download yourself." + ) + self.noRawWhenJpeg.setToolTip(tip) + tip = _( + "When matching RAW and JPEG photos are found, automatically mark both " + "for download." + ) + self.markRawJpeg.setToolTip(tip) + + explanation = _( + 'If you disable file consolidation, choose what to do when a download device is ' + 'inserted while completed downloads are displayed:' + ) + + else: + explanation = _( + 'When a download device is inserted while completed downloads are displayed:' + ) + self.noconsolidationLabel = QLabel(explanation) + self.noconsolidationLabel.setWordWrap(True) + self.noconsolidationLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) + # Unless this next call is made, for some reason the widget is far too high! :-( + self.noconsolidationLabel.setContentsMargins(0, 0, 1, 0) + + self.noConsolidationGroup = QButtonGroup() + self.noConsolidationGroup.buttonClicked.connect(self.noConsolidationGroupClicked) + + self.clearCompletedDownloads = QRadioButton(_('Clear completed downloads')) + self.keepCompletedDownloads = QRadioButton(_('Keep displaying completed downloads')) + self.promptCompletedDownloads = QRadioButton(_('Prompt for what to do')) + self.noConsolidationGroup.addButton(self.clearCompletedDownloads) + self.noConsolidationGroup.addButton(self.keepCompletedDownloads) + self.noConsolidationGroup.addButton(self.promptCompletedDownloads) + tip = _( + "Automatically clear the display of completed downloads whenever a new download " + "device is inserted." + ) + self.clearCompletedDownloads.setToolTip(tip) + tip = _( + "Keep displaying completed downloads whenever a new download device is inserted." + ) + self.keepCompletedDownloads.setToolTip(tip) + tip = _( + "Prompt whether to keep displaying completed downloads or clear them whenever a new " + "download device is inserted." + ) + self.promptCompletedDownloads.setToolTip(tip) + + if consolidation_implemented: + consolidationBoxLayout = QGridLayout() + consolidationBoxLayout.addWidget(self.consolidateIdentical, 0, 0, 1, 3) + + consolidationBoxLayout.addWidget(self.treatRawJpegLabel, 1, 1, 1, 2) + consolidationBoxLayout.addWidget(self.oneRawJpeg, 2, 1, 1, 2) + consolidationBoxLayout.addWidget(self.twoRawJpeg, 3, 1, 1, 2) + + consolidationBoxLayout.addWidget(self.markRawJpegLabel, 4, 2, 1, 1) + consolidationBoxLayout.addWidget(self.noJpegWhenRaw, 5, 2, 1, 1) + consolidationBoxLayout.addWidget(self.noRawWhenJpeg, 6, 2, 1, 1) + consolidationBoxLayout.addWidget(self.markRawJpeg, 7, 2, 1, 1, Qt.AlignTop) + + consolidationBoxLayout.addWidget(self.noconsolidationLabel, 8, 0, 1, 3) + consolidationBoxLayout.addWidget(self.keepCompletedDownloads, 9, 0, 1, 3) + consolidationBoxLayout.addWidget(self.clearCompletedDownloads, 10, 0, 1, 3) + consolidationBoxLayout.addWidget(self.promptCompletedDownloads, 11, 0, 1, 3) + + consolidationBoxLayout.setColumnMinimumWidth(0, checkbox_width) + consolidationBoxLayout.setColumnMinimumWidth(1, checkbox_width) + + consolidationBoxLayout.setRowMinimumHeight(7, checkbox_width * 2) + + self.consolidationBox.setLayout(consolidationBoxLayout) + + self.consolidation = QWidget() + consolidationLayout = QVBoxLayout() + consolidationLayout.addWidget(self.consolidationBox) + consolidationLayout.addStretch() + consolidationLayout.setContentsMargins(0, 0, 0, 0) + consolidationLayout.setSpacing(18) + self.consolidation.setLayout(consolidationLayout) + + self.setCompletedDownloadsValues() + self.setConsolidatedValues() + self.consolidateIdentical.stateChanged.connect(self.consolidateIdenticalChanged) + self.treatRawJpegGroup.buttonClicked.connect(self.treatRawJpegGroupClicked) + self.markRawJpegGroup.buttonClicked.connect(self.markRawJpegGroupClicked) + + self.newVersionBox = QGroupBox(_('Version Check')) + self.checkNewVersion = QCheckBox(_('Check for new version at startup')) + self.checkNewVersion.setToolTip( + _('Check for a new version of the program each time the program starts.') + ) + self.includeDevRelease = QCheckBox(_('Include development releases')) + tip = _( + 'Include alpha, beta and other development releases when checking for a new ' + 'version of the program.\n\nIf you are currently running a development version, ' + 'the check will always occur.' + ) + self.includeDevRelease.setToolTip(tip) + self.setVersionCheckValues() + self.checkNewVersion.stateChanged.connect(self.checkNewVersionChanged) + self.includeDevRelease.stateChanged.connect(self.includeDevReleaseChanged) + + newVersionLayout = QGridLayout() + newVersionLayout.addWidget(self.checkNewVersion, 0, 0, 1, 2) + newVersionLayout.addWidget(self.includeDevRelease, 1, 1, 1, 1) + newVersionLayout.setColumnMinimumWidth(0, checkbox_width) + self.newVersionBox.setLayout(newVersionLayout) + + self.metadataBox = QGroupBox(_('Metadata')) + self.ignoreMdatatimeMtpDng = QCheckBox(_('Ignore DNG date/time metadata on MTP devices')) + tip = _( + "Ignore date/time metadata in DNG files located on MTP devices, and use the " + "file's modification time instead.\n\nUseful for devices like some phones and " + "tablets that create incorrect DNG metadata." + ) + self.ignoreMdatatimeMtpDng.setToolTip(tip) + + self.setMetdataValues() + self.ignoreMdatatimeMtpDng.stateChanged.connect(self.ignoreMdatatimeMtpDngChanged) + + metadataLayout = QVBoxLayout() + metadataLayout.addWidget(self.ignoreMdatatimeMtpDng) + self.metadataBox.setLayout(metadataLayout) + + if not consolidation_implemented: + self.completedDownloadsBox = QGroupBox(_('Completed Downloads')) + completedDownloadsLayout = QVBoxLayout() + completedDownloadsLayout.addWidget(self.noconsolidationLabel) + completedDownloadsLayout.addWidget(self.keepCompletedDownloads) + completedDownloadsLayout.addWidget(self.clearCompletedDownloads) + completedDownloadsLayout.addWidget(self.promptCompletedDownloads) + self.completedDownloadsBox.setLayout(completedDownloadsLayout) + self.setCompletedDownloadsValues() + + self.miscWidget = QWidget() + miscLayout = QVBoxLayout() + miscLayout.addWidget(self.newVersionBox) + miscLayout.addWidget(self.metadataBox) + if not consolidation_implemented: + miscLayout.addWidget(self.completedDownloadsBox) + miscLayout.addStretch() + miscLayout.setContentsMargins(0, 0, 0, 0) + miscLayout.setSpacing(18) + self.miscWidget.setLayout(miscLayout) + + self.panels.addWidget(self.devices) + self.panels.addWidget(self.automation) + self.panels.addWidget(self.performance) + self.panels.addWidget(self.errorWidget) + self.panels.addWidget(self.warnings) + if consolidation_implemented: + self.panels.addWidget(self.consolidation) + self.panels.addWidget(self.miscWidget) + + layout = QVBoxLayout() + self.setLayout(layout) + layout.setSpacing(layout.contentsMargins().left() * 2) + layout.setContentsMargins(18, 18, 18, 18) + + buttons = QDialogButtonBox( + QDialogButtonBox.RestoreDefaults | QDialogButtonBox.Close | QDialogButtonBox.Help + ) + translateButtons(buttons) + self.restoreButton = buttons.button(QDialogButtonBox.RestoreDefaults) # type: QPushButton + self.restoreButton.clicked.connect(self.restoreDefaultsClicked) + self.helpButton = buttons.button(QDialogButtonBox.Help) # type: QPushButton + self.helpButton.clicked.connect(self.helpButtonClicked) + self.helpButton.setToolTip(_('Get help online...')) + self.closeButton = buttons.button(QDialogButtonBox.Close) # type: QPushButton + self.closeButton.clicked.connect(self.close) + + controlsLayout = QHBoxLayout() + controlsLayout.addWidget(self.chooser) + controlsLayout.addWidget(self.panels) + + controlsLayout.setStretch(0, 0) + controlsLayout.setStretch(1, 1) + controlsLayout.setSpacing(layout.contentsMargins().left()) + + layout.addLayout(controlsLayout) + layout.addWidget(buttons) + + self.device_right_side_buttons = ( + self.removeDevice, self.removeAllDevice, self.addPath, self.removePath, + self.removeAllPath + ) + + self.device_list_widgets = (self.knownDevices, self.ignoredPaths) + self.chooser.setCurrentRow(0) + + def _addItems(self, pref_list: str, pref_type: int) -> None: + if self.prefs.list_not_empty(key=pref_list): + for value in self.prefs[pref_list]: + QListWidgetItem(value, self.knownDevices, pref_type) + + def setDeviceWidgetValues(self) -> None: + self.onlyExternal.setChecked(self.prefs.only_external_mounts) + self.scanSpecificFolders.setChecked(self.prefs.scan_specific_folders) + self.setFoldersToScanWidgetValues() + self.knownDevices.clear() + self._addItems('volume_whitelist', KnownDeviceType.volume_whitelist) + self._addItems('volume_blacklist', KnownDeviceType.volume_blacklist) + self._addItems('camera_blacklist', KnownDeviceType.camera_blacklist) + if self.knownDevices.count(): + self.knownDevices.setCurrentRow(0) + self.removeDevice.setEnabled(self.knownDevices.count()) + self.removeAllDevice.setEnabled(self.knownDevices.count()) + self.setIgnorePathWidgetValues() + + def setFoldersToScanWidgetValues(self) -> None: + self.foldersToScan.clear() + if self.prefs.list_not_empty('folders_to_scan'): + self.foldersToScan.addItems(self.prefs.folders_to_scan) + self.foldersToScan.setCurrentRow(0) + self.setFoldersToScanState() + + def setFoldersToScanState(self) -> None: + scan_specific = self.prefs.scan_specific_folders + self.foldersToScanLabel.setEnabled(scan_specific) + self.foldersToScan.setEnabled(scan_specific) + self.addFolderToScan.setEnabled(scan_specific) + self.removeFolderToScan.setEnabled(scan_specific and self.foldersToScan.count() > 1) + + def setIgnorePathWidgetValues(self) -> None: + self.ignoredPaths.clear() + if self.prefs.list_not_empty('ignored_paths'): + self.ignoredPaths.addItems(self.prefs.ignored_paths) + self.ignoredPaths.setCurrentRow(0) + self.removePath.setEnabled(self.ignoredPaths.count()) + self.removeAllPath.setEnabled(self.ignoredPaths.count()) + self.ignoredPathsRe.setChecked(self.prefs.use_re_ignored_paths) + + def setAutomationWidgetValues(self) -> None: + self.autoDownloadStartup.setChecked(self.prefs.auto_download_at_startup) + self.autoDownloadInsertion.setChecked(self.prefs.auto_download_upon_device_insertion) + self.autoEject.setChecked(self.prefs.auto_unmount) + self.autoExit.setChecked(self.prefs.auto_exit) + self.setAutoExitErrorState() + + def setAutoExitErrorState(self) -> None: + if self.prefs.auto_exit: + self.autoExitError.setChecked(self.prefs.auto_exit_force) + self.autoExitError.setEnabled(True) + else: + self.autoExitError.setChecked(False) + self.autoExitError.setEnabled(False) + + def setPerformanceValues(self, check_boxes_only: bool=False) -> None: + self.generateThumbnails.setChecked(self.prefs.generate_thumbnails) + self.useThumbnailCache.setChecked(self.prefs.use_thumbnail_cache and + self.prefs.generate_thumbnails) + self.fdoThumbnails.setChecked(self.prefs.save_fdo_thumbnails and + self.prefs.generate_thumbnails) + + if not check_boxes_only: + available = available_cpu_count(physical_only=True) + self.maxCores.addItems(str(i + 1) for i in range(0, available)) + self.maxCores.setCurrentText(str(self.prefs.max_cpu_cores)) + + def setPerfomanceEnabled(self) -> None: + enable = self.prefs.generate_thumbnails + self.useThumbnailCache.setEnabled(enable) + self.fdoThumbnails.setEnabled(enable) + self.maxCores.setEnabled(enable) + self.coresLabel.setEnabled(enable) + + def setCacheValues(self) -> None: + self.thumbnailNumber.setText(thousands(self.thumbnail_cache.no_thumbnails())) + self.thumbnailSqlSize.setText(format_size_for_user(self.thumbnail_cache.db_size())) + self.thumbnailCacheDaysKeep.setValue(self.prefs.keep_thumbnails_days) + + @pyqtSlot('PyQt_PyObject') + def setCacheSize(self, size: int) -> None: + self.thumbnailCacheSize.setText(format_size_for_user(size)) + + def setErrorHandingValues(self) -> None: + if self.prefs.conflict_resolution == int(ConflictResolution.skip): + self.skipDownload.setChecked(True) + else: + self.addIdentifier.setChecked(True) + if self.prefs.backup_duplicate_overwrite: + self.overwriteBackup.setChecked(True) + else: + self.skipBackup.setChecked(True) + + def setWarningValues(self) -> None: + self.warnDownloadingAll.setChecked(self.prefs.warn_downloading_all) + if self.prefs.backup_files: + self.warnBackupProblem.setChecked(self.prefs.warn_backup_problem) + else: + self.warnBackupProblem.setChecked(False) + self.warnMissingLibraries.setChecked(self.prefs.warn_broken_or_missing_libraries) + self.warnMetadata.setChecked(self.prefs.warn_fs_metadata_error) + self.warnUnhandledFiles.setChecked(self.prefs.warn_unhandled_files) + self.setAddExceptFilesValues() + + self.setBackupWarningEnabled() + self.setUnhandledWarningEnabled() + + def setAddExceptFilesValues(self) -> None: + self.exceptTheseFiles.clear() + if self.prefs.list_not_empty('ignore_unhandled_file_exts'): + self.exceptTheseFiles.addItems(self.prefs.ignore_unhandled_file_exts) + self.exceptTheseFiles.setCurrentRow(0) + + def setBackupWarningEnabled(self) -> None: + self.warnBackupProblem.setEnabled(self.prefs.backup_files) + + def setUnhandledWarningEnabled(self) -> None: + enabled = self.prefs.warn_unhandled_files + for widget in (self.exceptTheseFilesLabel, self.exceptTheseFiles, self.addExceptFiles): + widget.setEnabled(enabled) + count = bool(self.exceptTheseFiles.count()) + for widget in (self.removeExceptFiles, self.removeAllExceptFiles): + widget.setEnabled(enabled and count) + + def setConsolidatedValues(self) -> None: + enabled = self.prefs.consolidate_identical + self.consolidateIdentical.setChecked(enabled) + + self.setTreatRawJpeg() + self.setMarkRawJpeg() + + if enabled: + # Must turn off the exclusive button group feature, or else + # it's impossible to set all the radio buttons to False + self.noConsolidationGroup.setExclusive(False) + for widget in ( + self.clearCompletedDownloads, + self.keepCompletedDownloads, self.promptCompletedDownloads): + widget.setChecked(False) + # Now turn it back on again + self.noConsolidationGroup.setExclusive(True) + else: + self.setCompletedDownloadsValues() + + self.setConsolidatedEnabled() + + def setTreatRawJpeg(self) -> None: + if self.prefs.consolidate_identical: + if self.prefs.treat_raw_jpeg == int(TreatRawJpeg.one_photo): + self.oneRawJpeg.setChecked(True) + else: + self.twoRawJpeg.setChecked(True) + else: + # Must turn off the exclusive button group feature, or else + # it's impossible to set all the radio buttons to False + self.treatRawJpegGroup.setExclusive(False) + self.oneRawJpeg.setChecked(False) + self.twoRawJpeg.setChecked(False) + # Now turn it back on again + self.treatRawJpegGroup.setExclusive(True) + + def setMarkRawJpeg(self) -> None: + if self.prefs.consolidate_identical and self.twoRawJpeg.isChecked(): + v = self.prefs.mark_raw_jpeg + if v == int(MarkRawJpeg.no_jpeg): + self.noJpegWhenRaw.setChecked(True) + elif v == int(MarkRawJpeg.no_raw): + self.noRawWhenJpeg.setChecked(True) + else: + self.markRawJpeg.setChecked(True) + else: + # Must turn off the exclusive button group feature, or else + # it's impossible to set all the radio buttons to False + self.markRawJpegGroup.setExclusive(False) + for widget in (self.noJpegWhenRaw, self.noRawWhenJpeg, self.markRawJpeg): + widget.setChecked(False) + # Now turn it back on again + self.markRawJpegGroup.setExclusive(True) + + def setConsolidatedEnabled(self) -> None: + enabled = self.prefs.consolidate_identical + + for widget in self.treatRawJpegGroup.buttons(): + widget.setEnabled(enabled) + self.treatRawJpegLabel.setEnabled(enabled) + + self.setMarkRawJpegEnabled() + + for widget in ( + self.noconsolidationLabel, self.clearCompletedDownloads, + self.keepCompletedDownloads, self.promptCompletedDownloads): + widget.setEnabled(not enabled) + + def setMarkRawJpegEnabled(self) -> None: + mark_enabled = self.prefs.consolidate_identical and self.twoRawJpeg.isChecked() + for widget in self.markRawJpegGroup.buttons(): + widget.setEnabled(mark_enabled) + self.markRawJpegLabel.setEnabled(mark_enabled) + + def setVersionCheckValues(self) -> None: + self.checkNewVersion.setChecked(self.prefs.check_for_new_versions) + self.includeDevRelease.setChecked( + self.prefs.include_development_release or self.is_prerelease + ) + self.setVersionCheckEnabled() + + def setVersionCheckEnabled(self) -> None: + self.includeDevRelease.setEnabled( + not(self.is_prerelease or not self.prefs.check_for_new_versions) + ) + + def setMetdataValues(self) -> None: + self.ignoreMdatatimeMtpDng.setChecked(self.prefs.ignore_mdatatime_for_mtp_dng) + + def setCompletedDownloadsValues(self) -> None: + s = self.prefs.completed_downloads + if s == int(CompletedDownloads.keep): + self.keepCompletedDownloads.setChecked(True) + elif s == int(CompletedDownloads.clear): + self.clearCompletedDownloads.setChecked(True) + else: + self.promptCompletedDownloads.setChecked(True) + + @pyqtSlot(int) + def onlyExternalChanged(self, state: int) -> None: + self.prefs.only_external_mounts = state == Qt.Checked + if self.rapidApp is not None: + self.rapidApp.search_for_devices_again = True + + @pyqtSlot(int) + def noDcimChanged(self, state: int) -> None: + self.prefs.scan_specific_folders = state == Qt.Checked + self.setFoldersToScanState() + if self.rapidApp is not None: + self.rapidApp.scan_non_cameras_again = True + + @pyqtSlot(int) + def ignoredPathsReChanged(self, state: int) -> None: + self.prefs.use_re_ignored_paths = state == Qt.Checked + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + def _equalizeWidgetWidth(self, widget_list) -> None: + max_width = max(widget.width() for widget in widget_list) + for widget in widget_list: + widget.setFixedWidth(max_width) + + def showEvent(self, e: QShowEvent): + self.chooser.minimum_width = self.restoreButton.width() + self._equalizeWidgetWidth(self.device_right_side_buttons) + self._equalizeWidgetWidth(self.device_list_widgets) + super().showEvent(e) + + @pyqtSlot(int) + def rowChanged(self, row: int) -> None: + self.panels.setCurrentIndex(row) + # Translators: substituted value is a description for the set of preferences + # shown in the preference dialog window, e.g. Devices, Automation, etc. + # This string is shown in a tooltip for the "Restore Defaults" button + self.restoreButton.setToolTip(_('Restores default %s preference values') % + self.chooser_items[row]) + + @pyqtSlot() + def removeDeviceClicked(self) -> None: + row = self.knownDevices.currentRow() + item = self.knownDevices.takeItem(row) # type: QListWidgetItem + known_device_type = item.type() + if known_device_type == KnownDeviceType.volume_whitelist: + self.prefs.del_list_value('volume_whitelist', item.text()) + elif known_device_type == KnownDeviceType.volume_blacklist: + self.prefs.del_list_value('volume_blacklist', item.text()) + else: + assert known_device_type == KnownDeviceType.camera_blacklist + self.prefs.del_list_value('camera_blacklist', item.text()) + + self.removeDevice.setEnabled(self.knownDevices.count()) + self.removeAllDevice.setEnabled(self.knownDevices.count()) + + if self.rapidApp is not None: + self.rapidApp.search_for_devices_again = True + + @pyqtSlot() + def removeAllDeviceClicked(self) -> None: + self.knownDevices.clear() + self.prefs.volume_whitelist = [''] + self.prefs.volume_blacklist = [''] + self.prefs.camera_blacklist = [''] + self.removeDevice.setEnabled(False) + self.removeAllDevice.setEnabled(False) + + if self.rapidApp is not None: + self.rapidApp.search_for_devices_again = True + + @pyqtSlot() + def removeFolderToScanClicked(self) -> None: + row = self.foldersToScan.currentRow() + if row >= 0 and self.foldersToScan.count() > 1: + item = self.foldersToScan.takeItem(row) + self.prefs.del_list_value('folders_to_scan', item.text()) + self.removeFolderToScan.setEnabled(self.foldersToScan.count() > 1) + + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + @pyqtSlot() + def addFolderToScanClicked(self) -> None: + dlg = FoldersToScanDialog(prefs=self.prefs, parent=self) + if dlg.exec(): + self.setFoldersToScanWidgetValues() + + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + @pyqtSlot() + def removePathClicked(self) -> None: + row = self.ignoredPaths.currentRow() + if row >= 0: + item = self.ignoredPaths.takeItem(row) + self.prefs.del_list_value('ignored_paths', item.text()) + self.removePath.setEnabled(self.ignoredPaths.count()) + self.removeAllPath.setEnabled(self.ignoredPaths.count()) + + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + @pyqtSlot() + def removeAllPathClicked(self) -> None: + self.ignoredPaths.clear() + self.prefs.ignored_paths = [''] + self.removePath.setEnabled(False) + self.removeAllPath.setEnabled(False) + + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + @pyqtSlot() + def addPathClicked(self) -> None: + dlg = IgnorePathDialog(prefs=self.prefs, parent=self) + if dlg.exec(): + self.setIgnorePathWidgetValues() + + if self.rapidApp is not None: + self.rapidApp.scan_all_again = True + + @pyqtSlot() + def ignorePathsReLabelClicked(self) -> None: + self.ignoredPathsRe.click() + + @pyqtSlot(int) + def autoDownloadStartupChanged(self, state: int) -> None: + self.prefs.auto_download_at_startup = state == Qt.Checked + + @pyqtSlot(int) + def autoDownloadInsertionChanged(self, state: int) -> None: + self.prefs.auto_download_upon_device_insertion = state == Qt.Checked + + @pyqtSlot(int) + def autoEjectChanged(self, state: int) -> None: + self.prefs.auto_unmount = state == Qt.Checked + + @pyqtSlot(int) + def autoExitChanged(self, state: int) -> None: + auto_exit = state == Qt.Checked + self.prefs.auto_exit = auto_exit + self.setAutoExitErrorState() + if not auto_exit: + self.prefs.auto_exit_force = False + + @pyqtSlot(int) + def autoExitErrorChanged(self, state: int) -> None: + self.prefs.auto_exit_force = state == Qt.Checked + + @pyqtSlot(int) + def generateThumbnailsChanged(self, state: int) -> None: + self.prefs.generate_thumbnails = state == Qt.Checked + self.setPerformanceValues(check_boxes_only=True) + self.setPerfomanceEnabled() + + @pyqtSlot(int) + def useThumbnailCacheChanged(self, state: int) -> None: + if self.prefs.generate_thumbnails: + self.prefs.use_thumbnail_cache = state == Qt.Checked + + @pyqtSlot(int) + def fdoThumbnailsChanged(self, state: int) -> None: + if self.prefs.generate_thumbnails: + self.prefs.save_fdo_thumbnails = state == Qt.Checked + + @pyqtSlot(int) + def thumbnailCacheDaysKeepChanged(self, value: int) -> None: + self.prefs.keep_thumbnails_days = value + + @pyqtSlot(int) + def maxCoresChanged(self, index: int) -> None: + if index >= 0: + self.prefs.max_cpu_cores = int(self.maxCores.currentText()) + + @pyqtSlot() + def purgeCacheClicked(self) -> None: + message = _( + 'Do you want to purge the thumbnail cache? The cache will be purged when the ' + 'program is next started.' + ) + msgBox = QMessageBox(parent=self) + msgBox.setWindowTitle(_('Purge Thumbnail Cache')) + msgBox.setText(message) + msgBox.setIcon(QMessageBox.Question) + msgBox.setStandardButtons(QMessageBox.Yes|QMessageBox.No) + if msgBox.exec_() == QMessageBox.Yes: + self.prefs.purge_thumbnails = True + self.prefs.optimize_thumbnail_db = False + else: + self.prefs.purge_thumbnails = False + + @pyqtSlot() + def optimizeCacheClicked(self) -> None: + message = _( + 'Do you want to optimize the thumbnail cache? The cache will be optimized when ' + 'the program is next started.' + ) + msgBox = QMessageBox(parent=self) + msgBox.setWindowTitle(_('Optimize Thumbnail Cache')) + msgBox.setText(message) + msgBox.setIcon(QMessageBox.Question) + msgBox.setStandardButtons(QMessageBox.Yes|QMessageBox.No) + if msgBox.exec_() == QMessageBox.Yes: + self.prefs.purge_thumbnails = False + self.prefs.optimize_thumbnail_db = True + else: + self.prefs.optimize_thumbnail_db = False + + @pyqtSlot(QAbstractButton) + def downloadErrorGroupClicked(self, button: QRadioButton) -> None: + if self.downloadErrorGroup.checkedButton() == self.skipDownload: + self.prefs.conflict_resolution = int(ConflictResolution.skip) + else: + self.prefs.conflict_resolution = int(ConflictResolution.add_identifier) + + @pyqtSlot(QAbstractButton) + def backupErrorGroupClicked(self, button: QRadioButton) -> None: + self.prefs.backup_duplicate_overwrite = self.backupErrorGroup.checkedButton() == \ + self.overwriteBackup + + @pyqtSlot(int) + def warnDownloadingAllChanged(self, state: int) -> None: + self.prefs.warn_downloading_all = state == Qt.Checked + + @pyqtSlot(int) + def warnBackupProblemChanged(self, state: int) -> None: + self.prefs.warn_backup_problem = state == Qt.Checked + + @pyqtSlot(int) + def warnMissingLibrariesChanged(self, state: int) -> None: + self.prefs.warn_broken_or_missing_libraries = state == Qt.Checked + + @pyqtSlot(int) + def warnMetadataChanged(self, state: int) -> None: + self.prefs.warn_fs_metadata_error = state == Qt.Checked + + @pyqtSlot(int) + def warnUnhandledFilesChanged(self, state: int) -> None: + self.prefs.warn_unhandled_files = state == Qt.Checked + self.setUnhandledWarningEnabled() + + @pyqtSlot() + def addExceptFilesClicked(self) -> None: + dlg = ExceptFileExtDialog(prefs=self.prefs, parent=self) + if dlg.exec(): + self.setAddExceptFilesValues() + + @pyqtSlot() + def removeExceptFilesClicked(self) -> None: + row = self.exceptTheseFiles.currentRow() + if row >= 0: + item = self.exceptTheseFiles.takeItem(row) + self.prefs.del_list_value('ignore_unhandled_file_exts', item.text()) + self.removeExceptFiles.setEnabled(self.exceptTheseFiles.count()) + self.removeAllExceptFiles.setEnabled(self.exceptTheseFiles.count()) + + @pyqtSlot() + def removeAllExceptFilesClicked(self) -> None: + self.exceptTheseFiles.clear() + self.prefs.ignore_unhandled_file_exts = [''] + self.removeExceptFiles.setEnabled(False) + self.removeAllExceptFiles.setEnabled(False) + + @pyqtSlot(int) + def consolidateIdenticalChanged(self, state: int) -> None: + self.prefs.consolidate_identical = state == Qt.Checked + self.setConsolidatedValues() + self.setConsolidatedEnabled() + + @pyqtSlot(QAbstractButton) + def treatRawJpegGroupClicked(self, button: QRadioButton) -> None: + if button == self.oneRawJpeg: + self.prefs.treat_raw_jpeg = int(TreatRawJpeg.one_photo) + else: + self.prefs.treat_raw_jpeg = int(TreatRawJpeg.two_photos) + self.setMarkRawJpeg() + self.setMarkRawJpegEnabled() + + @pyqtSlot(QAbstractButton) + def markRawJpegGroupClicked(self, button: QRadioButton) -> None: + if button == self.noJpegWhenRaw: + self.prefs.mark_raw_jpeg = int(MarkRawJpeg.no_jpeg) + elif button == self.noRawWhenJpeg: + self.prefs.mark_raw_jpeg = int(MarkRawJpeg.no_raw) + else: + self.prefs.mark_raw_jpeg = int(MarkRawJpeg.both) + + @pyqtSlot(int) + def noJpegWhenRawChanged(self, state: int) -> None: + self.prefs.do_not_mark_jpeg = state == Qt.Checked + + @pyqtSlot(int) + def noRawWhenJpegChanged(self, state: int) -> None: + self.prefs.do_not_mark_raw = state == Qt.Checked + + @pyqtSlot(int) + def checkNewVersionChanged(self, state: int) -> None: + do_check = state == Qt.Checked + self.prefs.check_for_new_versions = do_check + self.setVersionCheckEnabled() + + @pyqtSlot(int) + def includeDevReleaseChanged(self, state: int) -> None: + self.prefs.include_development_release = state == Qt.Checked + + @pyqtSlot(int) + def ignoreMdatatimeMtpDngChanged(self, state: int) -> None: + self.prefs.ignore_mdatatime_for_mtp_dng = state == Qt.Checked + + @pyqtSlot(QAbstractButton) + def noConsolidationGroupClicked(self, button: QRadioButton) -> None: + if button == self.keepCompletedDownloads: + self.prefs.completed_downloads = int(CompletedDownloads.keep) + elif button == self.clearCompletedDownloads: + self.prefs.completed_downloads = int(CompletedDownloads.clear) + else: + self.prefs.completed_downloads = int(CompletedDownloads.prompt) + + @pyqtSlot() + def restoreDefaultsClicked(self) -> None: + row = self.chooser.currentRow() + if row == 0: + for value in ('only_external_mounts', 'scan_specific_folders', 'folders_to_scan', + 'ignored_paths', 'use_re_ignored_paths'): + self.prefs.restore(value) + self.removeAllDeviceClicked() + self.setDeviceWidgetValues() + elif row == 1: + for value in ('auto_download_at_startup', 'auto_download_upon_device_insertion', + 'auto_unmount', 'auto_exit', 'auto_exit_force'): + self.prefs.restore(value) + self.setAutomationWidgetValues() + elif row == 2: + for value in ('generate_thumbnails', 'use_thumbnail_cache', 'save_fdo_thumbnails', + 'max_cpu_cores', 'keep_thumbnails_days'): + self.prefs.restore(value) + self.setPerformanceValues(check_boxes_only=True) + self.maxCores.setCurrentText(str(self.prefs.max_cpu_cores)) + self.setPerfomanceEnabled() + self.thumbnailCacheDaysKeep.setValue(self.prefs.keep_thumbnails_days) + elif row == 3: + for value in ('conflict_resolution', 'backup_duplicate_overwrite'): + self.prefs.restore(value) + self.setErrorHandingValues() + elif row == 4: + for value in ( + 'warn_downloading_all', 'warn_backup_problem', + 'warn_broken_or_missing_libraries', 'warn_fs_metadata_error', + 'warn_unhandled_files', 'ignore_unhandled_file_exts'): + self.prefs.restore(value) + self.setWarningValues() + self.setVersionCheckValues() + elif row == 5 and consolidation_implemented: + for value in ( + 'completed_downloads', 'consolidate_identical', 'one_raw_jpeg', + 'do_not_mark_jpeg', 'do_not_mark_raw'): + self.prefs.restore(value) + self.setConsolidatedValues() + elif (row == 6 and consolidation_implemented) or (row == 5 and not + consolidation_implemented): + for value in ('check_for_new_versions', 'include_development_release', + 'ignore_mdatatime_for_mtp_dng'): + self.prefs.restore(value) + if not consolidation_implemented: + self.prefs.restore('completed_downloads') + self.setVersionCheckValues() + self.setMetdataValues() + if not consolidation_implemented: + self.setCompletedDownloadsValues() + + @pyqtSlot() + def helpButtonClicked(self) -> None: + row = self.chooser.currentRow() + if row == 0: + location = '#devicepreferences' + elif row == 1: + location = '#automationpreferences' + elif row == 2: + location = '#thumbnailpreferences' + elif row == 3: + location = '#errorhandlingpreferences' + elif row == 4: + location = '#warningpreferences' + elif row == 5: + if consolidation_implemented: + location = '#consolidationpreferences' + else: + location = '#miscellaneousnpreferences' + elif row == 6: + location = '#miscellaneousnpreferences' + else: + location = '' + + webbrowser.open_new_tab("http://www.damonlynch.net/rapid/documentation/{}".format(location)) + + def closeEvent(self, event: QCloseEvent) -> None: + self.cacheSizeThread.quit() + self.cacheSizeThread.wait(1000) + event.accept() + + +class PreferenceAddDialog(QDialog): + """ + Base class for adding value to pref list + """ + def __init__(self, prefs: Preferences, + title: str, + instruction: str, + label: str, + pref_value: str, + parent=None) -> None: + super().__init__(parent=parent) + + self.prefs = prefs + self.pref_value = pref_value + + self.setWindowTitle(title) + + self.instructionLabel = QLabel(instruction) + self.instructionLabel.setWordWrap(False) + layout = QVBoxLayout() + self.setLayout(layout) + + self.valueEdit = QLineEdit() + formLayout = QFormLayout() + formLayout.addRow(label, self.valueEdit) + + buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + translateButtons(buttons) + buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) + + layout.addWidget(self.instructionLabel) + layout.addLayout(formLayout) + layout.addWidget(buttons) + + def accept(self): + value = self.valueEdit.text() + if value: + self.prefs.add_list_value(self.pref_value, value) + super().accept() + + +class FoldersToScanDialog(PreferenceAddDialog): + """ + Dialog prompting for a folder on devices to scan for photos and videos + """ + def __init__(self, prefs: Preferences, parent=None) -> None: + super().__init__( + prefs=prefs, + title=_('Enter a Folder to Scan'), + instruction=_('Specify a folder that will be scanned for photos and videos'), + label=_('Folder:'), + pref_value='folders_to_scan', + parent=parent + ) + + +class IgnorePathDialog(PreferenceAddDialog): + """ + Dialog prompting for a path to ignore when scanning devices + """ + + def __init__(self, prefs: Preferences, parent=None) -> None: + super().__init__( + prefs=prefs, + title=_('Enter a Path to Ignore'), + instruction=_('Specify a path that will never be scanned for photos or videos'), + label=_('Path:'), + pref_value='ignored_paths', + parent=parent + ) + + +class ExceptFileExtDialog(PreferenceAddDialog): + """ + Dialog prompting for file extensions never to warn about + """ + + def __init__(self, prefs: Preferences, parent=None) -> None: + super().__init__( + prefs=prefs, + title=_('Enter a File Extension'), + instruction=_('Specify a file extension (without the leading dot)'), + label=_('Extension:'), + pref_value='ignore_unhandled_file_exts', + parent=parent + ) + + def exts(self, exts: List[str]) -> str: + return make_internationalized_list([ext.upper() for ext in exts]) + + def accept(self): + value = self.valueEdit.text() + if value: + while value.startswith('.'): + value = value[1:] + value = value.upper() + if value.lower() in ALL_KNOWN_EXTENSIONS: + title = _('Invalid File Extension') + message = _("The file extension <b>%s</b> is recognized by Rapid Photo Downloader, " + "so it makes no sense to warn about its presence.") % value + details = _('Recognized file types:\n\n' + 'Photos:\n%(photos)s\n\nVideos:\n%(videos)s\n\n' + 'Audio:\n%(audio)s\n\nOther:\n%(other)s') % dict( + photos=self.exts(PHOTO_EXTENSIONS), + videos=self.exts(VIDEO_EXTENSIONS + VIDEO_THUMBNAIL_EXTENSIONS), + audio=self.exts(AUDIO_EXTENSIONS), + other=self.exts(['xmp']) + ) + msgbox = QMessageBox(parent=self) + msgbox.setWindowTitle(title) + msgbox.setText(message) + msgbox.setDetailedText(details) + msgbox.setIcon(QMessageBox.Information) + msgbox.exec() + self.valueEdit.setText(value) + self.valueEdit.selectAll() + return + else: + self.prefs.add_list_value(self.pref_value, value) + QDialog.accept(self) + +class CacheSize(QObject): + size = pyqtSignal('PyQt_PyObject') # don't convert python int to C++ int + + @pyqtSlot() + def start(self) -> None: + self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False) + + @pyqtSlot() + def getCacheSize(self) -> None: + self.size.emit(self.thumbnail_cache.cache_size()) + + +if __name__ == '__main__': + + # Application development test code: + + app = QApplication([]) + + app.setOrganizationName("Rapid Photo Downloader") + app.setOrganizationDomain("damonlynch.net") + app.setApplicationName("Rapid Photo Downloader") + + prefs = Preferences() + + prefDialog = PreferencesDialog(prefs) + prefDialog.show() + app.exec_() |