summaryrefslogtreecommitdiff
path: root/raphodo/nameeditor.py
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo/nameeditor.py')
-rwxr-xr-xraphodo/nameeditor.py1420
1 files changed, 1420 insertions, 0 deletions
diff --git a/raphodo/nameeditor.py b/raphodo/nameeditor.py
new file mode 100755
index 0000000..139c878
--- /dev/null
+++ b/raphodo/nameeditor.py
@@ -0,0 +1,1420 @@
+#!/usr/bin/env python3
+# Copyright (C) 2016-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 for editing download subfolder structure and file renaming
+"""
+
+__author__ = 'Damon Lynch'
+__copyright__ = "Copyright 2016-2017, Damon Lynch"
+
+from typing import Dict, Optional, List, Union, Tuple, Sequence
+import webbrowser
+import datetime
+import copy
+import logging
+
+from gettext import gettext as _
+
+from PyQt5.QtWidgets import (QTextEdit, QApplication, QComboBox, QPushButton, QLabel, QDialog,
+ QDialogButtonBox, QVBoxLayout, QFormLayout, QGridLayout, QGroupBox, QScrollArea, QWidget,
+ QFrame, QStyle, QSizePolicy, QLineEdit, QMessageBox)
+from PyQt5.QtGui import (QTextCharFormat, QFont, QTextCursor, QMouseEvent, QSyntaxHighlighter,
+ QTextDocument, QBrush, QColor, QFontMetrics, QKeyEvent, QResizeEvent,
+ QStandardItem, QPixmap, QWheelEvent)
+from PyQt5.QtCore import (Qt, pyqtSlot, QSignalMapper, QSize, pyqtSignal)
+
+from sortedcontainers import SortedList
+
+from raphodo.generatenameconfig import *
+import raphodo.generatename as gn
+from raphodo.constants import (CustomColors, PrefPosition, NameGenerationType, PresetPrefType,
+ PresetClass)
+from raphodo.rpdfile import SamplePhoto, SampleVideo, RPDFile, Photo, Video, FileType
+from raphodo.preferences import DownloadsTodayTracker, Preferences, match_pref_list
+import raphodo.exiftool as exiftool
+from raphodo.utilities import remove_last_char_from_list_str
+from raphodo.messagewidget import MessageWidget
+import raphodo.qrc_resources
+
+
+class PrefEditor(QTextEdit):
+ """
+ File renaming and subfolder generation preference editor
+ """
+
+ prefListGenerated = pyqtSignal()
+
+ def __init__(self, subfolder: bool, parent=None) -> None:
+ """
+ :param subfolder: if True, the editor is for editing subfolder generation
+ """
+
+ super().__init__(parent)
+ self.subfolder = subfolder
+
+ self.user_pref_list = [] # type: List[str]
+ self.user_pref_colors = [] # type: List[str]
+
+ self.heightMin = 0
+ self.heightMax = 65000
+ # Start out with about 4 lines in height:
+ self.setMinimumHeight(QFontMetrics(self.font()).lineSpacing() * 5)
+ self.document().documentLayout().documentSizeChanged.connect(self.wrapHeightToContents)
+
+ def wrapHeightToContents(self) -> None:
+ """
+ Adjust the text area size to show contents without vertical scrollbar
+
+ Derived from:
+ http://stackoverflow.com/questions/11851020/a-qwidget-like-qtextedit-that-wraps-its-height-
+ automatically-to-its-contents/11858803#11858803
+ """
+
+ docHeight = self.document().size().height() + 5
+ if self.heightMin <= docHeight <= self.heightMax and docHeight > self.minimumHeight():
+ self.setMinimumHeight(docHeight)
+
+ def mousePressEvent(self, event: QMouseEvent) -> None:
+ """
+ Automatically select a pref value if it was clicked in
+ :param event: the mouse event
+ """
+
+ super().mousePressEvent(event)
+ if event.button() == Qt.LeftButton:
+ position = self.textCursor().position()
+ pref_pos, start, end, left_start, left_end = self.locatePrefValue(position)
+
+ if pref_pos == PrefPosition.on_left:
+ start = left_start
+ end = left_end
+ if pref_pos != PrefPosition.not_here:
+ cursor = self.textCursor()
+ cursor.setPosition(start)
+ cursor.setPosition(end + 1, QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
+
+ def keyPressEvent(self, event: QKeyEvent) -> None:
+ """
+ Automatically select pref values when navigating through the document.
+
+ Suppress the return / enter key.
+
+ :param event: the key press event
+ """
+
+ key = event.key()
+ if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
+ return
+
+ cursor = self.textCursor() # type: QTextCursor
+
+ if cursor.hasSelection() and key in (Qt.Key_Left, Qt.Key_Right):
+ # Pass the key press on and let the selection deselect
+ pass
+ elif key in (Qt.Key_Left, Qt.Key_Right, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp,
+ Qt.Key_PageDown, Qt.Key_Up, Qt.Key_Down):
+ # Navigation key was pressed
+
+ # Was ctrl key pressed too?
+ ctrl_key = event.modifiers() & Qt.ControlModifier
+
+ selection_start = selection_end = -1
+
+ # This event is called before the cursor is moved, so
+ # move the cursor as if it would be moved
+ if key == Qt.Key_Right and not cursor.atEnd():
+ if ctrl_key:
+ cursor.movePosition(QTextCursor.WordRight)
+ else:
+ cursor.movePosition(QTextCursor.Right)
+ elif key == Qt.Key_Left and not cursor.atStart():
+ if ctrl_key:
+ cursor.movePosition(QTextCursor.WordLeft)
+ else:
+ cursor.movePosition(QTextCursor.Left)
+ elif key == Qt.Key_Up:
+ cursor.movePosition(QTextCursor.Up)
+ elif key == Qt.Key_Down:
+ cursor.movePosition(QTextCursor.Down)
+ elif key in (Qt.Key_Home, Qt.Key_PageUp):
+ if ctrl_key or key == Qt.Key_PageUp:
+ cursor.movePosition(QTextCursor.StartOfBlock)
+ else:
+ cursor.movePosition(QTextCursor.StartOfLine)
+ elif key in (Qt.Key_End, Qt.Key_PageDown):
+ if ctrl_key or key == Qt.Key_PageDown:
+ cursor.movePosition(QTextCursor.EndOfBlock)
+ else:
+ cursor.movePosition(QTextCursor.EndOfLine)
+
+ # Get position of where the cursor would move to
+ position = cursor.position()
+
+ # Determine if there is a pref value to the left or at that position
+ pref_pos, start, end, left_start, left_end = self.locatePrefValue(position)
+ if pref_pos == PrefPosition.on_left:
+ selection_start = left_start
+ selection_end = left_end + 1
+ elif pref_pos == PrefPosition.at:
+ selection_start = end + 1
+ selection_end = start
+ elif pref_pos == PrefPosition.positioned_in:
+ if key == Qt.Key_Left or key == Qt.Key_Home:
+ # because moving left, position the cursor on the left
+ selection_start = end + 1
+ selection_end = start
+ else:
+ # because moving right, position the cursor on the right
+ selection_start = start
+ selection_end = end + 1
+
+ if selection_end >= 0 and selection_start >= 0:
+ cursor.setPosition(selection_start)
+ cursor.setPosition(selection_end, QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
+ return
+
+ super().keyPressEvent(event)
+
+ def locatePrefValue(self, position: int) -> Tuple[PrefPosition, int, int, int, int]:
+ """
+ Determine where pref values are relative to the position passed.
+
+ :param position: some position in text, e.g. cursor position
+ :return: enum indicating where prefs are found and their start and end
+ positions. Return positions are -1 if not found.
+ """
+
+ start = end = -1
+ left_start = left_end = -1
+ pref_position = PrefPosition.not_here
+ b = self.highlighter.boundaries
+ if not(len(b)):
+ return (pref_position, start, end, left_start, left_end)
+
+ index = b.bisect_left((position, 0))
+ # Special cases
+ if index == 0:
+ # At or to the left of the first pref value
+ if b[0][0] == position:
+ pref_position = PrefPosition.at
+ start, end = b[0]
+ elif index == len(b):
+ # To the right of or in the last pref value
+ if position <= b[-1][1]:
+ start, end = b[-1]
+ pref_position = PrefPosition.positioned_in
+ elif b[-1][1] == position - 1:
+ left_start, left_end = b[-1]
+ pref_position = PrefPosition.on_left
+ else:
+ left = b[index -1]
+ right = b[index]
+
+ at = right[0] == position
+ to_left = left[1] == position -1
+ if at and to_left:
+ pref_position = PrefPosition.on_left_and_at
+ start, end = right
+ left_start, left_end = left
+ elif at:
+ pref_position = PrefPosition.at
+ start, end = right
+ elif to_left:
+ pref_position = PrefPosition.on_left
+ left_start, left_end = left
+ elif position <= left[1]:
+ pref_position = PrefPosition.positioned_in
+ start, end = b[index - 1]
+
+ return (pref_position, start, end, left_start, left_end)
+
+ def displayPrefList(self, pref_list: Sequence[str]) -> None:
+ p = pref_list
+ values = []
+ for i in range(0, len(pref_list), 3):
+ try:
+ value = '<{}>'.format(self.pref_mapper[(p[i], p[i+1], p[i+2])])
+ except KeyError:
+ if p[i] == SEPARATOR:
+ value = SEPARATOR
+ else:
+ assert p[i] == TEXT
+ value = p[i+1]
+ values.append(value)
+
+ self.document().clear()
+ cursor = self.textCursor() # type: QTextCursor
+ cursor.insertText(''.join(values))
+
+ def insertPrefValue(self, pref_value: str) -> None:
+ cursor = self.textCursor() # type: QTextCursor
+ cursor.insertText('<{}>'.format(pref_value))
+
+ def _setHighlighter(self) -> None:
+ self.highlighter = PrefHighlighter(list(self.string_to_pref_mapper.keys()),
+ self.pref_color,
+ self.document())
+
+ self.highlighter.blockHighlighted.connect(self.generatePrefList)
+
+ def setPrefMapper(self, pref_mapper: Dict[Tuple[str, str, str], str],
+ pref_color: Dict[str, str]) -> None:
+ self.pref_mapper = pref_mapper
+ self.string_to_pref_mapper = {value: key for key, value in pref_mapper.items()}
+
+ self.pref_color = pref_color
+ self._setHighlighter()
+
+ def _parseTextFragment(self, text_fragment) -> List[str]:
+ if self.subfolder:
+ text_fragments = text_fragment.split(os.sep)
+ for index, text_fragment in enumerate(text_fragments):
+ if text_fragment:
+ self.user_pref_list.extend([TEXT, text_fragment, ''])
+ self.user_pref_colors.append('')
+ if index < len(text_fragments) - 1:
+ self.user_pref_list.extend([SEPARATOR, '', ''])
+ self.user_pref_colors.append('')
+ else:
+ self.user_pref_list.extend([TEXT, text_fragment, ''])
+ self.user_pref_colors.append('')
+
+ def _addColor(self, pref_defn: str) -> None:
+ self.user_pref_colors.append(self.pref_color[pref_defn])
+
+ @pyqtSlot()
+ def generatePrefList(self) -> None:
+ """
+ After syntax highlighting has completed, use its findings
+ to generate the user's pref list
+ """
+
+ text = self.document().toPlainText()
+ b = self.highlighter.boundaries
+
+ self.user_pref_list = pl = [] # type: List[str]
+ self.user_pref_colors = [] # type: List[str]
+
+ # Handle any text at the very beginning
+ if b and b[0][0] > 0:
+ text_fragment = text[:b[0][0]]
+ self._parseTextFragment(text_fragment)
+
+ if len(b) > 1:
+ for index, item in enumerate(b[1:]):
+ start, end = b[index]
+ # Add + 1 to start to remove the opening <
+ pl.extend(self.string_to_pref_mapper[text[start + 1: end]])
+ # Add + 1 to start to include the closing >
+ self._addColor(text[start: end + 1])
+
+ text_fragment = text[b[index][1] + 1:item[0]]
+ self._parseTextFragment(text_fragment)
+
+ # Handle the final pref value
+ if b:
+ start, end = b[-1]
+ # Add + 1 to start to remove the opening <
+ pl.extend(self.string_to_pref_mapper[text[start + 1: end]])
+ # Add + 1 to start to include the closing >
+ self._addColor(text[start: end + 1])
+ final = end + 1
+ else:
+ final = 0
+
+ # Handle any remaining text at the very end (or the complete string if there are
+ # no pref definition values)
+ if final < len(text):
+ text_fragment = text[final:]
+ self._parseTextFragment(text_fragment)
+
+ assert len(self.user_pref_colors) == len(self.user_pref_list) / 3
+ self.prefListGenerated.emit()
+
+
+class PrefHighlighter(QSyntaxHighlighter):
+ """
+ Highlight non-text preference values in the editor
+ """
+
+ blockHighlighted = pyqtSignal()
+
+ def __init__(self, pref_defn_strings: List[str],
+ pref_color: Dict[str, str],
+ document: QTextDocument) -> None:
+ super().__init__(document)
+
+ # Where detected preference values start and end:
+ # [(start, end), (start, end), ...]
+ self.boundaries = SortedList()
+
+ pref_defns = ('<{}>'.format(pref) for pref in pref_defn_strings)
+ self.highlightingRules = []
+ for pref in pref_defns:
+ format = QTextCharFormat()
+ format.setForeground(QBrush(QColor(pref_color[pref])))
+ self.highlightingRules.append((pref, format))
+
+ def find_all(self, text: str, pref_defn: str):
+ """
+ Find all occurrences of a preference definition in the text
+ :param text: text to search
+ :param pref_defn: the preference definition
+ :return: yield the position in the document's text
+ """
+ if not len(pref_defn):
+ raise StopIteration
+ start = 0
+ while True:
+ start = text.find(pref_defn, start)
+ if start == -1:
+ raise StopIteration
+ yield start
+ start += len(pref_defn)
+
+ def highlightBlock(self, text: str) -> None:
+
+ # Recreate the preference value from scratch
+ self.boundaries = SortedList()
+
+ for expression, format in self.highlightingRules:
+ for index in self.find_all(text, expression):
+ length = len(expression)
+ self.setFormat(index, length, format)
+ self.boundaries.add((index, index + length - 1))
+
+ self.blockHighlighted.emit()
+
+
+def make_subfolder_menu_entry(prefs: Tuple[str]) -> str:
+ """
+ Create the text for a menu / combobox item
+
+ :param prefs: single pref item, with title and elements
+ :return: item text
+ """
+
+ desc = prefs[0]
+ elements = prefs[1:]
+ return _("%(description)s - %(elements)s") % dict(
+ description=desc, elements=os.sep.join(elements))
+
+
+def make_rename_menu_entry(prefs: Tuple[str]) -> str:
+ """
+ Create the text for a menu / combobox item
+
+ :param prefs: single pref item, with title and elements
+ :return: item text
+ """
+
+ desc = prefs[0]
+ elements = prefs[1]
+ return _("%(description)s - %(elements)s") % dict(description=desc, elements=elements)
+
+
+class PresetComboBox(QComboBox):
+ """
+ Combox box displaying built-in presets, custom presets,
+ and some commands relating to preset management.
+
+ Used in in dialog window used to edit name generation and
+ also in the rename files panel.
+ """
+
+ def __init__(self, prefs: Preferences,
+ preset_names: List[str],
+ preset_type: PresetPrefType,
+ edit_mode: bool,
+ parent=None) -> None:
+ """
+ :param prefs: program preferences
+ :param preset_names: list of custom preset names
+ :param preset_type: one of photo rename, video rename,
+ photo subfolder, or video subfolder
+ :param edit_mode: if True, the combo box is being displayed
+ in an edit dialog window, else it's being displayed in the
+ file rename panel
+ :param parent: parent widget
+ """
+
+ super().__init__(parent)
+ self.edit_mode = edit_mode
+ self.prefs = prefs
+
+ self.preset_edited = False
+ self.new_preset = False
+
+ self.preset_type = preset_type
+
+ if preset_type == PresetPrefType.preset_photo_subfolder:
+ self.builtin_presets = PHOTO_SUBFOLDER_MENU_DEFAULTS
+ elif preset_type == PresetPrefType.preset_video_subfolder:
+ self.builtin_presets = VIDEO_SUBFOLDER_MENU_DEFAULTS
+ elif preset_type == PresetPrefType.preset_photo_rename:
+ self.builtin_presets = PHOTO_RENAME_MENU_DEFAULTS
+ else:
+ assert preset_type == PresetPrefType.preset_video_rename
+ self.builtin_presets = VIDEO_RENAME_MENU_DEFAULTS
+
+ self._setup_entries(preset_names)
+
+ def _setup_entries(self, preset_names: List[str]) -> None:
+
+ idx = 0
+
+ if self.edit_mode:
+ for pref in self.builtin_presets:
+ self.addItem(make_subfolder_menu_entry(pref), PresetClass.builtin)
+ idx += 1
+ else:
+ for pref in self.builtin_presets:
+ self.addItem(pref[0], PresetClass.builtin)
+ idx += 1
+
+ if not len(preset_names):
+ # preset_separator bool is used to indicate the existence of
+ # a separator in the combo box that is used to distinguish
+ # custom from built-in prests
+ self.preset_separator = False
+ else:
+ self.preset_separator = True
+
+ self.insertSeparator(idx)
+ idx += 1
+
+ for name in preset_names:
+ self.addItem(name, PresetClass.custom)
+ idx += 1
+
+ self.insertSeparator(idx)
+
+ if self.edit_mode:
+ self.addItem(_('Save New Custom Preset...'), PresetClass.new_preset)
+ self.addItem(_('Remove All Custom Presets...'), PresetClass.remove_all)
+ self.setRemoveAllCustomEnabled(bool(len(preset_names)))
+ else:
+ self.addItem(_('Custom...'), PresetClass.start_editor)
+
+ def resetEntries(self, preset_names: List[str]) -> None:
+ assert not self.edit_mode
+ self.clear()
+ self._setup_entries(preset_names)
+
+ def addCustomPreset(self, text: str) -> None:
+ """
+ Adds a new custom preset name to the comboxbox and sets the
+ combobox to display it.
+
+ :param text: the custom preset name
+ """
+
+ assert self.edit_mode
+ if self.new_preset or self.preset_edited:
+ self.resetPresetList()
+ if not self.preset_separator:
+ self.insertSeparator(len(self.builtin_presets))
+ self.preset_separator = True
+ idx = len(self.builtin_presets) + 1
+ self.insertItem(idx, text, PresetClass.custom)
+ self.setCurrentIndex(idx)
+
+ def removeAllCustomPresets(self, no_presets: int) -> None:
+ assert self.edit_mode
+ assert self.preset_separator
+ start = len(self.builtin_presets)
+ if self.new_preset:
+ start += 2
+ elif self.preset_edited:
+ self.resetPresetList()
+ end = start + no_presets
+ for row in range(end, start -1, -1):
+ self.removeItem(row)
+ self.preset_separator = False
+
+ def setPresetNew(self) -> None:
+ assert self.edit_mode
+ assert not self.preset_edited
+ if self.new_preset:
+ return
+ item_text = _('(New Custom Preset)')
+ self.new_preset = True
+ self.insertItem(0, item_text, PresetClass.edited)
+ self.insertSeparator(1)
+ self.setCurrentIndex(0)
+
+ def setPresetEdited(self, text: str) -> None:
+ """
+ Adds a new entry at the top of the combobox indicating that the current
+ preset has been edited.
+
+ :param text: the preset name to use
+ """
+
+ assert self.edit_mode
+ assert not self.new_preset
+ assert not self.preset_edited
+ item_text = _('%s (edited)') % text
+ self.insertItem(0, item_text, PresetClass.edited)
+ self.insertSeparator(1)
+ self.addItem(_('Update Custom Preset "%s"') % text, PresetClass.update_preset)
+ self.preset_edited = True
+ self.setCurrentIndex(0)
+
+ def resetPresetList(self) -> None:
+ """
+ Removes the combo box first line 'Preset name (edited)' or '(New Custom Preset)',
+ and its separator
+ """
+
+ assert self.edit_mode
+ assert self.new_preset or self.preset_edited
+ # remove combo box first line 'Preset name (edited)' or '(New Custom Preset)'
+ self.removeItem(0)
+ # remove separator
+ self.removeItem(0)
+ # remove Update Preset
+ if self.preset_edited:
+ index = self.count() - 1
+ self.removeItem(index)
+ self.preset_edited = self.new_preset = False
+
+ def setRemoveAllCustomEnabled(self, enabled: bool) -> None:
+ assert self.edit_mode
+ # Our big assumption here is that the model is a QStandardItemModel
+ model = self.model()
+ count = self.count()
+ if self.preset_edited:
+ row = count - 2
+ else:
+ row = count - 1
+ item = model.item(row, 0) # type: QStandardItem
+ if not enabled:
+ item.setFlags(Qt.NoItemFlags)
+ else:
+ item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+
+ def getComboBoxIndex(self, preset_index: int) -> int:
+ """
+ Calculate the index into the combo box list allowing for the separator
+ and other elements in the list of entries the user sees
+
+ :param preset_index: the preset index (built-in & custom)
+ :return: the index into the actual combobox entries including
+ any separators etc.
+ """
+
+ if self.edit_mode and (self.new_preset or self.preset_edited):
+ preset_index += 2
+ if preset_index < len(self.builtin_presets):
+ return preset_index
+ else:
+ assert self.preset_separator
+ return preset_index + 1
+
+
+ def getPresetIndex(self, combobox_index: int) -> int:
+ """
+ Opposite of getComboBoxIndex: calculates the preset index based on the
+ given combox box index (which includes separators etc.)
+ :param combobox_index: the index into the combobox entries the user sees
+ :return: the index into the presets (built-in & custom)
+ """
+
+ if self.edit_mode and (self.new_preset or self.preset_edited):
+ combobox_index -= 2
+ if combobox_index < len(self.builtin_presets):
+ return combobox_index
+ else:
+ assert self.preset_separator
+ return combobox_index - 1
+
+
+class CreatePreset(QDialog):
+ """
+ Very simple dialog window that allows user entry of new preset name.
+
+ Save button is disabled when the current name entered equals an existing
+ preset name or is empty.
+ """
+
+ def __init__(self, existing_custom_names: List[str], parent=None) -> None:
+ super().__init__(parent)
+
+ self.existing_custom_names = existing_custom_names
+
+ self.setModal(True)
+
+ title = _("Save New Custom Preset - Rapid Photo Downloader")
+ self.setWindowTitle(title)
+
+ self.name = QLineEdit()
+ metrics = QFontMetrics(QFont())
+ self.name.setMinimumWidth(metrics.width(title))
+ self.name.textEdited.connect(self.nameEdited)
+ flayout = QFormLayout()
+ flayout.addRow(_('Preset Name:'), self.name)
+
+ buttonBox = QDialogButtonBox()
+ buttonBox.addButton(QDialogButtonBox.Cancel) # type: QPushButton
+ self.saveButton = buttonBox.addButton(QDialogButtonBox.Save) # type: QPushButton
+ self.saveButton.setEnabled(False)
+ buttonBox.rejected.connect(self.reject)
+ buttonBox.accepted.connect(self.accept)
+
+ layout = QVBoxLayout()
+ layout.addLayout(flayout)
+ layout.addWidget(buttonBox)
+
+ self.setLayout(layout)
+
+ @pyqtSlot(str)
+ def nameEdited(self, name: str):
+ enabled = False
+ if len(name) > 0:
+ enabled = name not in self.existing_custom_names
+ self.saveButton.setEnabled(enabled)
+
+ def presetName(self) -> str:
+ """
+ :return: the name of the name the user wants to save the preset as
+ """
+
+ return self.name.text()
+
+
+def make_sample_rpd_file(sample_job_code: str,
+ prefs: Preferences,
+ generation_type: NameGenerationType,
+ sample_rpd_file: Optional[Union[Photo, Video]]=None) -> RPDFile:
+ """
+ Create a sample_rpd_file used for displaying to the user an example of their
+ file renaming preference in action on a sample file.
+
+ :param sample_job_code: sample of a Job Code
+ :param prefs: user preferences
+ :param generation_type: one of photo/video filenames/subfolders
+ :param sample_rpd_file: sample RPDFile that will possibly be overwritten
+ with new values
+ :return: sample RPDFile
+ """
+
+ downloads_today_tracker = DownloadsTodayTracker(
+ day_start=prefs.day_start,
+ downloads_today=prefs.downloads_today)
+ sequences = gn.Sequences(downloads_today_tracker,
+ prefs.stored_sequence_no)
+ if sample_rpd_file is not None:
+ if sample_rpd_file.metadata is None:
+ logging.debug('Sample file is missing its metadata')
+ sample_rpd_file = None
+ else:
+ sample_rpd_file.sequences = sequences
+ sample_rpd_file.download_start_time = datetime.datetime.now()
+
+ else:
+ # sample_rpd_file is None
+ if generation_type in (NameGenerationType.photo_name,
+ NameGenerationType.photo_subfolder):
+ sample_rpd_file = SamplePhoto(sequences=sequences)
+ else:
+ sample_rpd_file = SampleVideo(sequences=sequences)
+
+ sample_rpd_file.job_code = sample_job_code
+ sample_rpd_file.strip_characters = prefs.strip_characters
+ if sample_rpd_file.file_type == FileType.photo:
+ sample_rpd_file.generate_extension_case = prefs.photo_extension
+ else:
+ sample_rpd_file.generate_extension_case = prefs.video_extension
+
+ return sample_rpd_file
+
+class EditorCombobox(QComboBox):
+ """
+ Regular combobox, but ignores the mouse wheel
+ """
+
+ def wheelEvent(self, event: QWheelEvent) -> None:
+ event.ignore()
+
+class PrefDialog(QDialog):
+ """
+ Dialog window to allow editing of file renaming and subfolder generation
+ """
+
+ def __init__(self, pref_defn: OrderedDict,
+ user_pref_list: List[str],
+ generation_type: NameGenerationType,
+ prefs: Preferences,
+ sample_rpd_file: Optional[Union[Photo, Video]]=None,
+ parent=None) -> None:
+ """
+ Set up dialog to display all its controls based on the preference
+ definition being used.
+
+ :param pref_defn: definition of possible preference choices, i.e.
+ one of DICT_VIDEO_SUBFOLDER_L0, DICT_SUBFOLDER_L0, DICT_VIDEO_RENAME_L0
+ or DICT_IMAGE_RENAME_L0
+ :param user_pref_list: the user's actual rename / subfolder generation
+ preferences
+ :param generation_type: enum specifying what kind of name is being edited
+ (one of photo filename, video filename, photo subfolder, video subfolder)
+ :param prefs: program preferences
+ :param exiftool_process: daemon exiftool process
+ :param sample_rpd_file: a sample photo or video, whose contents will be
+ modified (i.e. don't pass a live RPDFile)
+ """
+
+ super().__init__(parent)
+
+ self.setModal(True)
+
+ self.generation_type = generation_type
+ if generation_type == NameGenerationType.photo_subfolder:
+ self.setWindowTitle('Photo Subfolder Generation Editor')
+ self.preset_type = PresetPrefType.preset_photo_subfolder
+ self.builtin_pref_lists = PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV
+ self.builtin_pref_names = [make_subfolder_menu_entry(pref)
+ for pref in PHOTO_SUBFOLDER_MENU_DEFAULTS]
+ elif generation_type == NameGenerationType.video_subfolder:
+ self.setWindowTitle('Video Subfolder Generation Editor')
+ self.preset_type = PresetPrefType.preset_video_subfolder
+ self.builtin_pref_lists = VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV
+ self.builtin_pref_names = [make_subfolder_menu_entry(pref)
+ for pref in VIDEO_SUBFOLDER_MENU_DEFAULTS]
+ elif generation_type == NameGenerationType.photo_name:
+ self.setWindowTitle('Photo Renaming Editor')
+ self.preset_type = PresetPrefType.preset_photo_rename
+ self.builtin_pref_lists = PHOTO_RENAME_MENU_DEFAULTS_CONV
+ self.builtin_pref_names = [make_rename_menu_entry(pref)
+ for pref in PHOTO_RENAME_MENU_DEFAULTS]
+ else:
+ self.setWindowTitle('Video Renaming Editor')
+ self.preset_type = PresetPrefType.preset_video_rename
+ self.builtin_pref_lists = VIDEO_RENAME_MENU_DEFAULTS_CONV
+ self.builtin_pref_names = [make_rename_menu_entry(pref)
+ for pref in VIDEO_RENAME_MENU_DEFAULTS]
+
+ self.prefs = prefs
+
+ # Cache custom preset name and pref lists
+ self.udpateCachedPrefLists()
+
+ self.current_custom_name = None
+
+ # Setup values needed for name generation
+
+ self.sample_rpd_file = make_sample_rpd_file(sample_rpd_file=sample_rpd_file,
+ sample_job_code=self.prefs.most_recent_job_code(missing=_('Job Code')),
+ prefs=self.prefs,
+ generation_type=generation_type)
+
+ # Setup widgets and helper values
+
+ # Translators: please do not modify or leave out html formatting tags like <i> and
+ # <b>. These are used to format the text the users sees
+ warning_msg = _(
+ '<b><font color="red">Warning:</font></b> <i>There is insufficient data to fully '
+ 'generate the name. Please use other renaming options.</i>')
+
+ self.is_subfolder = generation_type in (NameGenerationType.photo_subfolder,
+ NameGenerationType.video_subfolder)
+
+ if self.is_subfolder:
+ # Translators: please do not modify, change the order of or leave out html formatting
+ # tags like <i> and <b>. These are used to format the text the users sees.
+ # In this case, the </i> really is supposed to come before the <i>.
+ subfolder_msg = _("The character</i> %(separator)s <i>creates a new subfolder "
+ "level.") % dict(separator=os.sep)
+ # Translators: please do not modify, change the order of or leave out html formatting
+ # tags like <i> and <b>. These are used to format the text the users sees
+ # In this case, the </i> really is supposed to come before the <i>.
+ subfolder_first_char_msg = _("There is no need start or end with the folder "
+ "separator </i> %(separator)s<i>, because it is added "
+ "automatically.") % dict(separator=os.sep)
+ messages = (warning_msg, subfolder_msg, subfolder_first_char_msg)
+ else:
+ # Translators: please do not modify or leave out html formatting tags like <i> and
+ # <b>. These are used to format the text the users sees
+ unique_msg = _(
+ '<b><font color="red">Warning:</font></b> <i>Unique filenames may not be '
+ 'generated. Make filenames unique by using Sequence values.</i>'
+ )
+ messages = (warning_msg, unique_msg)
+
+ self.messageWidget = MessageWidget(messages=messages)
+
+ self.editor = PrefEditor(subfolder=self.is_subfolder)
+ sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
+ sizePolicy.setVerticalStretch(1)
+ self.editor.setSizePolicy(sizePolicy)
+
+ self.editor.prefListGenerated.connect(self.updateExampleFilename)
+
+ # Generated subfolder / file name example
+ self.example = QLabel()
+
+ # Combobox with built-in and user defined presets
+ self.preset = PresetComboBox(prefs=prefs, preset_names=self.preset_names,
+ preset_type=self.preset_type, edit_mode=True)
+ self.preset.activated.connect(self.presetComboItemActivated)
+
+ flayout = QFormLayout()
+ flayout.addRow(_('Preset:'), self.preset)
+ flayout.addRow(_('Example:'), self.example)
+
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+
+ layout.addLayout(flayout)
+ layout.addSpacing(QFontMetrics(QFont()).height() / 2)
+ layout.addWidget(self.editor)
+ layout.addWidget(self.messageWidget)
+
+ self.area = QScrollArea()
+ sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
+ sizePolicy.setVerticalStretch(10)
+ self.area.setSizePolicy(sizePolicy)
+ self.area.setFrameShape(QFrame.NoFrame)
+ layout.addWidget(self.area)
+
+ gbSizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
+
+ areaWidget = QWidget()
+ areaLayout = QVBoxLayout()
+ areaWidget.setLayout(areaLayout)
+ areaWidget.setSizePolicy(gbSizePolicy)
+
+ self.area.setWidget(areaWidget)
+ self.area.setWidgetResizable(True)
+
+ areaLayout.setContentsMargins(0, 0, 0, 0)
+
+ self.pushButtonSizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.mapper = QSignalMapper(self)
+ self.widget_mapper = dict() # type: Dict[str, Union[QComboBox, QLabel]]
+ self.pref_mapper = dict() # type: Dict[Tuple[str, str, str], str]
+ self.pref_color = dict() # type: Dict[str, str]
+
+ titles = [title for title in pref_defn if title not in (TEXT, SEPARATOR)]
+ pref_colors = {title: color.value for title, color in zip(titles, CustomColors)}
+ self.filename_pref_color = pref_colors[FILENAME]
+
+ for title in titles:
+ title_i18n = _(title)
+ color = pref_colors[title]
+ level1 = pref_defn[title]
+ gb = QGroupBox(title_i18n)
+ gb.setSizePolicy(gbSizePolicy)
+ gb.setFlat(True)
+ areaLayout.addWidget(gb)
+ gLayout = QGridLayout()
+ gb.setLayout(gLayout)
+ if level1 is None:
+ assert title == JOB_CODE
+ widget1 = QLabel(' ' + title_i18n)
+ widget2 = self.makeInsertButton()
+ self.widget_mapper[title] = widget1
+ self.mapper.setMapping(widget2, title)
+ self.pref_mapper[(title, '', '')] = title_i18n
+ self.pref_color['<{}>'.format(title_i18n)] = color
+ gLayout.addWidget(self.makeColorCodeLabel(color), 0, 0)
+ gLayout.addWidget(widget1, 0, 1)
+ gLayout.addWidget(widget2, 0, 2)
+ elif title == METADATA:
+ elements = []
+ data = []
+ for element in level1:
+ element_i18n = _(element)
+ level2 = level1[element]
+ if level2 is None:
+ elements.append(element_i18n)
+ data.append([METADATA, element, ''])
+ self.pref_mapper[(METADATA, element, '')] = element_i18n
+ self.pref_color['<{}>'.format(element_i18n)] = color
+ else:
+ for e in level2:
+ e_i18n = _(e)
+ # Translators: appears in a combobox, e.g. Image Date (YYYY)
+ item = _('{choice} ({variant})').format(choice=element_i18n,
+ variant=e_i18n)
+ elements.append(item)
+ data.append([METADATA, element, e])
+ self.pref_mapper[(METADATA, element, e)] = item
+ self.pref_color['<{}>'.format(item)] = color
+ widget1 = EditorCombobox()
+ for element, data_item in zip(elements, data):
+ widget1.addItem(element, data_item)
+ widget2 = self.makeInsertButton()
+ widget1.currentTextChanged.connect(self.mapper.map)
+ self.mapper.setMapping(widget2, title)
+ self.mapper.setMapping(widget1, title)
+ self.widget_mapper[title] = widget1
+ gLayout.addWidget(self.makeColorCodeLabel(color), 0, 0)
+ gLayout.addWidget(widget1, 0, 1)
+ gLayout.addWidget(widget2, 0, 2)
+ else:
+ for row, level1 in enumerate(pref_defn[title]):
+ widget1 = EditorCombobox()
+ level1_i18n = _(level1)
+ items = (_('{choice} ({variant})').format(
+ choice=level1_i18n, variant=_(element))
+ for element in pref_defn[title][level1])
+ data = ([title, level1, element] for element in pref_defn[title][level1])
+ for item, data_item in zip(items, data):
+ widget1.addItem(item, data_item)
+ self.pref_mapper[tuple(data_item)] = item
+ self.pref_color['<{}>'.format(item)] = color
+ widget2 = self.makeInsertButton()
+ widget1.currentTextChanged.connect(self.mapper.map)
+
+ self.mapper.setMapping(widget2, level1)
+ self.mapper.setMapping(widget1, level1)
+ self.widget_mapper[level1] = widget1
+ gLayout.addWidget(self.makeColorCodeLabel(color), row, 0)
+ gLayout.addWidget(widget1, row, 1)
+ gLayout.addWidget(widget2, row, 2)
+
+ self.mapper.mapped[str].connect(self.choiceMade)
+
+ buttonBox = QDialogButtonBox(
+ QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Help
+ )
+ self.helpButton = buttonBox.button(QDialogButtonBox.Help) # type: QPushButton
+ self.helpButton.clicked.connect(self.helpButtonClicked)
+ self.helpButton.setToolTip(_('Get help online...'))
+ buttonBox.rejected.connect(self.reject)
+ buttonBox.accepted.connect(self.accept)
+
+ layout.addWidget(buttonBox)
+
+ self.editor.setPrefMapper(self.pref_mapper, self.pref_color)
+ self.editor.displayPrefList(user_pref_list)
+
+ self.show()
+ self.setWidgetSizes()
+
+ def helpButtonClicked(self) -> None:
+ if self.generation_type in (NameGenerationType.photo_name, NameGenerationType.video_name):
+ location = '#rename'
+ else:
+ location = '#subfoldergeneration'
+ webbrowser.open_new_tab("http://www.damonlynch.net/rapid/documentation/{}".format(location))
+
+ def makeInsertButton(self) -> QPushButton:
+ w = QPushButton(_('Insert'))
+ w.clicked.connect(self.mapper.map)
+ w.setSizePolicy(self.pushButtonSizePolicy)
+ return w
+
+ def setWidgetSizes(self) -> None:
+ """
+ Resize widgets for enhanced visual layout
+ """
+
+ # Set the widths of the comboboxes and labels to the width of the
+ # longest control
+ width = max(widget.width() for widget in self.widget_mapper.values())
+ for widget in self.widget_mapper.values():
+ widget.setMinimumWidth(width)
+
+ # Set the scroll area to be big enough to eliminate the horizontal scrollbar
+ scrollbar_width = self.style().pixelMetric(QStyle.PM_ScrollBarExtent)
+ self.area.setMinimumWidth(self.area.widget().width() + scrollbar_width)
+
+ @pyqtSlot(str)
+ def choiceMade(self, widget: str) -> None:
+ """
+ User has pushed one of the "Insert" buttons or selected a new value in one
+ of the combo boxes.
+
+ :param widget: widget's name, which uniquely identifies it
+ """
+
+ if widget == JOB_CODE:
+ pref_value = _(JOB_CODE)
+ else:
+ combobox = self.widget_mapper[widget] # type: QComboBox
+ pref_value = combobox.currentText()
+
+ self.editor.insertPrefValue(pref_value)
+
+ # Set focus not on the control that was just used, but the editor
+ self.editor.setFocus(Qt.OtherFocusReason)
+
+ def makeColorCodeLabel(self, color: str) -> QLabel:
+ """
+ Generate a colored square to show beside the combo boxes / label
+ :param color: color to use, e.g. #7a9c38
+ :return: the square in form of a label
+ """
+
+ colorLabel = QLabel(' ')
+ colorLabel.setStyleSheet('QLabel {background-color: %s;}' % color)
+ size = QFontMetrics(QFont()).height()
+ colorLabel.setFixedSize(QSize(size, size))
+ return colorLabel
+
+ def updateExampleFilename(self) -> None:
+
+ user_pref_list = self.editor.user_pref_list
+ self.user_pref_colors = self.editor.user_pref_colors
+
+ if not self.is_subfolder:
+ self.user_pref_colors.append(self.filename_pref_color)
+
+ self.messageWidget.setCurrentIndex(0)
+
+ if self.is_subfolder:
+ if user_pref_list:
+ try:
+ user_pref_list.index(SEPARATOR)
+ except ValueError:
+ # Inform the user that a subfolder separator (os.sep) is used to create
+ # subfolder levels
+ self.messageWidget.setCurrentIndex(2)
+ else:
+ if user_pref_list[0] == SEPARATOR or user_pref_list[-3] == SEPARATOR:
+ # inform the user that there is no need to start or finish with a
+ # subfolder separator (os.sep)
+ self.messageWidget.setCurrentIndex(3)
+ else:
+ # Inform the user that a subfolder separator (os.sep) is used to create
+ # subfolder levels
+ self.messageWidget.setCurrentIndex(2)
+
+ changed, user_pref_list, self.user_pref_colors = filter_subfolder_prefs(
+ user_pref_list, self.user_pref_colors)
+ else:
+ try:
+ user_pref_list.index(SEQUENCES)
+ except ValueError:
+ # Inform the user that sequences can be used to make filenames unique
+ self.messageWidget.setCurrentIndex(2)
+
+ if self.generation_type == NameGenerationType.photo_name:
+ self.name_generator = gn.PhotoName(user_pref_list)
+ elif self.generation_type == NameGenerationType.video_name:
+ self.name_generator = gn.VideoName(user_pref_list)
+ elif self.generation_type == NameGenerationType.photo_subfolder:
+ self.name_generator = gn.PhotoSubfolder(user_pref_list)
+ else:
+ assert self.generation_type == NameGenerationType.video_subfolder
+ self.name_generator = gn.VideoSubfolder(user_pref_list)
+
+ self.name_parts = self.name_generator.generate_name(self.sample_rpd_file, parts=True)
+ self.showExample()
+ self.updateComboBoxCurrentIndex()
+
+ def updateComboBoxCurrentIndex(self) -> None:
+ combobox_index, pref_list_index = self.getPresetMatch()
+ if pref_list_index >= 0:
+ self.preset.setCurrentIndex(combobox_index)
+ if self.preset.preset_edited or self.preset.new_preset:
+ self.preset.resetPresetList()
+ if pref_list_index >= len(self.builtin_pref_names):
+ self.current_custom_name = self.preset.currentText()
+ else:
+ self.current_custom_name = None
+ elif not (self.preset.new_preset or self.preset.preset_edited):
+ if self.current_custom_name is None:
+ self.preset.setPresetNew()
+ else:
+ self.preset.setPresetEdited(self.current_custom_name)
+ else:
+ self.preset.setCurrentIndex(0)
+
+ def showExample(self) -> None:
+ """
+ Insert text into example widget, eliding it if necessary
+ """
+
+ user_pref_colors = self.user_pref_colors
+
+ parts = copy.copy(self.name_parts)
+ metrics = QFontMetrics(self.example.font())
+ width = self.example.width() - metrics.width('…')
+
+ # Cannot elide rich text using Qt code. Thus, elide the plain text.
+ plain_text_name = ''.join(parts)
+
+ if self.is_subfolder:
+ plain_text_name = self.name_generator.filter_subfolder_characters(plain_text_name)
+ elided_text = metrics.elidedText(plain_text_name, Qt.ElideRight, width)
+ elided = False
+
+ while plain_text_name != elided_text:
+ elided = True
+ parts = remove_last_char_from_list_str(parts)
+ plain_text_name = ''.join(parts)
+ if self.is_subfolder:
+ plain_text_name = self.name_generator.filter_subfolder_characters(plain_text_name)
+ elided_text = metrics.elidedText(plain_text_name, Qt.ElideRight, width)
+
+ colored_parts = ['<span style="color: {};">{}</span>'.format(color, part) if color else part
+ for part, color in zip(parts, user_pref_colors)]
+
+ name = ''.join(colored_parts)
+ if elided:
+ name = '{}&hellip;'.format(name)
+
+ if self.is_subfolder:
+ name = self.name_generator.filter_subfolder_characters(name)
+
+ if self.sample_rpd_file.name_generation_problem:
+ self.messageWidget.setCurrentIndex(1)
+
+ self.example.setTextFormat(Qt.RichText)
+ self.example.setText(name)
+
+ def resizeEvent(self, event: QResizeEvent) -> None:
+ if self.example.text():
+ self.showExample()
+ super().resizeEvent(event)
+
+ def getPrefList(self) -> List[str]:
+ """
+ :return: the pref list the user has specified
+ """
+
+ return self.editor.user_pref_list
+
+ @pyqtSlot(int)
+ def presetComboItemActivated(self, index: int) -> None:
+ """
+ Respond to user activating the Preset combo box.
+
+ :param index: index of the item activated
+ """
+
+ preset_class = self.preset.currentData()
+ if preset_class == PresetClass.new_preset:
+ createPreset = CreatePreset(existing_custom_names=self.preset_names)
+ if createPreset.exec():
+ # User has created a new preset
+ preset_name = createPreset.presetName()
+ assert preset_name not in self.preset_names
+ self.current_custom_name = preset_name
+ self.preset.addCustomPreset(preset_name)
+ self.saveNewPreset(preset_name=preset_name)
+ if len(self.preset_names) == 1:
+ self.preset.setRemoveAllCustomEnabled(True)
+ else:
+ # User cancelled creating a new preset
+ self.updateComboBoxCurrentIndex()
+ elif preset_class in (PresetClass.builtin, PresetClass.custom):
+ index = self.combined_pref_names.index(self.preset.currentText())
+ pref_list = self.combined_pref_lists[index]
+ self.editor.displayPrefList(pref_list=pref_list)
+ if index >= len(self.builtin_pref_names):
+ self.movePresetToFront(index=len(self.builtin_pref_names) - index)
+ elif preset_class == PresetClass.remove_all:
+ self.preset.removeAllCustomPresets(no_presets=len(self.preset_names))
+ self.clearCustomPresets()
+ self.preset.setRemoveAllCustomEnabled(False)
+ self.updateComboBoxCurrentIndex()
+ elif preset_class == PresetClass.update_preset:
+ self.updateExistingPreset()
+ self.updateComboBoxCurrentIndex()
+
+ def updateExistingPreset(self) -> None:
+ """
+ Updates (saves) an existing preset (assumed to be self.current_custom_name)
+ with the new user_pref_list found in the editor.
+
+ Assumes cached self.preset_names and self.preset_pref_lists represent
+ current save preferences. Will update these and overwrite the relevant
+ preset preference.
+ """
+
+ preset_name = self.current_custom_name
+ user_pref_list = self.editor.user_pref_list
+ index = self.preset_names.index(preset_name)
+ self.preset_pref_lists[index] = user_pref_list
+ if index > 0:
+ self.movePresetToFront(index=index)
+ else:
+ self._updateCombinedPrefs()
+ self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists)
+
+ def movePresetToFront(self, index: int) -> None:
+ """
+ Extracts the preset from the current list of presets and moves it
+ to the front if not already there.
+
+ Assumes cached self.preset_names and self.preset_pref_lists represent
+ current save preferences. Will update these and overwrite the relevant
+ preset preference.
+
+ :param index: index into self.preset_pref_lists / self.preset_names of
+ the item to move
+ """
+
+ if index == 0:
+ return
+ preset_name = self.preset_names.pop(index)
+ pref_list = self.preset_pref_lists.pop(index)
+ self.preset_names.insert(0, preset_name)
+ self.preset_pref_lists.insert(0, pref_list)
+ self._updateCombinedPrefs()
+ self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists)
+
+ def saveNewPreset(self, preset_name: str) -> None:
+ """
+ Saves the current user_pref_list (retrieved from the editor) and
+ saves it in the program preferences.
+
+ Assumes cached self.preset_names and self.preset_pref_lists represent
+ current save preferences. Will update these and overwrite the relevant
+ preset preference.
+
+ :param preset_name: name for the new preset
+ """
+
+ user_pref_list = self.editor.user_pref_list
+ self.preset_names.insert(0, preset_name)
+ self.preset_pref_lists.insert(0, user_pref_list)
+ self._updateCombinedPrefs()
+ self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists)
+
+ def clearCustomPresets(self) -> None:
+ """
+ Deletes all of the custom presets.
+
+ Assumes cached self.preset_names and self.preset_pref_lists represent
+ current save preferences. Will update these and overwrite the relevant
+ preset preference.
+ """
+ self.preset_names = []
+ self.preset_pref_lists = []
+ self.current_custom_name = None
+ self._updateCombinedPrefs()
+ self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists)
+
+ def udpateCachedPrefLists(self) -> None:
+ self.preset_names, self.preset_pref_lists = self.prefs.get_preset(
+ preset_type=self.preset_type)
+ self._updateCombinedPrefs()
+
+ def _updateCombinedPrefs(self):
+ self.combined_pref_names = self.builtin_pref_names + self.preset_names
+ self.combined_pref_lists = self.builtin_pref_lists + tuple(self.preset_pref_lists)
+
+ def getPresetMatch(self) -> Tuple[int, int]:
+ """
+ :return: Tuple of the Preset combobox index and the combined pref/name list index,
+ if the current user pref list matches an entry in it. Else Tuple of (-1, -1).
+ """
+
+ index = match_pref_list(pref_lists=self.combined_pref_lists,
+ user_pref_list=self.editor.user_pref_list)
+ if index >= 0:
+ combobox_name = self.combined_pref_names[index]
+ return self.preset.findText(combobox_name), index
+ return -1, -1
+
+ @pyqtSlot()
+ def accept(self) -> None:
+ """
+ Slot called when the okay button is clicked.
+
+ If there are unsaved changes, query the user if they want their changes
+ saved as a new preset or if the existing preset should be updated
+ """
+
+ if self.preset.preset_edited or self.preset.new_preset:
+ msgBox = QMessageBox()
+ title = _("Save Preset - Rapid Photo Downloader")
+ msgBox.setTextFormat(Qt.RichText)
+ msgBox.setIcon(QMessageBox.Question)
+ msgBox.setWindowTitle(title)
+ if self.preset.new_preset:
+ message = _("<b>Do you want to save the changes in a new custom preset?</b><br><br>"
+ "Creating a custom preset is not required, but can help you keep "
+ "organized.<br><br>"
+ "The changes to the preferences will still be applied regardless of "
+ "whether you create a new custom preset or not.")
+ msgBox.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
+ updateButton = newButton = None
+ else:
+ assert self.preset.preset_edited
+ message = _("<b>Do you want to save the changes in a custom preset?</b><br><br>"
+ "If you like, you can create a new custom preset or update the "
+ "existing custom preset.<br><br>"
+ "The changes to the preferences will still be applied regardless of "
+ "whether you save a custom preset or not.")
+ updateButton = msgBox.addButton(_('Update Custom Preset "%s"') %
+ self.current_custom_name, QMessageBox.YesRole)
+ newButton = msgBox.addButton(_('Save New Custom Preset'), QMessageBox.YesRole)
+ msgBox.addButton(QMessageBox.No)
+
+ msgBox.setText(message)
+ choice = msgBox.exec()
+ save_new = update = False
+ if self.preset.new_preset:
+ save_new = choice == QMessageBox.Yes
+ else:
+ if msgBox.clickedButton() == updateButton:
+ update = True
+ elif msgBox.clickedButton() == newButton:
+ save_new = True
+
+ if save_new:
+ createPreset = CreatePreset(existing_custom_names=self.preset_names)
+ if createPreset.exec():
+ # User has created a new preset
+ preset_name = createPreset.presetName()
+ assert preset_name not in self.preset_names
+ self.saveNewPreset(preset_name=preset_name)
+ elif update:
+ self.updateExistingPreset()
+
+ # Regardless of any user actions, close the dialog box
+ super().accept()
+
+
+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 = PrefDialog(DICT_IMAGE_RENAME_L0, PHOTO_RENAME_MENU_DEFAULTS_CONV[1],
+ NameGenerationType.photo_name, prefs)
+ # prefDialog = PrefDialog(DICT_VIDEO_RENAME_L0, VIDEO_RENAME_MENU_DEFAULTS_CONV[1],
+ # NameGenerationType.video_name, prefs)
+ # prefDialog = PrefDialog(DICT_SUBFOLDER_L0, PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2],
+ # NameGenerationType.photo_subfolder, prefs)
+ prefDialog.show()
+ app.exec_()
+