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 download destination details
21"""
22
23__author__ = 'Damon Lynch'
24__copyright__ = "Copyright 2016-2020, Damon Lynch"
25
26import os
27import math
28from typing import Optional, Dict, Tuple, Union, DefaultDict, Set
29import logging
30from collections import defaultdict
31
32from PyQt5.QtCore import QSize, Qt, QStorageInfo, QRect, pyqtSlot, QPoint
33from PyQt5.QtWidgets import (
34    QStyleOptionFrame, QStyle, QStylePainter, QWidget, QSplitter, QSizePolicy, QAction, QMenu,
35    QActionGroup
36)
37from PyQt5.QtGui import QColor, QPixmap, QIcon, QPaintEvent, QPalette, QMouseEvent
38
39
40from raphodo.devicedisplay import DeviceDisplay, BodyDetails, icon_size
41from raphodo.storage import (StorageSpace, get_path_display_name, get_mount_size)
42from raphodo.constants import (
43    CustomColors, DestinationDisplayType, DisplayingFilesOfType, DestinationDisplayMousePos,
44    PresetPrefType, NameGenerationType, DestinationDisplayTooltipState, FileType
45)
46from raphodo.utilities import thousands, format_size_for_user
47from raphodo.rpdfile import FileTypeCounter, Photo, Video
48from raphodo.nameeditor import PrefDialog, make_subfolder_menu_entry
49import raphodo.generatenameconfig as gnc
50from raphodo.generatenameconfig import *
51
52
53def make_body_details(bytes_total: int,
54                      bytes_free: int,
55                      files_to_display: DisplayingFilesOfType,
56                      marked: FileTypeCounter,
57                      photos_size_to_download: int,
58                      videos_size_to_download: int) -> BodyDetails:
59    """
60    Gather the details to render for destination storage usage
61    for photo and video downloads, and their backups.
62
63    :param bytes_total:
64    :param bytes_free:
65    :param files_to_display:
66    :param marked:
67    :param photos_size_to_download:
68    :param videos_size_to_download:
69    :return:
70    """
71
72    bytes_total_text = format_size_for_user(bytes_total, no_decimals=0)
73    existing_bytes = bytes_total - bytes_free
74    existing_size = format_size_for_user(existing_bytes)
75
76    photos = videos = photos_size = videos_size = ''
77
78    if files_to_display != DisplayingFilesOfType.videos:
79        # Translators: %(variable)s represents Python code, not a plural of the term
80        # variable. You must keep the %(variable)s untranslated, or the program will
81        # crash.
82        photos = _('%(no_photos)s Photos') % {'no_photos':
83                                                  thousands(marked[FileType.photo])}
84        photos_size = format_size_for_user(photos_size_to_download)
85    if files_to_display != DisplayingFilesOfType.photos:
86        # Translators: %(variable)s represents Python code, not a plural of the term
87        # variable. You must keep the %(variable)s untranslated, or the program will
88        # crash.
89        videos = _('%(no_videos)s Videos') % {'no_videos':
90                                                  thousands(marked[FileType.video])}
91        videos_size = format_size_for_user(videos_size_to_download)
92
93    size_to_download = photos_size_to_download + videos_size_to_download
94    comp1_file_size_sum = photos_size_to_download
95    comp2_file_size_sum = videos_size_to_download
96    comp3_file_size_sum = existing_bytes
97    comp1_text = photos
98    comp2_text = videos
99    comp3_text = _('Used')
100    comp4_text = _('Excess')
101    comp1_size_text = photos_size
102    comp2_size_text = videos_size
103    comp3_size_text = existing_size
104
105    bytes_to_use = size_to_download + existing_bytes
106    percent_used = ''
107
108    if bytes_total == 0:
109        bytes_free_of_total = _('Device size unknown')
110        comp4_file_size_sum = 0
111        comp4_size_text = 0
112        comp3_size_text = 0
113    elif bytes_to_use > bytes_total:
114        bytes_total_ = bytes_total
115        bytes_total = bytes_to_use
116        excess_bytes = bytes_to_use - bytes_total_
117        comp4_file_size_sum = excess_bytes
118        comp4_size_text = format_size_for_user(excess_bytes)
119        # Translators: %(variable)s represents Python code, not a plural of the term
120        # variable. You must keep the %(variable)s untranslated, or the program will
121        # crash.
122        bytes_free_of_total = _('No space free on %(size_total)s device') % dict(
123            size_total=bytes_total_text
124        )
125    else:
126        comp4_file_size_sum = 0
127        comp4_size_text = 0
128        bytes_free = bytes_total - bytes_to_use
129        # Translators: %(variable)s represents Python code, not a plural of the term
130        # variable. You must keep the %(variable)s untranslated, or the program will
131        # crash.
132        bytes_free_of_total = _('%(size_free)s free of %(size_total)s') % dict(
133            size_free=format_size_for_user(bytes_free, no_decimals=1),
134            size_total=bytes_total_text
135        )
136
137    return BodyDetails(
138        bytes_total_text=bytes_total_text,
139        bytes_total=bytes_total,
140        percent_used_text=percent_used,
141        bytes_free_of_total=bytes_free_of_total,
142        comp1_file_size_sum=comp1_file_size_sum,
143        comp2_file_size_sum=comp2_file_size_sum,
144        comp3_file_size_sum=comp3_file_size_sum,
145        comp4_file_size_sum=comp4_file_size_sum,
146        comp1_text=comp1_text,
147        comp2_text=comp2_text,
148        comp3_text=comp3_text,
149        comp4_text=comp4_text,
150        comp1_size_text=comp1_size_text,
151        comp2_size_text=comp2_size_text,
152        comp3_size_text=comp3_size_text,
153        comp4_size_text=comp4_size_text,
154        color1=QColor(CustomColors.color1.value),
155        color2=QColor(CustomColors.color2.value),
156        color3=QColor(CustomColors.color3.value),
157        displaying_files_of_type=files_to_display
158    )
159
160def adjusted_download_size(photos_size_to_download: int,
161                           videos_size_to_download: int,
162                           os_stat_device: int,
163                           downloading_to) -> Tuple[int, int]:
164    """
165    Adjust download size to account for situations where
166    photos and videos are being backed up to the same
167    partition (device) they're downloaded to.
168
169    :return: photos_size_to_download, videos_size_to_download
170    """
171    if os_stat_device in downloading_to:
172        file_types = downloading_to[os_stat_device]
173        if FileType.photo in file_types:
174            photos_size_to_download = photos_size_to_download * 2
175        if FileType.video in file_types:
176            videos_size_to_download = videos_size_to_download * 2
177    return photos_size_to_download, videos_size_to_download
178
179
180class DestinationDisplay(QWidget):
181    """
182    Custom widget handling the display of download destinations, not including the file system
183    browsing component.
184
185    Serves a dual purpose, depending on whether photos and videos are being downloaded
186    to the same file system or not:
187
188    1. Display how much storage space the checked files will use in addition
189       to the space used by existing files.
190
191    2. Display the download destination (path), and a local menu to control subfolder
192       generation.
193
194    Where photos and videos are being downloaded to the same file system, the storage space display
195    is combined into one widget, which appears in its own panel above the photo and video
196    destination panels.
197
198    Where photos and videos are being downloaded to different file systems, the combined
199    display (above) is invisible, and photo and video panels have the own section in which
200    to display their storage space display
201    """
202
203    photos = _('Photos')
204    videos = _('Videos')
205    projected_space_msg = _('Projected storage use after download')
206
207    def __init__(self, menu: bool=False,
208                 file_type: FileType=None,
209                 parent=None) -> None:
210        """
211        :param menu: whether to render a drop down menu
212        :param file_type: whether for photos or videos. Relevant only for menu display.
213        """
214
215        super().__init__(parent)
216        self.rapidApp = parent
217        if parent is not None:
218            self.prefs = self.rapidApp.prefs
219        else:
220            self.prefs = None
221
222        self.storage_space = None  # type: StorageSpace
223
224        self.map_action = dict()  # type: Dict[int, QAction]
225
226        if menu:
227            menuIcon = QIcon(':/icons/settings.svg')
228            self.file_type = file_type
229            self.createActionsAndMenu()
230            self.mouse_pos = DestinationDisplayMousePos.normal
231            self.tooltip_display_state = DestinationDisplayTooltipState.path
232        else:
233            menuIcon = None
234            self.menu = None
235            self.mouse_pos = None
236            self.tooltip_display_state = None
237
238        self.deviceDisplay = DeviceDisplay(menuButtonIcon=menuIcon)
239        size = icon_size()
240        self.icon = QIcon(':/icons/folder.svg').pixmap(QSize(size, size))  # type: QPixmap
241        self.display_name = ''
242        self.photos_size_to_download = self.videos_size_to_download = 0
243        self.files_to_display = None   # type: DisplayingFilesOfType
244        self.marked = FileTypeCounter()
245        self.display_type = None  # type: DestinationDisplayType
246        self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
247
248        # default number of built-in subfolder generation defaults
249        self.no_builtin_defaults = 5
250        self.max_presets = 5
251        # maximum number of menu entries showing name generation presets
252        self.max_menu_entries = self.no_builtin_defaults + self.max_presets
253
254        self.sample_rpd_file = None  # type: Union[Photo, Video]
255
256        self.os_stat_device = 0  # type: int
257        self._downloading_to = defaultdict(list)  # type: DefaultDict[int, Set[FileType]]
258
259    @property
260    def downloading_to(self) -> DefaultDict[int, Set[FileType]]:
261        return self._downloading_to
262
263    @downloading_to.setter
264    def downloading_to(self, downloading_to) -> None:
265        if downloading_to is not None:
266            self._downloading_to = downloading_to
267            # TODO determine if this is always needed here
268            self.update()
269
270    def createActionsAndMenu(self) -> None:
271        self.setMouseTracking(True)
272        self.menu = QMenu()
273
274        if self.file_type == FileType.photo:
275            defaults = gnc.PHOTO_SUBFOLDER_MENU_DEFAULTS
276        else:
277            defaults = gnc.VIDEO_SUBFOLDER_MENU_DEFAULTS
278
279        self.subfolder0Act = QAction(
280            make_subfolder_menu_entry(defaults[0]),
281            self,
282            checkable=True,
283            triggered=self.doSubfolder0
284        )
285        self.subfolder1Act = QAction(
286            make_subfolder_menu_entry(defaults[1]),
287            self,
288            checkable=True,
289            triggered=self.doSubfolder1
290        )
291        self.subfolder2Act = QAction(
292            make_subfolder_menu_entry(defaults[2]),
293            self,
294            checkable=True,
295            triggered=self.doSubfolder2
296        )
297        self.subfolder3Act = QAction(
298            make_subfolder_menu_entry(defaults[3]),
299            self,
300            checkable=True,
301            triggered=self.doSubfolder3
302        )
303        self.subfolder4Act = QAction(
304            make_subfolder_menu_entry(defaults[4]),
305            self,
306            checkable=True,
307            triggered=self.doSubfolder4
308        )
309        self.subfolder5Act = QAction(
310            'Preset 0',
311            self,
312            checkable=True,
313            triggered=self.doSubfolder5
314        )
315        self.subfolder6Act = QAction(
316            'Preset 1',
317            self,
318            checkable=True,
319            triggered=self.doSubfolder6
320        )
321        self.subfolder7Act = QAction(
322            'Preset 2',
323            self,
324            checkable=True,
325            triggered=self.doSubfolder7
326        )
327        self.subfolder8Act = QAction(
328            'Preset 3',
329            self,
330            checkable=True,
331            triggered=self.doSubfolder8
332        )
333        self.subfolder9Act = QAction(
334            'Preset 4',
335            self,
336            checkable=True,
337            triggered=self.doSubfolder9
338        )
339        # Translators: Custom refers to the user choosing a non-default value that
340        # they customize themselves
341        self.subfolderCustomAct = QAction(
342            _('Custom...'),
343            self,
344            checkable=True,
345            triggered=self.doSubfolderCustom
346        )
347
348        self.subfolderGroup = QActionGroup(self)
349
350        self.subfolderGroup.addAction(self.subfolder0Act)
351        self.subfolderGroup.addAction(self.subfolder1Act)
352        self.subfolderGroup.addAction(self.subfolder2Act)
353        self.subfolderGroup.addAction(self.subfolder3Act)
354        self.subfolderGroup.addAction(self.subfolder4Act)
355        self.subfolderGroup.addAction(self.subfolder5Act)
356        self.subfolderGroup.addAction(self.subfolder6Act)
357        self.subfolderGroup.addAction(self.subfolder7Act)
358        self.subfolderGroup.addAction(self.subfolder8Act)
359        self.subfolderGroup.addAction(self.subfolder9Act)
360        self.subfolderGroup.addAction(self.subfolderCustomAct)
361
362        self.menu.addAction(self.subfolder0Act)
363        self.menu.addAction(self.subfolder1Act)
364        self.menu.addAction(self.subfolder2Act)
365        self.menu.addAction(self.subfolder3Act)
366        self.menu.addAction(self.subfolder4Act)
367        self.menu.addSeparator()
368        self.menu.addAction(self.subfolder5Act)
369        self.menu.addAction(self.subfolder6Act)
370        self.menu.addAction(self.subfolder7Act)
371        self.menu.addAction(self.subfolder8Act)
372        self.menu.addAction(self.subfolder9Act)
373        self.menu.addAction(self.subfolderCustomAct)
374
375        self.map_action[0] = self.subfolder0Act
376        self.map_action[1] = self.subfolder1Act
377        self.map_action[2] = self.subfolder2Act
378        self.map_action[3] = self.subfolder3Act
379        self.map_action[4] = self.subfolder4Act
380        self.map_action[5] = self.subfolder5Act
381        self.map_action[6] = self.subfolder6Act
382        self.map_action[7] = self.subfolder7Act
383        self.map_action[8] = self.subfolder8Act
384        self.map_action[9] = self.subfolder9Act
385        self.map_action[-1] = self.subfolderCustomAct
386
387    def presetType(self) -> PresetPrefType:
388        if self.file_type == FileType.photo:
389            return PresetPrefType.preset_photo_subfolder
390        else:
391            return PresetPrefType.preset_video_subfolder
392
393    def _cacheCustomPresetValues(self) -> int:
394        """
395        Get custom photo or video presets, and assign them to class members
396        :return: index into the combo of default prefs + custom prefs
397        """
398        preset_type = self.presetType()
399        self.preset_names, self.preset_pref_lists = self.prefs.get_preset(preset_type=preset_type)
400
401        if self.file_type == FileType.photo:
402            index = self.prefs.photo_subfolder_index(self.preset_pref_lists)
403        else:
404            index = self.prefs.video_subfolder_index(self.preset_pref_lists)
405        return index
406
407    def setupMenuActions(self) -> None:
408        index = self._cacheCustomPresetValues()
409
410        action = self.map_action[index]  # type: QAction
411        action.setChecked(True)
412
413        # Set visibility of custom presets menu items to match how many we are displaying
414        for idx, text in enumerate(self.preset_names[:self.max_presets]):
415            action = self.map_action[self.no_builtin_defaults + idx]
416            action.setText(text)
417            action.setVisible(True)
418
419        for i in range(self.max_presets - min(len(self.preset_names), self.max_presets)):
420            idx = len(self.preset_names) + self.no_builtin_defaults + i
421            action = self.map_action[idx]
422            action.setVisible(False)
423
424    def doSubfolder0(self) -> None:
425        self.menuItemChosen(0)
426
427    def doSubfolder1(self) -> None:
428        self.menuItemChosen(1)
429
430    def doSubfolder2(self) -> None:
431        self.menuItemChosen(2)
432
433    def doSubfolder3(self) -> None:
434        self.menuItemChosen(3)
435
436    def doSubfolder4(self) -> None:
437        self.menuItemChosen(4)
438
439    def doSubfolder5(self) -> None:
440        self.menuItemChosen(5)
441
442    def doSubfolder6(self) -> None:
443        self.menuItemChosen(6)
444
445    def doSubfolder7(self) -> None:
446        self.menuItemChosen(7)
447
448    def doSubfolder8(self) -> None:
449        self.menuItemChosen(8)
450
451    def doSubfolder9(self) -> None:
452        self.menuItemChosen(9)
453
454    def doSubfolderCustom(self):
455        self.menuItemChosen(-1)
456
457    def menuItemChosen(self, index: int) -> None:
458        self.mouse_pos = DestinationDisplayMousePos.normal
459        self.update()
460
461        user_pref_list = None
462
463        if index == -1:
464            if self.file_type == FileType.photo:
465                pref_defn = DICT_SUBFOLDER_L0
466                pref_list = self.prefs.photo_subfolder
467                generation_type = NameGenerationType.photo_subfolder
468            else:
469                pref_defn = DICT_VIDEO_SUBFOLDER_L0
470                pref_list = self.prefs.video_subfolder
471                generation_type = NameGenerationType.video_subfolder
472
473            prefDialog = PrefDialog(
474                pref_defn=pref_defn, user_pref_list=pref_list, generation_type=generation_type,
475                prefs=self.prefs, sample_rpd_file=self.sample_rpd_file,
476                max_entries=self.max_menu_entries
477            )
478            if prefDialog.exec():
479                user_pref_list = prefDialog.getPrefList()
480                if not user_pref_list:
481                    user_pref_list = None
482
483        elif index >= self.no_builtin_defaults:
484            assert index < self.max_menu_entries
485            user_pref_list = self.preset_pref_lists[index - self.no_builtin_defaults]
486
487        else:
488            if self.file_type == FileType.photo:
489                user_pref_list = gnc.PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[index]
490            else:
491                user_pref_list = gnc.VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV[index]
492
493        if user_pref_list is not None:
494            logging.debug("Updating %s subfolder generation preference value", self.file_type.name)
495            if self.file_type == FileType.photo:
496                self.prefs.photo_subfolder = user_pref_list
497            else:
498                self.prefs.video_subfolder = user_pref_list
499            self.rapidApp.folder_preview_manager.change_subfolder_structure()
500
501    def setDestination(self, path: str) -> None:
502        """
503        Set the downloaded destination path
504        :param path: valid path
505        """
506
507        self.display_name, self.path = get_path_display_name(path)
508        try:
509            self.os_stat_device = os.stat(path).st_dev
510        except FileNotFoundError:
511            logging.error('Cannot set download destination display: %s does not exist', path)
512            self.os_stat_device = 0
513
514        mount = QStorageInfo(path)
515        bytes_total, bytes_free = get_mount_size(mount=mount)
516
517        self.storage_space = StorageSpace(bytes_free=bytes_free, bytes_total=bytes_total, path=path)
518
519    def setDownloadAttributes(self, marked: FileTypeCounter,
520                              photos_size: int,
521                              videos_size: int,
522                              files_to_display: DisplayingFilesOfType,
523                              display_type: DestinationDisplayType,
524                              merge: bool) -> None:
525        """
526        Set the attributes used to generate the visual display of the
527        files marked to be downloaded
528
529        :param marked: number and type of files marked for download
530        :param photos_size: size in bytes of photos marked for download
531        :param videos_size: size in bytes of videos marked for download
532        :param files_to_display: whether displaying photos or videos or both
533        :param display_type: whether showing only the header (folder only),
534         usage only, or both
535        :param merge: whether to replace or add to the current values
536        """
537
538        if not merge:
539            self.marked = marked
540            self.photos_size_to_download = photos_size
541            self.videos_size_to_download = videos_size
542        else:
543            self.marked.update(marked)
544            self.photos_size_to_download += photos_size
545            self.videos_size_to_download += videos_size
546
547        self.files_to_display = files_to_display
548
549        self.display_type = display_type
550
551        if self.display_type != DestinationDisplayType.usage_only:
552            self.tool_tip = self.path
553        else:
554            self.tool_tip = self.projected_space_msg
555        self.setToolTip(self.tool_tip)
556
557        self.update()
558        self.updateGeometry()
559
560    def sufficientSpaceAvailable(self) -> bool:
561        """
562        Check to see that there is sufficient space with which to perform a download.
563
564        :return: True or False value if sufficient space. Will always return False if
565         the download destination is not yet set.
566        """
567
568        if self.storage_space is None:
569            return False
570
571        # allow for destinations that don't properly report their size
572        if self.storage_space.bytes_total == 0:
573            return True
574
575        photos_size_to_download, videos_size_to_download = adjusted_download_size(
576            photos_size_to_download=self.photos_size_to_download,
577            videos_size_to_download=self.videos_size_to_download,
578            os_stat_device=self.os_stat_device,
579            downloading_to=self._downloading_to)
580        return photos_size_to_download + videos_size_to_download < self.storage_space.bytes_free
581
582    def paintEvent(self, event: QPaintEvent) -> None:
583        """
584        Render the custom widget
585        """
586
587        painter = QStylePainter()
588        painter.begin(self)
589
590        x = 0
591        y = 0
592        width = self.width()
593
594        rect = self.rect()  # type: QRect
595
596        if self.display_type == DestinationDisplayType.usage_only and QSplitter().lineWidth():
597            # Draw a frame if that's what the style requires
598            option = QStyleOptionFrame()
599            option.initFrom(self)
600            painter.drawPrimitive(QStyle.PE_Frame, option)
601
602            w = QSplitter().lineWidth()
603            rect.adjust(w, w, -w, -w)
604
605        palette = QPalette()
606        backgroundColor = palette.base().color()
607        painter.fillRect(rect, backgroundColor)
608
609        if self.storage_space is None:
610            painter.end()
611            return
612
613        highlight_menu = self.mouse_pos == DestinationDisplayMousePos.menu
614
615        if self.display_type != DestinationDisplayType.usage_only:
616            # Render the folder icon, folder name, and the menu icon
617            self.deviceDisplay.paint_header(
618                painter=painter, x=x, y=y, width=width, display_name=self.display_name,
619                icon=self.icon, highlight_menu=highlight_menu
620            )
621            y = y + self.deviceDisplay.device_name_height
622
623        if self.display_type != DestinationDisplayType.folder_only:
624            # Render the projected storage space
625            if self.display_type == DestinationDisplayType.usage_only:
626                y += self.deviceDisplay.padding
627
628            photos_size_to_download, videos_size_to_download = adjusted_download_size(
629                photos_size_to_download=self.photos_size_to_download,
630                videos_size_to_download=self.videos_size_to_download,
631                os_stat_device=self.os_stat_device,
632                downloading_to=self._downloading_to
633            )
634
635            details = make_body_details(
636                bytes_total=self.storage_space.bytes_total,
637                bytes_free=self.storage_space.bytes_free,
638                files_to_display=self.files_to_display,
639                marked=self.marked,
640                photos_size_to_download=photos_size_to_download,
641                videos_size_to_download=videos_size_to_download
642            )
643
644            self.deviceDisplay.paint_body(
645                painter=painter, x=x, y=y, width=width, details=details
646            )
647
648        painter.end()
649
650    def sizeHint(self) -> QSize:
651        if self.display_type == DestinationDisplayType.usage_only:
652            height = self.deviceDisplay.padding
653        else:
654            height = 0
655
656        if self.display_type != DestinationDisplayType.usage_only:
657            height += self.deviceDisplay.device_name_height
658        if self.display_type != DestinationDisplayType.folder_only:
659            height += self.deviceDisplay.storage_height
660        return QSize(self.deviceDisplay.view_width, height)
661
662    def minimumSize(self) -> QSize:
663        return self.sizeHint()
664
665    @pyqtSlot(QMouseEvent)
666    def mousePressEvent(self, event: QMouseEvent) -> None:
667        if self.menu is None:
668            return
669
670        iconRect = self.deviceDisplay.menu_button_rect(0, 0, self.width())
671
672        if iconRect.contains(event.pos()):
673            if event.button() == Qt.LeftButton:
674                menuTopReal = iconRect.bottomLeft()
675                x = math.ceil(menuTopReal.x())
676                y = math.ceil(menuTopReal.y())
677                self.setupMenuActions()
678                self.menu.popup(self.mapToGlobal(QPoint(x, y)))
679
680    @pyqtSlot(QMouseEvent)
681    def mouseMoveEvent(self, event: QMouseEvent) -> None:
682        """
683        Sets the tooltip depending on the position of the mouse.
684        """
685
686        if self.menu is None:
687            # Relevant only for photo and video destination panels, not the combined
688            # storage space display.
689            return
690
691        if self.display_type == DestinationDisplayType.folders_and_usage:
692            # make tooltip different when hovering above storage space compared
693            # to when hovering above the destination folder
694
695            headerRect = QRect(0, 0, self.width(), self.deviceDisplay.device_name_height)
696            if not headerRect.contains(event.pos()):
697                if self.tooltip_display_state != DestinationDisplayTooltipState.storage_space:
698                    # Display tooltip for storage space
699                    self.setToolTip(self.projected_space_msg)
700                    self.tooltip_display_state = DestinationDisplayTooltipState.storage_space
701                    self.update()
702                return
703
704        iconRect = self.deviceDisplay.menu_button_rect(0, 0, self.width())
705        if iconRect.contains(event.pos()):
706            if self.mouse_pos == DestinationDisplayMousePos.normal:
707                self.mouse_pos = DestinationDisplayMousePos.menu
708
709                if self.file_type == FileType.photo:
710                    self.setToolTip(_('Configure photo subfolder creation'))
711                else:
712                    self.setToolTip(_('Configure video subfolder creation'))
713                self.tooltip_display_state = DestinationDisplayTooltipState.menu
714                self.update()
715
716        else:
717            if (self.mouse_pos == DestinationDisplayMousePos.menu or
718                    self.tooltip_display_state != DestinationDisplayTooltipState.path):
719                self.mouse_pos = DestinationDisplayMousePos.normal
720                self.setToolTip(self.tool_tip)
721                self.tooltip_display_state = DestinationDisplayTooltipState.path
722                self.update()
723
724
725
726