# Copyright (C) 2017 Damon Lynch # This file is part of Rapid Photo Downloader. # # Rapid Photo Downloader is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Rapid Photo Downloader is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Rapid Photo Downloader. If not, # see . """ Combobox widget to easily choose file locations """ __author__ = 'Damon Lynch' __copyright__ = "Copyright 2017, Damon Lynch" from typing import Optional, Dict, Tuple, Union, List import os import logging from collections import defaultdict from gettext import gettext as _ from PyQt5.QtCore import (pyqtSlot, pyqtSignal) from PyQt5.QtWidgets import (QComboBox, QFileDialog) from PyQt5.QtGui import (QIcon, ) import raphodo.qrc_resources as qrc_resources from raphodo.constants import StandardFileLocations, FileType, max_remembered_destinations from raphodo.preferences import Preferences from raphodo.storage import (xdg_desktop_directory, xdg_photos_directory, xdg_videos_directory, ValidMounts) from raphodo.utilities import make_path_end_snippets_unique class FolderCombo(QComboBox): """ Combobox widget to easily choose file locations. """ # Signal emitted whenever user chooses a path pathChosen = pyqtSignal(str) def __init__(self, parent, prefs: Preferences, file_type: FileType, file_chooser_title: str, special_dirs: Optional[Tuple[StandardFileLocations]]=None, valid_mounts: ValidMounts=None) -> None: super().__init__(parent) self.prefs = prefs self.file_chooser_title = file_chooser_title self.file_type = file_type self.valid_mounts = valid_mounts self.special_dirs = special_dirs # Flag to indicate whether the combo box is displaying a path error self.invalid_path = False self.activated.connect(self.processPath) self._setup_entries() def _setup_entries(self) -> None: logging.debug("Rebuilding %s combobox entries...", self.file_type.name) # Track where the remembered destinations (paths) are in the pop up menu # -1 indicates there are none. self.destinations_start = -1 # Home directory home_dir = os.path.expanduser('~') home_label = os.path.basename(home_dir) # Desktop directory, if it exists desktop_dir = xdg_desktop_directory(home_on_failure=False) if desktop_dir is not None and os.path.isdir(desktop_dir): desktop_label = os.path.basename(desktop_dir) else: desktop_label = None # Any external mounts mounts = () if self.valid_mounts is not None: mounts = tuple( ( (mount.name(), mount.rootPath()) for mount in self.valid_mounts.mountedValidMountPoints() ) ) # Pictures and Videos directories, if required and if they exist pictures_dir = pictures_label = videos_dir = videos_label = None if self.special_dirs is not None: for dir in self.special_dirs: if dir == StandardFileLocations.pictures: pictures_dir = xdg_photos_directory(home_on_failure=False) if pictures_dir is not None and os.path.isdir(pictures_dir): pictures_label = os.path.basename(pictures_dir) elif dir == StandardFileLocations.videos: videos_dir = xdg_videos_directory(home_on_failure=False) if videos_dir is not None and os.path.isdir(videos_dir): videos_label = os.path.basename(videos_dir) self.addItem(QIcon(':/icons/home.svg'), home_label, home_dir) idx = 1 if desktop_label: self.addItem(QIcon(':/icons/desktop.svg'), desktop_label, desktop_dir) idx += 1 self.addItem(QIcon(':/icons/drive-harddisk.svg'), _('File System'), '/') idx += 1 if mounts: for name, path in mounts: self.addItem(QIcon(':icons/drive-removable-media.svg'), name, path) idx += 1 if pictures_label is not None or videos_label is not None: self.insertSeparator(idx) idx += 1 if pictures_label is not None: self.addItem(QIcon(':/icons/pictures-folder.svg'), pictures_label, pictures_dir) idx += 1 if videos_label is not None: self.addItem(QIcon(':/icons/videos-folder.svg'), videos_label, videos_dir) idx += 1 # Remembered paths / destinations dests = self._get_dests() valid_dests = [dest for dest in dests if dest and os.path.isdir(dest)] if valid_dests: valid_names = make_path_end_snippets_unique(*valid_dests) else: valid_names = [] if valid_names: folder_icon = QIcon(':/icons/folder.svg') self.insertSeparator(idx) idx += 1 self.destinations_start = idx for name, path in zip(valid_names, valid_dests): self.addItem(folder_icon, name, path) idx += 1 self.insertSeparator(idx) idx += 1 self.addItem(_('Other...')) logging.debug("...%s combobox entries added", self.count()) def showPopup(self) -> None: """ Refresh the combobox menu each time the menu is shown, to handle adding or removing of external volumes or default directories """ self.refreshFolderList() super().showPopup() def refreshFolderList(self) -> None: """ Refresh the combobox to reflect any file system changes """ self.clear() self._setup_entries() self.setPath(self.chosen_path) def setPath(self, path: str) -> None: """ Set the path displayed in the combo box. This must be called for the combobox to function properly. :param path: the path to display """ self.chosen_path = path invalid = False dests = self._get_dests() standard_path = False if self.destinations_start == -1: # Deduct two from the count, to allow for the "Other..." at the end, along with its # separator default_end = self.count() - 2 else: default_end = self.destinations_start if self.invalid_path: default_start = 2 else: default_start = 0 for i in range(default_start, default_end): if self.itemData(i) == path: self.setCurrentIndex(i) standard_path = True logging.info("%s path %s is a default value or path to an external volume", self.file_type.name, path) break if standard_path: if path in dests: logging.info("Removing %s from list of stored %s destinations because its now a " "standard path", path, self.file_type.name) self.prefs.del_list_value(self._get_dest_pref_key(), path) else: valid_dests = [dest for dest in dests if dest and os.path.isdir(dest)] if path in valid_dests: self._make_dest_active(path, len(valid_dests)) elif os.path.isdir(path): # Add path to destinations in prefs, and regenerate the combobox entries self.prefs.add_list_value(self._get_dest_pref_key(), path, max_list_size=max_remembered_destinations) self.clear() self._setup_entries() # List may or may not have grown in size dests = self._get_dests() valid_dests = [dest for dest in dests if dest and os.path.isdir(dest)] self._make_dest_active(path, len(valid_dests)) else: invalid = True # Translators: indicate in combobox that a path does not exist self.insertItem(0, QIcon(':icons/error.svg'), _('%s (location does not exist)') % os.path.basename(path), path) self.setCurrentIndex(0) if self.destinations_start != -1: self.destinations_start += 1 self.invalid_path = invalid def _make_dest_active(self, path: str, dest_len: int) -> None: """ Make the path be the displayed value in the combobox **Key assumption**: the path is NOT one of the default paths or a path to an external volume :param path: the path to display :param dest_len: remembered paths (destinations) list length """ for j in range(self.destinations_start, self.destinations_start + dest_len): if self.itemData(j) == path: self.setCurrentIndex(j) break def _get_dests(self) -> List[str]: if self.file_type == FileType.photo: return self.prefs.photo_backup_destinations else: return self.prefs.video_backup_destinations def _get_dest_pref_key(self) -> str: if self.file_type == FileType.photo: return 'photo_backup_destinations' else: return 'video_backup_destinations' @pyqtSlot(int) def processPath(self, index: int) -> None: """Handle the path that the user has chosen via the combo box""" if index == self.count() - 1: try: if os.path.isdir(self.chosen_path): chosen_path = self.chosen_path else: chosen_path = os.path.expanduser('~') except AttributeError: chosen_path = os.path.expanduser('~') path = QFileDialog.getExistingDirectory(self, self.file_chooser_title, chosen_path, QFileDialog.ShowDirsOnly) if path: self.setPath(path) self.pathChosen.emit(path) else: self.setPath(chosen_path) else: path = self.itemData(index) self.setPath(path) self.pathChosen.emit(path)