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 system folders and allow the user to select one 21""" 22 23__author__ = 'Damon Lynch' 24__copyright__ = "Copyright 2016-2020, Damon Lynch" 25 26import os 27import pathlib 28from typing import List, Set 29import logging 30import shlex 31import subprocess 32 33from PyQt5.QtCore import ( 34 QDir, Qt, QModelIndex, QItemSelectionModel, QSortFilterProxyModel, QPoint, QSize 35) 36from PyQt5.QtWidgets import ( 37 QTreeView, QAbstractItemView, QFileSystemModel, QSizePolicy, QStyledItemDelegate, 38 QStyleOptionViewItem, QMenu 39) 40 41from PyQt5.QtGui import QPainter, QFont 42 43import raphodo.qrc_resources as qrc_resources 44from raphodo.constants import minPanelWidth, minFileSystemViewHeight, Roles 45from raphodo.storage import gvfs_gphoto2_path 46from raphodo.viewutils import scaledIcon, standard_font_size 47 48 49class FileSystemModel(QFileSystemModel): 50 """ 51 Use Qt's built-in functionality to model the file system. 52 53 Augment it by displaying provisional subfolders in the photo and video 54 download destinations. 55 """ 56 57 def __init__(self, parent) -> None: 58 super().__init__(parent) 59 60 # More filtering done in the FileSystemFilter 61 self.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot ) 62 63 self.folder_icon = scaledIcon(':/icons/folder.svg') 64 self.download_folder_icon = scaledIcon(':/icons/folder-filled.svg') 65 66 self.setRootPath('/') 67 68 # The next two values are set via FolderPreviewManager.update() 69 # They concern provisional folders that will be used if the 70 # download proceeds, and all files are downloaded. 71 72 # First value: subfolders we've created to demonstrate to the user 73 # where their files will be downloaded to 74 self.preview_subfolders = set() # type: Set[str] 75 # Second value: subfolders that already existed, but that we still 76 # want to indicate to the user where their files will be downloaded to 77 self.download_subfolders = set() # type: Set[str] 78 79 # Folders that were actually used to download files into 80 self.subfolders_downloaded_into = set() # type: Set[str] 81 82 def data(self, index: QModelIndex, role=Qt.DisplayRole): 83 if role == Qt.DecorationRole: 84 path = index.data(QFileSystemModel.FilePathRole) # type: str 85 if path in self.download_subfolders or path in self.subfolders_downloaded_into: 86 return self.download_folder_icon 87 else: 88 return self.folder_icon 89 if role == Roles.folder_preview: 90 path = index.data(QFileSystemModel.FilePathRole) 91 return path in self.preview_subfolders and path not in self.subfolders_downloaded_into 92 93 return super().data(index, role) 94 95 def add_subfolder_downloaded_into(self, path: str, download_folder: str) -> bool: 96 """ 97 Add a path to the set of subfolders that indicate where files where 98 downloaded. 99 :param path: the full path to the folder 100 :return: True if the path was not added before, else False 101 """ 102 103 if path not in self.subfolders_downloaded_into: 104 self.subfolders_downloaded_into.add(path) 105 106 pl_subfolders = pathlib.Path(path) 107 pl_download_folder = pathlib.Path(download_folder) 108 109 for subfolder in pl_subfolders.parents: 110 if not pl_download_folder in subfolder.parents: 111 break 112 self.subfolders_downloaded_into.add(str(subfolder)) 113 return True 114 return False 115 116 117class FileSystemView(QTreeView): 118 def __init__(self, model: FileSystemModel, rapidApp, parent=None) -> None: 119 super().__init__(parent) 120 self.rapidApp = rapidApp 121 self.fileSystemModel = model 122 self.setHeaderHidden(True) 123 self.setSelectionBehavior(QAbstractItemView.SelectRows) 124 self.setSelectionMode(QAbstractItemView.SingleSelection) 125 self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) 126 self.setMinimumWidth(minPanelWidth()) 127 self.setMinimumHeight(minFileSystemViewHeight()) 128 self.setContextMenuPolicy(Qt.CustomContextMenu) 129 self.customContextMenuRequested.connect(self.onCustomContextMenu) 130 self.contextMenu = QMenu() 131 self.openInFileBrowserAct = self.contextMenu.addAction(_('Open in File Browser...')) 132 self.openInFileBrowserAct.triggered.connect(self.doOpenInFileBrowserAct) 133 self.openInFileBrowserAct.setEnabled(self.rapidApp.file_manager is not None) 134 self.clickedIndex = None # type: QModelIndex 135 136 def hideColumns(self) -> None: 137 """ 138 Call only after the model has been initialized 139 """ 140 for i in (1, 2, 3): 141 self.hideColumn(i) 142 143 def goToPath(self, path: str, scrollTo: bool=True) -> None: 144 """ 145 Select the path, expand its subfolders, and scroll to it 146 :param path: 147 :return: 148 """ 149 if not path: 150 return 151 index = self.model().mapFromSource(self.fileSystemModel.index(path)) 152 self.setExpanded(index, True) 153 selection = self.selectionModel() 154 selection.select(index, QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows) 155 if scrollTo: 156 self.scrollTo(index, QAbstractItemView.PositionAtTop) 157 158 def expandPreviewFolders(self, path: str) -> bool: 159 """ 160 Expand any unexpanded preview folders 161 162 :param path: path under which to expand folders 163 :return: True if path was expanded, else False 164 """ 165 166 self.goToPath(path, scrollTo=True) 167 if not path: 168 return False 169 170 expanded = False 171 for path in self.fileSystemModel.download_subfolders: 172 # print('path', path) 173 index = self.model().mapFromSource(self.fileSystemModel.index(path)) 174 if not self.isExpanded(index): 175 self.expand(index) 176 expanded = True 177 return expanded 178 179 def expandPath(self, path) -> None: 180 index = self.model().mapFromSource(self.fileSystemModel.index(path)) 181 if not self.isExpanded(index): 182 self.expand(index) 183 184 def onCustomContextMenu(self, point: QPoint) -> None: 185 index = self.indexAt(point) 186 if index.isValid(): 187 self.clickedIndex = index 188 self.contextMenu.exec(self.mapToGlobal(point)) 189 190 def doOpenInFileBrowserAct(self): 191 index = self.clickedIndex 192 if index: 193 uri = self.fileSystemModel.filePath(index.model().mapToSource(index)) 194 cmd = '{} "{}"'.format(self.rapidApp.file_manager, uri) 195 logging.debug("Launching: %s", cmd) 196 args = shlex.split(cmd) 197 subprocess.Popen(args) 198 199 200class FileSystemFilter(QSortFilterProxyModel): 201 """ 202 Filter out the display of RPD's cache and temporary directories 203 """ 204 205 def __init__(self, parent=None): 206 super().__init__(parent) 207 self.filtered_dir_names = set() 208 209 def setTempDirs(self, dirs: List[str]) -> None: 210 filters = [os.path.basename(path) for path in dirs] 211 self.filtered_dir_names = self.filtered_dir_names | set(filters) 212 self.invalidateFilter() 213 214 def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex=None) -> bool: 215 index = self.sourceModel().index(sourceRow, 0, sourceParent) # type: QModelIndex 216 path = index.data(QFileSystemModel.FilePathRole) # type: str 217 218 if gvfs_gphoto2_path(path): 219 logging.debug("Rejecting browsing path %s", path) 220 return False 221 222 if not self.filtered_dir_names: 223 return True 224 225 file_name = index.data(QFileSystemModel.FileNameRole) 226 return file_name not in self.filtered_dir_names 227 228 229class FileSystemDelegate(QStyledItemDelegate): 230 """ 231 Italicize provisional download folders that were not already created 232 """ 233 234 def __init__(self, parent=None): 235 super().__init__(parent) 236 237 def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None: 238 if index is None: 239 return 240 241 folder_preview = index.data(Roles.folder_preview) 242 if folder_preview: 243 font = QFont() 244 font.setItalic(True) 245 option.font = font 246 247 super().paint(painter, option, index) 248