1# Copyright (C) 2016-2020 Damon Lynch <damonlynch@gmail.com> 2 3# This file is part of Rapid Photo Downloader. 4# 5# Rapid Photo Downloader is free software: you can redistribute it and/or 6# modify it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Rapid Photo Downloader is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Rapid Photo Downloader. If not, 17# see <http://www.gnu.org/licenses/>. 18 19""" 20Display file renaming preferences, including sequence numbers 21""" 22 23__author__ = 'Damon Lynch' 24__copyright__ = "Copyright 2016-2020, Damon Lynch" 25 26from typing import Optional, Dict, Tuple, Union 27import logging 28 29from PyQt5.QtCore import Qt, pyqtSlot, QTime 30from PyQt5.QtWidgets import ( 31 QWidget, QSizePolicy, QComboBox, QFormLayout, QVBoxLayout, QLabel, QSpinBox, QTimeEdit, 32 QCheckBox, QGroupBox, QScrollArea, QFrame 33) 34from PyQt5.QtGui import (QColor, QPalette) 35 36 37from raphodo.constants import ( 38 PresetPrefType, NameGenerationType, ThumbnailBackgroundName, PresetClass, FileType 39) 40from raphodo.utilities import platform_c_maxint 41from raphodo.rpdfile import Photo, Video 42from raphodo.nameeditor import PrefDialog, make_sample_rpd_file, PresetComboBox 43import raphodo.exiftool as exiftool 44import raphodo.generatename as gn 45from raphodo.generatenameconfig import * 46from raphodo.viewutils import QFramedWidget 47from raphodo.panelview import QPanelView 48from raphodo.preferences import Preferences, DownloadsTodayTracker 49 50 51class RenameWidget(QFramedWidget): 52 """ 53 Display combo boxes for file renaming and file extension case handling, and 54 an example file name 55 """ 56 57 def __init__(self, preset_type: PresetPrefType, 58 prefs: Preferences, 59 exiftool_process: exiftool.ExifTool, 60 parent) -> None: 61 super().__init__(parent) 62 self.setBackgroundRole(QPalette.Base) 63 self.setAutoFillBackground(True) 64 self.exiftool_process = exiftool_process 65 self.prefs = prefs 66 self.preset_type = preset_type 67 if preset_type == PresetPrefType.preset_photo_rename: 68 self.file_type = FileType.photo 69 self.pref_defn = DICT_IMAGE_RENAME_L0 70 self.generation_type = NameGenerationType.photo_name 71 self.index_lookup = self.prefs.photo_rename_index 72 self.pref_conv = PHOTO_RENAME_MENU_DEFAULTS_CONV 73 self.generation_type = NameGenerationType.photo_name 74 else: 75 self.file_type = FileType.video 76 self.pref_defn = DICT_VIDEO_RENAME_L0 77 self.generation_type = NameGenerationType.video_name 78 self.index_lookup = self.prefs.video_rename_index 79 self.pref_conv = VIDEO_RENAME_MENU_DEFAULTS_CONV 80 self.generation_type = NameGenerationType.video_name 81 82 self.sample_rpd_file = make_sample_rpd_file( 83 sample_job_code=self.prefs.most_recent_job_code(missing=_('Job Code')), 84 prefs=self.prefs, generation_type=self.generation_type 85 ) 86 87 layout = QFormLayout() 88 self.setLayout(layout) 89 90 self.getCustomPresets() 91 92 self.renameCombo = PresetComboBox( 93 prefs=self.prefs, preset_names=self.preset_names, preset_type=preset_type, 94 parent=self, edit_mode=False 95 ) 96 self.setRenameComboIndex() 97 self.renameCombo.activated.connect(self.renameComboItemActivated) 98 99 # File extensions 100 self.extensionCombo = QComboBox() 101 self.extensionCombo.addItem(_(ORIGINAL_CASE), ORIGINAL_CASE) 102 self.extensionCombo.addItem(_(UPPERCASE), UPPERCASE) 103 self.extensionCombo.addItem(_(LOWERCASE), LOWERCASE) 104 if preset_type == PresetPrefType.preset_photo_rename: 105 pref_value = self.prefs.photo_extension 106 else: 107 pref_value = self.prefs.video_extension 108 try: 109 index = [ORIGINAL_CASE, UPPERCASE, LOWERCASE].index(pref_value) 110 except ValueError: 111 if preset_type == PresetPrefType.preset_photo_rename: 112 t = 'Photo' 113 else: 114 t = 'Video' 115 logging.error('%s extension case value is invalid. Resetting to lower case.', t) 116 index = 2 117 self.extensionCombo.setCurrentIndex(index) 118 self.extensionCombo.currentIndexChanged.connect(self.extensionChanged) 119 120 self.example = QLabel() 121 self.updateExampleFilename() 122 123 layout.addRow(_('Preset:'), self.renameCombo) 124 layout.addRow(_('Extension:'), self.extensionCombo) 125 layout.addRow(_('Example:'), self.example) 126 127 self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) 128 129 def setRenameComboIndex(self) -> None: 130 """ 131 Set the value being displayed in the combobox to reflect the 132 current renaming preference. 133 134 Takes into account built-in renaming presets and custom presets. 135 """ 136 137 index = self.index_lookup(self.preset_pref_lists) 138 if index == -1: 139 # Set to the "Custom..." value 140 cb_index = self.renameCombo.count() - 1 141 else: 142 # Set to appropriate combobox idex, allowing for possible separator 143 cb_index = self.renameCombo.getComboBoxIndex(index) 144 logging.debug( 145 "Setting %s combobox chosen value to %s", self.file_type.name, 146 self.renameCombo.itemText(cb_index) 147 ) 148 self.renameCombo.setCurrentIndex(cb_index) 149 150 def pref_list(self) -> List[str]: 151 """ 152 :return: the user's file naming preference according to whether 153 this widget is handling photos or videos 154 """ 155 if self.preset_type == PresetPrefType.preset_photo_rename: 156 return self.prefs.photo_rename 157 else: 158 return self.prefs.video_rename 159 160 @pyqtSlot(int) 161 def renameComboItemActivated(self, index: int) -> None: 162 """ 163 Respond to user activating the Rename preset combo box. 164 165 :param index: index of the item activated 166 """ 167 168 user_pref_list = None 169 170 preset_class = self.renameCombo.currentData() 171 if preset_class == PresetClass.start_editor: 172 173 prefDialog = PrefDialog( 174 self.pref_defn, self.pref_list(), self.generation_type, self.prefs, 175 self.sample_rpd_file 176 ) 177 178 if prefDialog.exec(): 179 user_pref_list = prefDialog.getPrefList() 180 if not user_pref_list: 181 user_pref_list = None 182 183 # Regardless of whether the user clicked OK or cancel, refresh the rename combo 184 # box entries 185 self.getCustomPresets() 186 self.renameCombo.resetEntries(self.preset_names) 187 self.setUserPrefList(user_pref_list=user_pref_list) 188 self.setRenameComboIndex() 189 else: 190 assert preset_class == PresetClass.custom or preset_class == PresetClass.builtin 191 index = self.renameCombo.getPresetIndex(self.renameCombo.currentIndex()) 192 user_pref_list = self.combined_pref_lists[index] 193 self.setUserPrefList(user_pref_list=user_pref_list) 194 195 self.updateExampleFilename() 196 197 def getCustomPresets(self) -> None: 198 """ 199 Get the custom presets from the user preferences and store them in lists 200 """ 201 202 self.preset_names, self.preset_pref_lists = self.prefs.get_preset( 203 preset_type=self.preset_type) 204 self.combined_pref_lists = self.pref_conv + tuple(self.preset_pref_lists) 205 206 def setUserPrefList(self, user_pref_list: List[str]) -> None: 207 """ 208 Update the user preferences with a new preference value 209 :param user_pref_list: the photo or video rename preference list 210 """ 211 212 if user_pref_list is not None: 213 logging.debug("Setting new %s rename preference value", self.file_type.name) 214 if self.preset_type == PresetPrefType.preset_photo_rename: 215 self.prefs.photo_rename = user_pref_list 216 else: 217 self.prefs.video_rename = user_pref_list 218 219 def updateExampleFilename(self, downloads_today: Optional[List[str]]=None, 220 stored_sequence_no: Optional[int]=None) -> None: 221 """ 222 Update filename shown to user that serves as an example of the 223 renaming rule in practice on sample data. 224 225 :param downloads_today: if specified, update the downloads today value 226 :param stored_sequence_no: if specified, update the stored sequence value 227 """ 228 229 if downloads_today: 230 self.sample_rpd_file.sequences.downloads_today_tracker.downloads_today = downloads_today 231 if stored_sequence_no is not None: 232 self.sample_rpd_file.sequences.stored_sequence_no = stored_sequence_no 233 234 if self.preset_type == PresetPrefType.preset_photo_rename: 235 self.name_generator = gn.PhotoName(self.prefs.photo_rename) 236 logging.debug("Updating example photo name in rename panel") 237 else: 238 self.name_generator = gn.VideoName(self.prefs.video_rename) 239 logging.debug("Updating example video name in rename panel") 240 241 self.example.setText(self.name_generator.generate_name(self.sample_rpd_file)) 242 243 def updateSampleFile(self, sample_rpd_file: Union[Photo, Video]) -> None: 244 self.sample_rpd_file = make_sample_rpd_file( 245 sample_rpd_file=sample_rpd_file, 246 sample_job_code=self.prefs.most_recent_job_code(missing=_('Job Code')), 247 prefs=self.prefs, 248 generation_type=self.generation_type 249 ) 250 self.updateExampleFilename() 251 252 @pyqtSlot(int) 253 def extensionChanged(self, index: int) -> None: 254 """ 255 Respond to user changing the case of file extensions in file name generation. 256 257 Save new preference value, and update example file name. 258 """ 259 260 value = self.extensionCombo.currentData() 261 if self.preset_type == PresetPrefType.preset_photo_rename: 262 self.prefs.photo_extension = value 263 else: 264 self.prefs.video_extension = value 265 self.sample_rpd_file.generate_extension_case = value 266 self.updateExampleFilename() 267 268 269class RenameOptionsWidget(QFramedWidget): 270 """ 271 Display and allow editing of preference values for Downloads today 272 and Stored Sequence Number and associated options, as well as 273 the strip incompatible characters option. 274 """ 275 276 def __init__(self, prefs: Preferences, 277 photoRenameWidget: RenameWidget, 278 videoRenameWidget: RenameWidget, 279 parent) -> None: 280 super().__init__(parent) 281 282 self.prefs = prefs 283 self.photoRenameWidget = photoRenameWidget 284 self.videoRenameWidget = videoRenameWidget 285 286 self.setBackgroundRole(QPalette.Base) 287 self.setAutoFillBackground(True) 288 289 compatibilityLayout = QVBoxLayout() 290 layout = QVBoxLayout() 291 self.setLayout(layout) 292 293 # QSpinBox cannot display values greater than this value 294 self.c_maxint = platform_c_maxint() 295 296 tip = _('A counter for how many downloads occur on each day') 297 self.downloadsTodayLabel = QLabel(_('Downloads today:')) 298 self.downloadsToday = QSpinBox() 299 self.downloadsToday.setMinimum(0) 300 # QSpinBox defaults to a maximum of 99 301 self.downloadsToday.setMaximum(self.c_maxint) 302 self.downloadsToday.setToolTip(tip) 303 304 # This instance of the downloads today tracker is secondary to the 305 # instance in the rename files process. That process automatically 306 # updates the value and then once a download is complete, the 307 # downloads today value here is overwritten. 308 self.downloads_today_tracker = DownloadsTodayTracker( 309 day_start=self.prefs.day_start, 310 downloads_today=self.prefs.downloads_today) 311 312 downloads_today = self.downloads_today_tracker.get_or_reset_downloads_today() 313 if self.prefs.downloads_today != self.downloads_today_tracker.downloads_today: 314 self.prefs.downloads_today = self.downloads_today_tracker.downloads_today 315 316 self.downloadsToday.setValue(downloads_today) 317 self.downloadsToday.valueChanged.connect(self.downloadsTodayChanged) 318 319 tip = _('A counter that is remembered each time the program is run ') 320 self.storedNumberLabel = QLabel(_('Stored number:')) 321 self.storedNumberLabel.setToolTip(tip) 322 self.storedNumber = QSpinBox() 323 self.storedNumberLabel.setBuddy(self.storedNumber) 324 self.storedNumber.setMinimum(0) 325 self.storedNumber.setMaximum(self.c_maxint) 326 self.storedNumber.setToolTip(tip) 327 328 self.storedNumber.setValue(self.stored_sequence_no) 329 self.storedNumber.valueChanged.connect(self.storedNumberChanged) 330 331 tip = _('The time at which the <i>Downloads today</i> sequence number should be reset') 332 self.dayStartLabel = QLabel(_('Day start:')) 333 self.dayStartLabel.setToolTip(tip) 334 335 self.dayStart = QTimeEdit() 336 self.dayStart.setToolTip(tip) 337 self.dayStart.setTime(self.prefs.get_day_start_qtime()) 338 self.dayStart.timeChanged.connect(self.timeChanged) 339 # 24 hour format, if wanted in a future release: 340 # self.dayStart.setDisplayFormat('HH:mm:ss') 341 342 self.sync = QCheckBox(_('Synchronize RAW + JPEG')) 343 self.sync.setChecked(self.prefs.synchronize_raw_jpg) 344 self.sync.stateChanged.connect(self.syncChanged) 345 tip = _( 346 'Synchronize sequence numbers for matching RAW and JPEG pairs.\n\n' 347 'See the online documentation for more details.' 348 ) 349 self.sync.setToolTip(tip) 350 351 self.sequences = QGroupBox(_('Sequence Numbers')) 352 353 sequencesLayout = QFormLayout() 354 355 sequencesLayout.addRow(self.storedNumberLabel, self.storedNumber) 356 sequencesLayout.addRow(self.downloadsTodayLabel, self.downloadsToday) 357 sequencesLayout.addRow(self.dayStartLabel, self.dayStart) 358 sequencesLayout.addRow(self.sync) 359 360 self.sequences.setLayout(sequencesLayout) 361 362 self.stripCharacters = QCheckBox(_('Strip incompatible characters')) 363 self.stripCharacters.setChecked(self.prefs.strip_characters) 364 self.stripCharacters.stateChanged.connect(self.stripCharactersChanged) 365 self.stripCharacters.setToolTip( 366 _( 367 'Whether photo, video and folder names should have any characters removed that ' 368 'are not allowed by other operating systems' 369 ) 370 ) 371 self.compatibility = QGroupBox(_('Compatibility')) 372 self.compatibility.setLayout(compatibilityLayout) 373 compatibilityLayout.addWidget(self.stripCharacters) 374 375 layout.addWidget(self.sequences) 376 layout.addWidget(self.compatibility) 377 layout.addStretch() 378 layout.setSpacing(18) 379 380 self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) 381 382 @property 383 def stored_sequence_no(self) -> int: 384 try: 385 stored_value = int(self.prefs.stored_sequence_no) + 1 386 assert 0 <= stored_value <= self.c_maxint 387 except (ValueError, AssertionError): 388 stored_value = 0 389 logging.error("Resetting invalid stored sequence number to 0") 390 self.prefs.stored_sequence_no = -1 391 return stored_value 392 393 @stored_sequence_no.setter 394 def stored_sequence_no(self, value: int) -> None: 395 logging.debug("Setting stored sequence no to %d", value) 396 self.prefs.stored_sequence_no = value - 1 397 398 @pyqtSlot(QTime) 399 def timeChanged(self, time: QTime) -> None: 400 hour = time.hour() 401 minute = time.minute() 402 self.prefs.day_start = '{}:{}'.format(hour, minute) 403 logging.debug("Setting day start to %s", self.prefs.day_start) 404 self.downloads_today_tracker.set_day_start(hour=hour, minute=minute) 405 406 @pyqtSlot(int) 407 def downloadsTodayChanged(self, value: int) -> None: 408 self.downloads_today_tracker.reset_downloads_today(value=value) 409 dt = self.downloads_today_tracker.downloads_today 410 logging.debug("Setting downloads today value to %s %s", dt[0], dt[1]) 411 self.prefs.downloads_today = dt 412 if self.prefs.photo_rename_pref_uses_downloads_today(): 413 self.photoRenameWidget.updateExampleFilename(downloads_today=dt) 414 if self.prefs.video_rename_pref_uses_downloads_today(): 415 self.videoRenameWidget.updateExampleFilename(downloads_today=dt) 416 417 @pyqtSlot(int) 418 def storedNumberChanged(self, value: int) -> None: 419 self.stored_sequence_no = value 420 if self.prefs.photo_rename_pref_uses_stored_sequence_no(): 421 self.photoRenameWidget.updateExampleFilename(stored_sequence_no=value - 1) 422 if self.prefs.video_rename_pref_uses_stored_sequence_no(): 423 self.videoRenameWidget.updateExampleFilename(stored_sequence_no=value - 1) 424 425 @pyqtSlot(int) 426 def syncChanged(self, state: int) -> None: 427 sync = state == Qt.Checked 428 logging.debug("Setting synchronize RAW + JPEG sequence values to %s", sync) 429 self.prefs.synchronize_raw_jpg = sync 430 431 @pyqtSlot(int) 432 def stripCharactersChanged(self, state: int) -> None: 433 strip = state == Qt.Checked 434 logging.debug("Setting strip incompatible characers to %s", strip) 435 self.prefs.strip_characters = strip 436 437 438class RenamePanel(QScrollArea): 439 """ 440 Renaming preferences widget, for photos, videos, and general 441 renaming options. 442 """ 443 444 def __init__(self, parent) -> None: 445 super().__init__(parent) 446 if parent is not None: 447 self.rapidApp = parent 448 self.prefs = self.rapidApp.prefs 449 else: 450 self.prefs = None 451 452 self.setFrameShape(QFrame.NoFrame) 453 454 self.photoRenamePanel = QPanelView( 455 label=_('Photo Renaming'), headerColor=QColor(ThumbnailBackgroundName), 456 headerFontColor=QColor(Qt.white) 457 ) 458 self.videoRenamePanel = QPanelView( 459 label=_('Video Renaming'), headerColor=QColor(ThumbnailBackgroundName), 460 headerFontColor=QColor(Qt.white) 461 ) 462 self.renameOptionsPanel = QPanelView( 463 label=_('Renaming Options'), headerColor=QColor(ThumbnailBackgroundName), 464 headerFontColor=QColor(Qt.white) 465 ) 466 self.photoRenameWidget = RenameWidget( 467 preset_type=PresetPrefType.preset_photo_rename, prefs=self.prefs, parent=self, 468 exiftool_process=self.rapidApp.exiftool_process 469 ) 470 self.photoRenamePanel.addWidget(self.photoRenameWidget) 471 self.videoRenameWidget = RenameWidget( 472 preset_type=PresetPrefType.preset_video_rename, prefs=self.prefs, parent=self, 473 exiftool_process=self.rapidApp.exiftool_process 474 ) 475 self.videoRenamePanel.addWidget(self.videoRenameWidget) 476 477 self.renameOptions = RenameOptionsWidget( 478 prefs=self.prefs, parent=self, photoRenameWidget=self.photoRenameWidget, 479 videoRenameWidget=self.videoRenameWidget 480 ) 481 self.renameOptionsPanel.addWidget(self.renameOptions) 482 483 widget = QWidget() 484 layout = QVBoxLayout() 485 layout.setContentsMargins(0, 0, 0, 0) 486 widget.setLayout(layout) 487 layout.addWidget(self.photoRenamePanel) 488 layout.addWidget(self.videoRenamePanel) 489 layout.addWidget(self.renameOptionsPanel) 490 self.setWidget(widget) 491 self.setWidgetResizable(True) 492 self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) 493 494 def updateSequences(self, downloads_today: List[str], stored_sequence_no: int) -> None: 495 """ 496 Update the value displayed in the display to reflect any values changed after 497 the completion of a download. 498 499 :param downloads_today: new downloads today value 500 :param stored_sequence_no: new stored sequence number value 501 """ 502 503 self.renameOptions.downloadsToday.setValue(int(downloads_today[1])) 504 self.renameOptions.downloads_today_tracker.downloads_today = downloads_today 505 self.renameOptions.storedNumber.setValue(stored_sequence_no + 1) 506 507 def setSamplePhoto(self, sample_photo: Photo) -> None: 508 self.photoRenameWidget.updateSampleFile(sample_rpd_file=sample_photo) 509 510 def setSampleVideo(self, sample_video: Video) -> None: 511 self.videoRenameWidget.updateSampleFile(sample_rpd_file=sample_video) 512 513 514 515