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