diff options
Diffstat (limited to 'raphodo/foldercombo.py')
-rw-r--r-- | raphodo/foldercombo.py | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/raphodo/foldercombo.py b/raphodo/foldercombo.py new file mode 100644 index 0000000..4bc182c --- /dev/null +++ b/raphodo/foldercombo.py @@ -0,0 +1,297 @@ +# Copyright (C) 2017 Damon Lynch <damonlynch@gmail.com> + +# 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 <http://www.gnu.org/licenses/>. + +""" +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) + + + + + |