1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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)
|