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