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