summaryrefslogtreecommitdiff
path: root/raphodo
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo')
-rw-r--r--raphodo/__about__.py6
-rw-r--r--raphodo/aboutdialog.py6
-rw-r--r--raphodo/constants.py14
-rw-r--r--raphodo/destinationdisplay.py5
-rw-r--r--raphodo/didyouknow.py1
-rw-r--r--raphodo/errorlog.py10
-rw-r--r--raphodo/filebrowse.py30
-rw-r--r--raphodo/generatename.py7
-rwxr-xr-xraphodo/nameeditor.py143
-rw-r--r--raphodo/preferences.py102
-rw-r--r--raphodo/proximity.py4
-rwxr-xr-xraphodo/rapid.py43
-rwxr-xr-xraphodo/renameandmovefile.py101
-rwxr-xr-xraphodo/scan.py5
-rw-r--r--raphodo/storage.py63
-rw-r--r--raphodo/thumbnaildisplay.py44
16 files changed, 387 insertions, 197 deletions
diff --git a/raphodo/__about__.py b/raphodo/__about__.py
index d0dce5f..2657dd2 100644
--- a/raphodo/__about__.py
+++ b/raphodo/__about__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2016-2018 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -29,10 +29,10 @@ __summary__ = 'Downloads, renames and backs up photos and videos from cameras, p
'memory cards and other devices'
__uri__ = 'http://www.damonlynch.net/rapid'
-__version__ = '0.9.6'
+__version__ = '0.9.7'
__author__ = 'Damon Lynch'
__email__ = 'damonlynch@gmail.com'
__license__ = 'GPL'
-__copyright__ = 'Copyright 2007-2017 {}'.format(__author__)
+__copyright__ = 'Copyright 2007-2018 {}'.format(__author__)
diff --git a/raphodo/aboutdialog.py b/raphodo/aboutdialog.py
index 0a80296..280228b 100644
--- a/raphodo/aboutdialog.py
+++ b/raphodo/aboutdialog.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2016-2018 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -21,7 +21,7 @@ Display an About window
"""
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2016-2017, Damon Lynch"
+__copyright__ = "Copyright 2016-2018, Damon Lynch"
from gettext import gettext as _
@@ -108,7 +108,7 @@ class AboutDialog(QDialog):
# Credits view
credits_text = """
- Copyright © 2007-2017 Damon Lynch.
+ Copyright © 2007-2018 Damon Lynch.
Portions copyright © 2008-2015 Canonical Ltd.
Portions copyright © 2013 Bernard Baeyens.
Portions copyright © 2012-2015 Jim Easterbrook.
diff --git a/raphodo/constants.py b/raphodo/constants.py
index dbb8aa8..f2d39d4 100644
--- a/raphodo/constants.py
+++ b/raphodo/constants.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2007-2017 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2007-2018 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -18,7 +18,7 @@
# see <http://www.gnu.org/licenses/>.
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2007-2017, Damon Lynch"
+__copyright__ = "Copyright 2007-2018, Damon Lynch"
from enum import (Enum, IntEnum)
from PyQt5.QtCore import Qt
@@ -411,7 +411,7 @@ proximity_time_steps = [5, 10, 15, 30, 45, 60, 90, 120, 180, 240, 480, 960, 1440
class TemporalProximityState(Enum):
empty = 1
- pending = 2
+ pending = 2 # e.g. 2 devices scanning, only 1 scan finished
generating = 3
regenerate = 4
generated = 5
@@ -430,6 +430,11 @@ class StandardFileLocations(Enum):
downloads = 8
+class FileManagerType(Enum):
+ regular = 1
+ select = 2
+
+
max_remembered_destinations = 10
ThumbnailBackgroundName = MediumGray
@@ -489,7 +494,8 @@ class Desktop(Enum):
lxde = 7
lxqt = 8
ubuntugnome = 9
- unknown = 10
+ popgnome = 10
+ unknown = 11
class Distro(Enum):
diff --git a/raphodo/destinationdisplay.py b/raphodo/destinationdisplay.py
index 87b0a99..5a59fab 100644
--- a/raphodo/destinationdisplay.py
+++ b/raphodo/destinationdisplay.py
@@ -444,8 +444,9 @@ class DestinationDisplay(QWidget):
pref_list = self.prefs.video_subfolder
generation_type = NameGenerationType.video_subfolder
- prefDialog = PrefDialog(pref_defn, pref_list, generation_type, self.prefs,
- self.sample_rpd_file)
+ prefDialog = PrefDialog(
+ pref_defn, pref_list, generation_type, self.prefs, self.sample_rpd_file
+ )
if prefDialog.exec():
user_pref_list = prefDialog.getPrefList()
if not user_pref_list:
diff --git a/raphodo/didyouknow.py b/raphodo/didyouknow.py
index b0e6780..140c4ea 100644
--- a/raphodo/didyouknow.py
+++ b/raphodo/didyouknow.py
@@ -428,7 +428,6 @@ href="http://damonlynch.net/rapid/documentation/#caches">online documentation</a
# Miscellaneous Preferences
-
class Tips:
def __getitem__(self, item) -> str:
if 0 > item >= len(tips):
diff --git a/raphodo/errorlog.py b/raphodo/errorlog.py
index 763d094..b59399b 100644
--- a/raphodo/errorlog.py
+++ b/raphodo/errorlog.py
@@ -50,6 +50,7 @@ from raphodo.constants import ErrorType
from raphodo.rpdfile import RPDFile
from raphodo.problemnotification import Problem, Problems
from raphodo.viewutils import translateButtons
+from raphodo.storage import open_in_file_manager
# ErrorLogMessage = namedtuple('ErrorLogMessage', 'title body name uri')
@@ -405,10 +406,11 @@ class ErrorReport(QDialog):
index = int(fake_uri[fake_uri.find('///') + 3:])
uri = self.uris[index]
- cmd = '{} {}'.format(self.rapidApp.file_manager, uri)
- logging.debug("Launching: %s", cmd)
- args = shlex.split(cmd)
- subprocess.Popen(args)
+ open_in_file_manager(
+ file_manager=self.rapidApp.file_manager,
+ file_manager_type=self.rapidApp.file_manager_type,
+ uri=uri
+ )
def _saveUrls(self, text: str) -> str:
"""
diff --git a/raphodo/filebrowse.py b/raphodo/filebrowse.py
index 4508d7c..6f17b29 100644
--- a/raphodo/filebrowse.py
+++ b/raphodo/filebrowse.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -21,7 +21,7 @@ Display file system folders and allow the user to select one
"""
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2016, Damon Lynch"
+__copyright__ = "Copyright 2016-2017, Damon Lynch"
import os
import pathlib
@@ -32,14 +32,19 @@ import subprocess
from gettext import gettext as _
-from PyQt5.QtCore import (QDir, Qt, QModelIndex, QItemSelectionModel, QSortFilterProxyModel, QPoint)
-from PyQt5.QtWidgets import (QTreeView, QAbstractItemView, QFileSystemModel, QSizePolicy,
- QStyledItemDelegate, QStyleOptionViewItem, QMenu)
+from PyQt5.QtCore import (
+ QDir, Qt, QModelIndex, QItemSelectionModel, QSortFilterProxyModel, QPoint
+)
+from PyQt5.QtWidgets import (
+ QTreeView, QAbstractItemView, QFileSystemModel, QSizePolicy, QStyledItemDelegate,
+ QStyleOptionViewItem, QMenu
+)
from PyQt5.QtGui import QIcon
-from PyQt5.QtGui import (QPainter, QFont)
+from PyQt5.QtGui import QPainter, QFont
import raphodo.qrc_resources as qrc_resources
-from raphodo.constants import (minPanelWidth, minFileSystemViewHeight, Roles)
+from raphodo.constants import minPanelWidth, minFileSystemViewHeight, Roles
+from raphodo.storage import gvfs_gphoto2_path
class FileSystemModel(QFileSystemModel):
@@ -130,7 +135,7 @@ class FileSystemView(QTreeView):
"""
Call only after the model has been initialized
"""
- for i in (1,2,3):
+ for i in (1, 2, 3):
self.hideColumn(i)
def goToPath(self, path: str, scrollTo: bool=True) -> None:
@@ -205,10 +210,16 @@ class FileSystemFilter(QSortFilterProxyModel):
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex=None) -> bool:
+ index = self.sourceModel().index(sourceRow, 0, sourceParent) # type: QModelIndex
+ path = index.data(QFileSystemModel.FilePathRole) # type: str
+
+ if gvfs_gphoto2_path(path):
+ logging.debug("Rejecting browsing path %s", path)
+ return False
+
if not self.filtered_dir_names:
return True
- index = self.sourceModel().index(sourceRow, 0, sourceParent) # type: QModelIndex
file_name = index.data(QFileSystemModel.FileNameRole)
return file_name not in self.filtered_dir_names
@@ -217,6 +228,7 @@ class FileSystemDelegate(QStyledItemDelegate):
"""
Italicize provisional download folders that were not already created
"""
+
def __init__(self, parent=None):
super().__init__(parent)
diff --git a/raphodo/generatename.py b/raphodo/generatename.py
index bf28a7e..2057485 100644
--- a/raphodo/generatename.py
+++ b/raphodo/generatename.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# Copyright (C) 2007-2016 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2007-2017 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -20,7 +20,7 @@
### USA
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2007-2016, Damon Lynch"
+__copyright__ = "Copyright 2007-2017, Damon Lynch"
import re
from datetime import datetime, timedelta
@@ -35,7 +35,6 @@ locale.setlocale(locale.LC_ALL, '')
from gettext import gettext as _
from raphodo.preferences import DownloadsTodayTracker
-import raphodo.problemnotification as pn
from raphodo.problemnotification import (
RenamingProblems, FilenameNotFullyGeneratedProblem, make_href,
FolderNotFullyGeneratedProblemProblem, Problem
@@ -746,7 +745,7 @@ class Sequences:
"""
def __init__(self, downloads_today_tracker: DownloadsTodayTracker,
- stored_sequence_no: int):
+ stored_sequence_no: int) -> None:
self.session_sequence_no = 0
self.sequence_letter = -1
self.downloads_today_tracker = downloads_today_tracker
diff --git a/raphodo/nameeditor.py b/raphodo/nameeditor.py
index 5bb570e..83b16b4 100755
--- a/raphodo/nameeditor.py
+++ b/raphodo/nameeditor.py
@@ -275,10 +275,12 @@ class PrefEditor(QTextEdit):
cursor.insertText('<{}>'.format(pref_value))
def _setHighlighter(self) -> None:
- self.highlighter = PrefHighlighter(list(self.string_to_pref_mapper.keys()),
- self.pref_color,
- self.document())
+ self.highlighter = PrefHighlighter(
+ list(self.string_to_pref_mapper.keys()), self.pref_color, self.document()
+ )
+ # when color coding of text in the editor is complete,
+ # generate the preference list
self.highlighter.blockHighlighted.connect(self.generatePrefList)
def setPrefMapper(self, pref_mapper: Dict[Tuple[str, str, str], str],
@@ -289,7 +291,7 @@ class PrefEditor(QTextEdit):
self.pref_color = pref_color
self._setHighlighter()
- def _parseTextFragment(self, text_fragment) -> List[str]:
+ def _parseTextFragment(self, text_fragment) -> None:
if self.subfolder:
text_fragments = text_fragment.split(os.sep)
for index, text_fragment in enumerate(text_fragments):
@@ -421,7 +423,8 @@ def make_subfolder_menu_entry(prefs: Tuple[str]) -> str:
desc = prefs[0]
elements = prefs[1:]
return _("%(description)s - %(elements)s") % dict(
- description=desc, elements=os.sep.join(elements))
+ description=desc, elements=os.sep.join(elements)
+ )
def make_rename_menu_entry(prefs: Tuple[str]) -> str:
@@ -603,20 +606,26 @@ class PresetComboBox(QComboBox):
self.removeItem(index)
self.preset_edited = self.new_preset = False
- def setRemoveAllCustomEnabled(self, enabled: bool) -> None:
+ def _setRowEnabled(self, enabled: bool, offset: int) -> None:
assert self.edit_mode
# Our big assumption here is that the model is a QStandardItemModel
model = self.model()
count = self.count()
if self.preset_edited:
- row = count - 2
+ row = count - offset - 1
else:
- row = count - 1
+ row = count - offset
item = model.item(row, 0) # type: QStandardItem
if not enabled:
item.setFlags(Qt.NoItemFlags)
else:
- item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
+ item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
+
+ def setRemoveAllCustomEnabled(self, enabled: bool) -> None:
+ self._setRowEnabled(enabled=enabled, offset=1)
+
+ def setSaveNewCustomPresetEnabled(self, enabled: bool) -> None:
+ self._setRowEnabled(enabled=enabled, offset=2)
def getComboBoxIndex(self, preset_index: int) -> int:
"""
@@ -636,11 +645,10 @@ class PresetComboBox(QComboBox):
assert self.preset_separator
return preset_index + 1
-
def getPresetIndex(self, combobox_index: int) -> int:
"""
Opposite of getComboBoxIndex: calculates the preset index based on the
- given combox box index (which includes separators etc.)
+ given combo box index (which includes separators etc.)
:param combobox_index: the index into the combobox entries the user sees
:return: the index into the presets (built-in & custom)
"""
@@ -726,9 +734,9 @@ def make_sample_rpd_file(sample_job_code: str,
downloads_today_tracker = DownloadsTodayTracker(
day_start=prefs.day_start,
- downloads_today=prefs.downloads_today)
- sequences = gn.Sequences(downloads_today_tracker,
- prefs.stored_sequence_no)
+ downloads_today=prefs.downloads_today
+ )
+ sequences = gn.Sequences(downloads_today_tracker, prefs.stored_sequence_no)
if sample_rpd_file is not None:
if sample_rpd_file.metadata is None:
logging.debug('Sample file is missing its metadata')
@@ -754,6 +762,7 @@ def make_sample_rpd_file(sample_job_code: str,
return sample_rpd_file
+
class EditorCombobox(QComboBox):
"""
Regular combobox, but ignores the mouse wheel
@@ -762,6 +771,7 @@ class EditorCombobox(QComboBox):
def wheelEvent(self, event: QWheelEvent) -> None:
event.ignore()
+
class PrefDialog(QDialog):
"""
Dialog window to allow editing of file renaming and subfolder generation
@@ -840,7 +850,8 @@ class PrefDialog(QDialog):
# <b>. These are used to format the text the users sees
warning_msg = _(
'<b><font color="red">Warning:</font></b> <i>There is insufficient data to fully '
- 'generate the name. Please use other renaming options.</i>')
+ 'generate the name. Please use other renaming options.</i>'
+ )
self.is_subfolder = generation_type in (
NameGenerationType.photo_subfolder, NameGenerationType.video_subfolder
@@ -850,14 +861,16 @@ class PrefDialog(QDialog):
# Translators: please do not modify, change the order of or leave out html formatting
# tags like <i> and <b>. These are used to format the text the users sees.
# In this case, the </i> really is supposed to come before the <i>.
- subfolder_msg = _("The character</i> %(separator)s <i>creates a new subfolder "
- "level.") % dict(separator=os.sep)
+ subfolder_msg = _(
+ "The character</i> %(separator)s <i>creates a new subfolder level."
+ ) % dict(separator=os.sep)
# Translators: please do not modify, change the order of or leave out html formatting
# tags like <i> and <b>. These are used to format the text the users sees
# In this case, the </i> really is supposed to come before the <i>.
- subfolder_first_char_msg = _("There is no need start or end with the folder "
- "separator </i> %(separator)s<i>, because it is added "
- "automatically.") % dict(separator=os.sep)
+ subfolder_first_char_msg = _(
+ "There is no need start or end with the folder separator </i> %(separator)s<i>, "
+ "because it is added automatically."
+ ) % dict(separator=os.sep)
messages = (warning_msg, subfolder_msg, subfolder_first_char_msg)
else:
# Translators: please do not modify or leave out html formatting tags like <i> and
@@ -893,7 +906,7 @@ class PrefDialog(QDialog):
self.setLayout(layout)
layout.addLayout(flayout)
- layout.addSpacing(QFontMetrics(QFont()).height() / 2)
+ layout.addSpacing(int(QFontMetrics(QFont()).height() / 2))
layout.addWidget(self.editor)
layout.addWidget(self.messageWidget)
@@ -1136,20 +1149,27 @@ class PrefDialog(QDialog):
self.updateComboBoxCurrentIndex()
def updateComboBoxCurrentIndex(self) -> None:
+ """
+ Sets the combo value to match the current preference value
+ """
+
combobox_index, pref_list_index = self.getPresetMatch()
if pref_list_index >= 0:
+ # the editor contains an existing preset
self.preset.setCurrentIndex(combobox_index)
if self.preset.preset_edited or self.preset.new_preset:
self.preset.resetPresetList()
+ self.preset.setSaveNewCustomPresetEnabled(enabled=False)
if pref_list_index >= len(self.builtin_pref_names):
self.current_custom_name = self.preset.currentText()
else:
self.current_custom_name = None
elif not (self.preset.new_preset or self.preset.preset_edited):
- if self.current_custom_name is None:
- self.preset.setPresetNew()
- else:
- self.preset.setPresetEdited(self.current_custom_name)
+ if self.current_custom_name is None:
+ self.preset.setPresetNew()
+ else:
+ self.preset.setPresetEdited(self.current_custom_name)
+ self.preset.setSaveNewCustomPresetEnabled(enabled=True)
else:
self.preset.setCurrentIndex(0)
@@ -1228,6 +1248,7 @@ class PrefDialog(QDialog):
self.saveNewPreset(preset_name=preset_name)
if len(self.preset_names) == 1:
self.preset.setRemoveAllCustomEnabled(True)
+ self.preset.setSaveNewCustomPresetEnabled(enabled=False)
else:
# User cancelled creating a new preset
self.updateComboBoxCurrentIndex()
@@ -1264,8 +1285,10 @@ class PrefDialog(QDialog):
self.movePresetToFront(index=index)
else:
self._updateCombinedPrefs()
- self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
- preset_pref_lists=self.preset_pref_lists)
+ self.prefs.set_preset(
+ preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists
+ )
def movePresetToFront(self, index: int) -> None:
"""
@@ -1287,8 +1310,10 @@ class PrefDialog(QDialog):
self.preset_names.insert(0, preset_name)
self.preset_pref_lists.insert(0, pref_list)
self._updateCombinedPrefs()
- self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
- preset_pref_lists=self.preset_pref_lists)
+ self.prefs.set_preset(
+ preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists
+ )
def saveNewPreset(self, preset_name: str) -> None:
"""
@@ -1306,8 +1331,10 @@ class PrefDialog(QDialog):
self.preset_names.insert(0, preset_name)
self.preset_pref_lists.insert(0, user_pref_list)
self._updateCombinedPrefs()
- self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
- preset_pref_lists=self.preset_pref_lists)
+ self.prefs.set_preset(
+ preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists
+ )
def clearCustomPresets(self) -> None:
"""
@@ -1321,8 +1348,10 @@ class PrefDialog(QDialog):
self.preset_pref_lists = []
self.current_custom_name = None
self._updateCombinedPrefs()
- self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names,
- preset_pref_lists=self.preset_pref_lists)
+ self.prefs.set_preset(
+ preset_type=self.preset_type, preset_names=self.preset_names,
+ preset_pref_lists=self.preset_pref_lists
+ )
def udpateCachedPrefLists(self) -> None:
self.preset_names, self.preset_pref_lists = self.prefs.get_preset(
@@ -1339,8 +1368,9 @@ class PrefDialog(QDialog):
if the current user pref list matches an entry in it. Else Tuple of (-1, -1).
"""
- index = match_pref_list(pref_lists=self.combined_pref_lists,
- user_pref_list=self.editor.user_pref_list)
+ index = match_pref_list(
+ pref_lists=self.combined_pref_lists, user_pref_list=self.editor.user_pref_list
+ )
if index >= 0:
combobox_name = self.combined_pref_names[index]
return self.preset.findText(combobox_name), index
@@ -1362,22 +1392,27 @@ class PrefDialog(QDialog):
msgBox.setIcon(QMessageBox.Question)
msgBox.setWindowTitle(title)
if self.preset.new_preset:
- message = _("<b>Do you want to save the changes in a new custom preset?</b><br><br>"
- "Creating a custom preset is not required, but can help you keep "
- "organized.<br><br>"
- "The changes to the preferences will still be applied regardless of "
- "whether you create a new custom preset or not.")
+ message = _(
+ "<b>Do you want to save the changes in a new custom preset?</b><br><br>"
+ "Creating a custom preset is not required, but can help you keep "
+ "organized.<br><br>"
+ "The changes to the preferences will still be applied regardless of "
+ "whether you create a new custom preset or not."
+ )
msgBox.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
updateButton = newButton = None
else:
assert self.preset.preset_edited
- message = _("<b>Do you want to save the changes in a custom preset?</b><br><br>"
- "If you like, you can create a new custom preset or update the "
- "existing custom preset.<br><br>"
- "The changes to the preferences will still be applied regardless of "
- "whether you save a custom preset or not.")
- updateButton = msgBox.addButton(_('Update Custom Preset "%s"') %
- self.current_custom_name, QMessageBox.YesRole)
+ message = _(
+ "<b>Do you want to save the changes in a custom preset?</b><br><br>"
+ "If you like, you can create a new custom preset or update the "
+ "existing custom preset.<br><br>"
+ "The changes to the preferences will still be applied regardless of "
+ "whether you save a custom preset or not."
+ )
+ updateButton = msgBox.addButton(
+ _('Update Custom Preset "%s"') % self.current_custom_name, QMessageBox.YesRole
+ )
newButton = msgBox.addButton(_('Save New Custom Preset'), QMessageBox.YesRole)
msgBox.addButton(QMessageBox.No)
@@ -1418,12 +1453,18 @@ if __name__ == '__main__':
prefs = Preferences()
- prefDialog = PrefDialog(DICT_IMAGE_RENAME_L0, PHOTO_RENAME_MENU_DEFAULTS_CONV[1],
- NameGenerationType.photo_name, prefs)
+ # prefDialog = PrefDialog(DICT_IMAGE_RENAME_L0, PHOTO_RENAME_MENU_DEFAULTS_CONV[1],
+ # NameGenerationType.photo_name, prefs)
# prefDialog = PrefDialog(DICT_VIDEO_RENAME_L0, VIDEO_RENAME_MENU_DEFAULTS_CONV[1],
# NameGenerationType.video_name, prefs)
- # prefDialog = PrefDialog(DICT_SUBFOLDER_L0, PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2],
- # NameGenerationType.photo_subfolder, prefs)
+ prefDialog = PrefDialog(
+ DICT_SUBFOLDER_L0, PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2],
+ NameGenerationType.photo_subfolder, prefs
+ )
+ # prefDialog = PrefDialog(
+ # DICT_VIDEO_SUBFOLDER_L0, VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV[2],
+ # NameGenerationType.video_subfolder, prefs
+ # )
prefDialog.show()
app.exec_()
diff --git a/raphodo/preferences.py b/raphodo/preferences.py
index 832b7ac..001c147 100644
--- a/raphodo/preferences.py
+++ b/raphodo/preferences.py
@@ -32,11 +32,13 @@ from PyQt5.QtCore import QSettings, QTime, Qt
from gettext import gettext as _
-from raphodo.storage import (xdg_photos_directory, xdg_videos_directory, xdg_photos_identifier,
- xdg_videos_identifier)
+from raphodo.storage import (
+ xdg_photos_directory, xdg_videos_directory, xdg_photos_identifier, xdg_videos_identifier
+)
from raphodo.generatenameconfig import *
import raphodo.constants as constants
-from raphodo.utilities import available_cpu_count
+from raphodo.constants import PresetPrefType
+from raphodo.utilities import available_cpu_count, make_internationalized_list
import raphodo.__about__
from raphodo.rpdfile import ALL_KNOWN_EXTENSIONS
@@ -444,7 +446,7 @@ class Preferences:
def restore(self, key: str) -> None:
self[key] = self.defaults[key]
- def get_preset(self, preset_type: constants.PresetPrefType) -> Tuple[List[str],
+ def get_preset(self, preset_type: PresetPrefType) -> Tuple[List[str],
List[List[str]]]:
"""
Returns the custom presets for the particular type.
@@ -473,7 +475,7 @@ class Preferences:
return preset_names, preset_pref_lists
- def set_preset(self, preset_type: constants.PresetPrefType,
+ def set_preset(self, preset_type: PresetPrefType,
preset_names: List[str],
preset_pref_lists: List[str]) -> None:
"""
@@ -491,15 +493,17 @@ class Preferences:
preset = preset_type.name
- if not preset_names:
- self.settings.remove(preset)
- else:
- self.settings.beginWriteArray(preset)
- for i in range(len(preset_names)):
- self.settings.setArrayIndex(i)
- self.settings.setValue('name', preset_names[i])
- self.settings.setValue('pref_list', preset_pref_lists[i])
- self.settings.endArray()
+ # Clear all the existing presets with that name.
+ # If we don't do this, when the array shrinks, old values can hang around,
+ # even though the array size is set correctly.
+ self.settings.remove(preset)
+
+ self.settings.beginWriteArray(preset)
+ for i in range(len(preset_names)):
+ self.settings.setArrayIndex(i)
+ self.settings.setValue('name', preset_names[i])
+ self.settings.setValue('pref_list', preset_pref_lists[i])
+ self.settings.endArray()
self.settings.endGroup()
@@ -601,10 +605,12 @@ class Preferences:
msg = ''
valid = True
- tests = ((self.photo_rename, DICT_IMAGE_RENAME_L0),
- (self.video_rename, DICT_VIDEO_RENAME_L0),
- (self.photo_subfolder, DICT_SUBFOLDER_L0),
- (self.video_subfolder, DICT_VIDEO_SUBFOLDER_L0))
+ tests = (
+ (self.photo_rename, DICT_IMAGE_RENAME_L0),
+ (self.video_rename, DICT_VIDEO_RENAME_L0),
+ (self.photo_subfolder, DICT_SUBFOLDER_L0),
+ (self.video_subfolder, DICT_VIDEO_SUBFOLDER_L0)
+ )
# test file renaming
for pref, pref_defn in tests[:2]:
@@ -622,23 +628,63 @@ class Preferences:
L1s = [pref[i] for i in range(0, len(pref), 3)]
if L1s[0] == SEPARATOR:
- raise PrefValueKeyComboError(_(
- "Subfolder preferences should not start with a %s") % os.sep)
+ raise PrefValueKeyComboError(
+ _("Subfolder preferences should not start with a %s") % os.sep
+ )
elif L1s[-1] == SEPARATOR:
- raise PrefValueKeyComboError(_(
- "Subfolder preferences should not end with a %s") % os.sep)
+ raise PrefValueKeyComboError(
+ _("Subfolder preferences should not end with a %s") % os.sep
+ )
else:
for i in range(len(L1s) - 1):
if L1s[i] == SEPARATOR and L1s[i + 1] == SEPARATOR:
- raise PrefValueKeyComboError(_(
- "Subfolder preferences should not contain "
- "two %s one after the other") % os.sep)
+ raise PrefValueKeyComboError(
+ _(
+ "Subfolder preferences should not contain two %s one after "
+ "the other"
+ ) % os.sep
+ )
except PrefError as e:
valid = False
msg += e.msg + "\n"
- return (valid, msg)
+ return valid, msg
+
+ def _filter_duplicate_generation_prefs(self, preset_type: PresetPrefType) -> None:
+ preset_names, preset_pref_lists = self.get_preset(preset_type=preset_type)
+ seen = set()
+ filtered_names = []
+ filtered_pref_lists = []
+ duplicates = []
+ for name, pref_list in zip(preset_names, preset_pref_lists):
+ value = tuple(pref_list)
+ if value in seen:
+ duplicates.append(name)
+ else:
+ seen.add(value)
+ filtered_names.append(name)
+ filtered_pref_lists.append(pref_list)
+
+ if duplicates:
+ human_readable = preset_type.name[len('preset_'):].replace('_', ' ')
+ logging.warning(
+ 'Removed %s duplicate(s) from %s presets: %s',
+ len(duplicates), human_readable, make_internationalized_list(duplicates)
+ )
+ self.set_preset(
+ preset_type=preset_type, preset_names=filtered_names,
+ preset_pref_lists=filtered_pref_lists
+ )
+
+ def filter_duplicate_generation_prefs(self) -> None:
+ """
+ Remove any duplicate subfolder generation or file renaming custom presets
+ """
+
+ logging.info("Checking for duplicate name generation preference values")
+ for preset_type in PresetPrefType:
+ self._filter_duplicate_generation_prefs(preset_type)
def must_synchronize_raw_jpg(self) -> bool:
"""
@@ -676,7 +722,7 @@ class Preferences:
:return: a tuple of the photo & video rename and subfolder
generation preferences
"""
- return (self.photo_rename, self.photo_subfolder, self.video_rename, self.video_subfolder)
+ return self.photo_rename, self.photo_subfolder, self.video_rename, self.video_subfolder
def get_day_start_qtime(self) -> QTime:
"""
@@ -981,4 +1027,4 @@ def match_pref_list(pref_lists: List[List[str]], user_pref_list: List[str]) -> i
try:
return pref_lists.index(user_pref_list)
except ValueError:
- return -1 \ No newline at end of file
+ return -1
diff --git a/raphodo/proximity.py b/raphodo/proximity.py
index 6bedf0c..b3875d3 100644
--- a/raphodo/proximity.py
+++ b/raphodo/proximity.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2015-2017 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2015-2018 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -17,7 +17,7 @@
# see <http://www.gnu.org/licenses/>.
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2015-2017, Damon Lynch"
+__copyright__ = "Copyright 2015-2018, Damon Lynch"
from collections import (namedtuple, defaultdict, deque, Counter)
from operator import attrgetter
diff --git a/raphodo/rapid.py b/raphodo/rapid.py
index 6c1254a..683520b 100755
--- a/raphodo/rapid.py
+++ b/raphodo/rapid.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# Copyright (C) 2011-2017 Damon Lynch <damonlynch@gmail.com>
+# Copyright (C) 2011-2018 Damon Lynch <damonlynch@gmail.com>
# This file is part of Rapid Photo Downloader.
#
@@ -29,7 +29,7 @@ Project line length: 100 characters (i.e. word wrap at 99)
"""
__author__ = 'Damon Lynch'
-__copyright__ = "Copyright 2011-2017, Damon Lynch"
+__copyright__ = "Copyright 2011-2018, Damon Lynch"
import sys
import logging
@@ -94,7 +94,7 @@ from raphodo.storage import (
has_one_or_more_folders, mountPaths, get_desktop_environment, get_desktop,
gvfs_controls_mounts, get_default_file_manager, validate_download_folder,
validate_source_folder, get_fdo_cache_thumb_base_directory, WatchDownloadDirs, get_media_dir,
- StorageSpace
+ StorageSpace, gvfs_gphoto2_path
)
from raphodo.interprocess import (
ScanArguments, CopyFilesArguments, RenameAndMoveFileData, BackupArguments,
@@ -640,6 +640,9 @@ class RapidWindow(QMainWindow):
"Version downgrade detected, from %s to %s",
__about__.__version__, previous_version
)
+ if pv < pkg_resources.parse_version('0.9.7b1'):
+ # Remove any duplicate subfolder generation or file renaming custom presets
+ self.prefs.filter_duplicate_generation_prefs()
def startThreadControlSockets(self) -> None:
"""
@@ -809,7 +812,7 @@ class RapidWindow(QMainWindow):
self.updateThumbnailModelAfterProximityChange
)
- self.file_manager = get_default_file_manager()
+ self.file_manager, self.file_manager_type = get_default_file_manager()
if self.file_manager:
logging.info("Default file manager: %s", self.file_manager)
else:
@@ -2574,7 +2577,7 @@ class RapidWindow(QMainWindow):
"""
Respond to This Computer Toggle Switch
- :param on: whether swich is on or off
+ :param on: whether switch is on or off
"""
if on:
@@ -2594,14 +2597,15 @@ class RapidWindow(QMainWindow):
"""
Respond to Devices Toggle Switch
- :param on: whether swich is on or off
+ :param on: whether switch is on or off
"""
self.prefs.device_autodetection = on
if not on:
for scan_id in list(self.devices.volumes_and_cameras):
self.removeDevice(scan_id=scan_id, adjust_temporal_proximity=False)
- if len(self.devices) == 0:
+ state = self.proximityStatePostDeviceRemoval()
+ if state == TemporalProximityState.empty:
self.temporalProximity.setState(TemporalProximityState.empty)
else:
self.generateTemporalProximityTableData("devices were removed as a download source")
@@ -2611,6 +2615,19 @@ class RapidWindow(QMainWindow):
QTimer.singleShot(100, self.devicesViewToggledOn)
self.adjustLeftPanelSliderHandles()
+ def proximityStatePostDeviceRemoval(self) -> TemporalProximityState:
+ """
+ :return: set correct proximity state after a device is removed
+ """
+
+ # ignore devices that are scanning - we don't care about them, because the scan
+ # could take a long time, especially with phones
+ if len(self.devices) - len(self.devices.scanning) > 0:
+ # Other already scanned devices are present
+ return TemporalProximityState.regenerate
+ else:
+ return TemporalProximityState.empty
+
@pyqtSlot()
def devicesViewToggledOn(self) -> None:
self.searchForCameras()
@@ -4446,13 +4463,6 @@ Do you want to proceed with the download?
Initiate Timeline generation if it's right to do so
"""
- if len(self.devices.scanning):
- logging.info(
- "Was tasked to generate Timeline because %s, but ignoring request "
- "because a scan is occurring", reason
- )
- return
-
if self.temporalProximity.state == TemporalProximityState.ctime_rebuild:
logging.info(
"Was tasked to generate Timeline because %s, but ignoring request "
@@ -5106,7 +5116,8 @@ Do you want to proceed with the download?
self.setDownloadCapabilities()
if adjust_temporal_proximity:
- if len(self.devices) == 0:
+ state = self.proximityStatePostDeviceRemoval()
+ if state == TemporalProximityState.empty:
self.temporalProximity.setState(TemporalProximityState.empty)
elif files_removed:
self.generateTemporalProximityTableData("a download source was removed")
@@ -6098,7 +6109,7 @@ def main():
sys.exit(1)
media_dir = get_media_dir()
- auto_detect = args.path.startswith(media_dir)
+ auto_detect = args.path.startswith(media_dir) or gvfs_gphoto2_path(args.path)
if auto_detect:
this_computer_source = False
this_computer_location = None
diff --git a/raphodo/renameandmovefile.py b/raphodo/renameandmovefile.py
index 1405cc5..ae2c5e5 100755
--- a/raphodo/renameandmovefile.py
+++ b/raphodo/renameandmovefile.py
@@ -44,7 +44,6 @@ from gettext import gettext as _
import raphodo.exiftool as exiftool
import raphodo.generatename as gn
-import raphodo.problemnotification as pn
from raphodo.preferences import DownloadsTodayTracker, Preferences
from raphodo.constants import (ConflictResolution, FileType, DownloadStatus, RenameAndMoveStatus)
from raphodo.interprocess import (RenameAndMoveFileData, RenameAndMoveFileResults, DaemonProcess)
@@ -153,10 +152,9 @@ def load_metadata(rpd_file: Union[Photo, Video],
"""
Loads the metadata for the file.
+ :param rpd_file: photo or video
:param et_process: the daemon ExifTool process
- :param temp_file: If true, the the metadata from the temporary file
- rather than the original source file is used. This is important,
- because the metadata can be modified by the filemodify process
+ :param problems: problems encountered renaming the file
:return True if operation succeeded, false otherwise
"""
if rpd_file.metadata is None:
@@ -205,6 +203,7 @@ def generate_subfolder(rpd_file: Union[Photo, Video],
:param rpd_file: file to work on
:param et_process: the daemon ExifTool process
+ :param problems: problems encountered renaming the file
"""
if rpd_file.file_type == FileType.photo:
@@ -223,6 +222,7 @@ def generate_name(rpd_file: Union[Photo, Video],
:param rpd_file: file to work on
:param et_process: the daemon ExifTool process
+ :param problems: problems encountered renaming the file
"""
if rpd_file.file_type == FileType.photo:
@@ -268,9 +268,11 @@ class RenameMoveFileWorker(DaemonProcess):
dt = datetime.fromtimestamp(modification_time)
date = dt.strftime("%x")
time = dt.strftime("%X")
- except:
- logging.error("Could not determine the file modification time of %s",
- rpd_file.download_full_file_name)
+ except Exception:
+ logging.error(
+ "Could not determine the file modification time of %s",
+ rpd_file.download_full_file_name
+ )
date = time = ''
source = rpd_file.get_souce_href()
@@ -312,7 +314,7 @@ class RenameMoveFileWorker(DaemonProcess):
"""
Handle cases where file failed to download
"""
- uri=get_uri(
+ uri = get_uri(
full_file_name=rpd_file.full_file_name, camera_details=rpd_file.camera_details
)
device = make_href(name=rpd_file.device_display_name, uri=rpd_file.device_uri)
@@ -365,7 +367,7 @@ class RenameMoveFileWorker(DaemonProcess):
assert isinstance(i1_date_time, datetime)
i1_date = i1_date_time.strftime("%x")
i1_time = i1_date_time.strftime("%X")
- assert isinstance(image2_date_time,datetime)
+ assert isinstance(image2_date_time, datetime)
image2_date = image2_date_time.strftime("%x")
image2_time = image2_date_time.strftime("%X")
@@ -383,7 +385,7 @@ class RenameMoveFileWorker(DaemonProcess):
def _move_associate_file(self, extension: str,
full_base_name: str,
- temp_associate_file: str) -> str:
+ temp_associate_file: str) -> str:
"""
Move (rename) the associate file using the pre-generated name.
@@ -628,7 +630,7 @@ class RenameMoveFileWorker(DaemonProcess):
DuplicateFileWhenSyncingProblem(
name=rpd_file.name,
uri=rpd_file.get_uri(),
- file_type = rpd_file.title,
+ file_type=rpd_file.title,
)
)
@@ -678,12 +680,15 @@ class RenameMoveFileWorker(DaemonProcess):
generate_name(rpd_file, self.exiftool_process, self.problems)
if rpd_file.name_generation_problem:
- logging.warning("Encountered a problem generating file name for file %s",
- rpd_file.name)
+ logging.warning(
+ "Encountered a problem generating file name for file %s",
+ rpd_file.name
+ )
rpd_file.status = DownloadStatus.downloaded_with_warning
else:
- logging.debug("Generated file name %s for file %s", rpd_file.download_name,
- rpd_file.name)
+ logging.debug(
+ "Generated file name %s for file %s", rpd_file.download_name, rpd_file.name
+ )
else:
logging.error("Failed to generate subfolder name for file: %s", rpd_file.name)
@@ -700,8 +705,9 @@ class RenameMoveFileWorker(DaemonProcess):
move_succeeded = False
rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder)
- rpd_file.download_full_file_name = os.path.join(rpd_file.download_path,
- rpd_file.download_name)
+ rpd_file.download_full_file_name = os.path.join(
+ rpd_file.download_path, rpd_file.download_name
+ )
rpd_file.download_full_base_name = os.path.splitext(rpd_file.download_full_file_name)[0]
if not os.path.isdir(rpd_file.download_path):
@@ -727,8 +733,10 @@ class RenameMoveFileWorker(DaemonProcess):
try:
if os.path.exists(rpd_file.download_full_file_name):
raise OSError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name)
- logging.debug("Renaming %s to %s .....",
- rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
+ logging.debug(
+ "Renaming %s to %s .....",
+ rpd_file.temp_full_file_name, rpd_file.download_full_file_name
+ )
os.rename(rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
logging.debug("....successfully renamed file")
move_succeeded = True
@@ -753,8 +761,9 @@ class RenameMoveFileWorker(DaemonProcess):
self.prepare_rpd_file(rpd_file)
- synchronize_raw_jpg = (self.prefs.must_synchronize_raw_jpg() and
- rpd_file.file_type == FileType.photo)
+ synchronize_raw_jpg = (
+ self.prefs.must_synchronize_raw_jpg() and rpd_file.file_type == FileType.photo
+ )
if synchronize_raw_jpg:
sync_result = self.sync_raw_jpg(rpd_file)
@@ -780,8 +789,8 @@ class RenameMoveFileWorker(DaemonProcess):
date_time=rpd_file.date_time(),
sequence_number_used=sequence)
- if not synchronize_raw_jpg or (synchronize_raw_jpg and
- sync_result.sequence_to_use is None):
+ if not synchronize_raw_jpg or (
+ synchronize_raw_jpg and sync_result.sequence_to_use is None):
uses_sequence_session_no = self.prefs.any_pref_uses_session_sequence_no()
uses_sequence_letter = self.prefs.any_pref_uses_sequence_letter_value()
if uses_sequence_session_no or uses_sequence_letter:
@@ -809,6 +818,23 @@ class RenameMoveFileWorker(DaemonProcess):
return move_succeeded
+ def initialise_downloads_today_stored_number(self) -> None:
+ """
+ Initialize (or reinitialize) Downloads Today and Stored No
+ sequence values from the program preferences.
+ """
+
+ # Synchronize QSettings instance in preferences class
+ self.prefs.sync()
+
+ # Track downloads today, using a class whose purpose is to
+ # take the value in the user prefs, increment, and then
+ # finally used to update the prefs
+ self.downloads_today_tracker = DownloadsTodayTracker(
+ day_start=self.prefs.day_start,
+ downloads_today=self.prefs.downloads_today
+ )
+
def run(self) -> None:
"""
Generate subfolder and filename, and attempt to move the file
@@ -826,6 +852,12 @@ class RenameMoveFileWorker(DaemonProcess):
# suffixes to duplicate files
self.duplicate_files = {}
+ self.initialise_downloads_today_stored_number()
+
+ self.sequences = gn.Sequences(
+ self.downloads_today_tracker, self.prefs.stored_sequence_no
+ )
+
with stdchannel_redirected(sys.stderr, os.devnull):
with exiftool.ExifTool() as self.exiftool_process:
while True:
@@ -839,18 +871,13 @@ class RenameMoveFileWorker(DaemonProcess):
data = pickle.loads(content) # type: RenameAndMoveFileData
if data.message == RenameAndMoveStatus.download_started:
- # Synchronize QSettings instance in preferences class
- self.prefs.sync()
-
- # Track downloads today, using a class whose purpose is to
- # take the value in the user prefs, increment, and then
- # finally used to update the prefs
- self.downloads_today_tracker = DownloadsTodayTracker(
- day_start=self.prefs.day_start,
- downloads_today=self.prefs.downloads_today)
-
- self.sequences = gn.Sequences(self.downloads_today_tracker,
- self.prefs.stored_sequence_no)
+
+ # reinitialize downloads today and stored sequence number
+ # in case the user has updated them via the user interface
+ self.initialise_downloads_today_stored_number()
+ self.sequences.downloads_today_tracker = self.downloads_today_tracker
+ self.sequences.stored_sequence_no = self.prefs.stored_sequence_no
+
dl_today = self.downloads_today_tracker.get_or_reset_downloads_today()
logging.debug("Completed downloads today: %s", dl_today)
@@ -859,9 +886,7 @@ class RenameMoveFileWorker(DaemonProcess):
elif data.message == RenameAndMoveStatus.download_completed:
if len(self.problems):
self.content = pickle.dumps(
- RenameAndMoveFileResults(
- problems=self.problems
- ),
+ RenameAndMoveFileResults(problems=self.problems),
pickle.HIGHEST_PROTOCOL
)
self.send_message_to_sink()
diff --git a/raphodo/scan.py b/raphodo/scan.py
index abad59e..7b98941 100755
--- a/raphodo/scan.py
+++ b/raphodo/scan.py
@@ -90,7 +90,7 @@ from raphodo.problemnotification import (
CameraFileReadProblem, FileMetadataLoadProblem, FileWriteProblem, FsMetadataReadProblem,
FileZeroLengthProblem
)
-from raphodo.storage import get_uri, CameraDetails
+from raphodo.storage import get_uri, CameraDetails, gvfs_gphoto2_path
FileInfo = namedtuple('FileInfo', 'path modification_time size ext_lower base_name file_type')
CameraFile = namedtuple('CameraFile', 'name size')
@@ -393,6 +393,9 @@ class ScanWorker(WorkerInPublishPullPipeline):
for dir_name, dir_list, file_list in walk(path_to_walk):
if len(dir_list) > 0:
+ # Do not scan gvfs gphoto2 mount
+ dir_list[:] = (d for d in dir_list if not gvfs_gphoto2_path(dir_name + d))
+
if self.scan_preferences.ignored_paths:
# Don't inspect paths the user wants ignored
# Altering subdirs in place controls the looping
diff --git a/raphodo/storage.py b/raphodo/storage.py
index 6856c38..f4ef551 100644
--- a/raphodo/storage.py
+++ b/raphodo/storage.py
@@ -75,7 +75,7 @@ from gi.repository import GUdev, UDisks, GLib
from gettext import gettext as _
-from raphodo.constants import Desktop, Distro
+from raphodo.constants import Desktop, Distro, FileManagerType
from raphodo.utilities import (
process_running, log_os_release, remove_topmost_directory_from_path, find_mount_point
)
@@ -170,6 +170,24 @@ def get_media_dir() -> str:
raise ("Mounts.setValidMountPoints() not implemented on %s", sys.platform())
+_gvfs_gphoto2 = re.compile('gvfs.*gphoto2.*host')
+
+
+def gvfs_gphoto2_path(path: str) -> bool:
+ """
+ :return: True if the path appears to be a GVFS gphoto2 path
+
+ >>> p = "/run/user/1000/gvfs/gphoto2:host=%5Busb%3A002%2C013%5D"
+ >>> gvfs_gphoto2_path(p)
+ True
+ >>> p = '/home/damon'
+ >>> gvfs_gphoto2_path(p)
+ False
+ """
+
+ return _gvfs_gphoto2.search(path) is not None
+
+
class ValidMounts():
r"""
Operations to find 'valid' mount points, i.e. the places in which
@@ -359,6 +377,8 @@ def get_desktop() -> Desktop:
env = 'cinnamon'
elif env == 'ubuntu:gnome':
env = 'ubuntugnome'
+ elif env == 'pop:gnome':
+ env = 'popgnome'
try:
return Desktop[env]
except KeyError:
@@ -546,7 +566,8 @@ def get_fdo_cache_thumb_base_directory() -> str:
return os.path.join(BaseDirectory.xdg_cache_home, 'thumbnails')
-def get_default_file_manager(remove_args: bool = True) -> Optional[str]:
+def get_default_file_manager(remove_args: bool = True) -> Tuple[
+ Optional[str], Optional[FileManagerType]]:
"""
Attempt to determine the default file manager for the system
:param remove_args: if True, remove any arguments such as %U from
@@ -558,7 +579,7 @@ def get_default_file_manager(remove_args: bool = True) -> Optional[str]:
try:
desktop_file = subprocess.check_output(cmd, universal_newlines=True) # type: str
except:
- return None
+ return None, None
# Remove new line character from output
desktop_file = desktop_file[:-1]
if desktop_file.endswith(';'):
@@ -570,21 +591,40 @@ def get_default_file_manager(remove_args: bool = True) -> Optional[str]:
try:
desktop_entry = DesktopEntry(path)
except xdg.Exceptions.ParsingError:
- return None
+ return None, None
try:
desktop_entry.parse(path)
except:
- return None
+ return None, None
fm = desktop_entry.getExec()
+ if fm.startswith('dolphin'):
+ file_manager_type = FileManagerType.select
+ else:
+ file_manager_type = FileManagerType.regular
if remove_args:
- return fm.split()[0]
+ return fm.split()[0], file_manager_type
else:
- return fm
+ return fm, file_manager_type
# Special case: LXQt
if get_desktop() == Desktop.lxqt:
if shutil.which('pcmanfm-qt'):
- return 'pcmanfm-qt'
+ return 'pcmanfm-qt', FileManagerType.regular
+
+ return None, None
+
+def open_in_file_manager(file_manager: str,
+ file_manager_type: FileManagerType,
+ uri: str) -> None:
+ if file_manager_type == FileManagerType.regular:
+ arg = ''
+ else:
+ arg = '--select '
+
+ cmd = '{} {}"{}"'.format(file_manager, arg, uri)
+ logging.debug("Launching: %s", cmd)
+ args = shlex.split(cmd)
+ subprocess.Popen(args)
_desktop = get_desktop()
@@ -612,7 +652,7 @@ def get_uri(full_file_name: Optional[str]=None,
prefix = 'file://'
if desktop_environment:
desktop = get_desktop()
- if full_file_name and desktop in (Desktop.mate, Desktop.kde):
+ if full_file_name and desktop == Desktop.mate:
full_file_name = os.path.dirname(full_file_name)
else:
if not desktop_environment:
@@ -637,11 +677,6 @@ def get_uri(full_file_name: Optional[str]=None,
prefix = 'mtp:/' + pathname2url(
'{}/{}'.format(camera_details.display_name, camera_details.storage_desc)
)
- # Dolphin doesn't highlight the file if it's passed.
- # Instead it tries to open it, but fails.
- # So don't pass the file, just the directory it's in.
- if full_file_name:
- full_file_name = os.path.dirname(full_file_name)
else:
logging.error("Don't know how to generate MTP prefix for %s", _desktop.name)
else:
diff --git a/raphodo/thumbnaildisplay.py b/raphodo/thumbnaildisplay.py
index cde71f8..e7a3f9b 100644
--- a/raphodo/thumbnaildisplay.py
+++ b/raphodo/thumbnaildisplay.py
@@ -59,7 +59,9 @@ from raphodo.constants import (
Desktop, DeviceState, extensionColor, FadeSteps, FadeMilliseconds, PaleGray, DarkGray,
DoubleDarkGray, Plural, manually_marked_previously_downloaded, thumbnail_margin
)
-from raphodo.storage import get_program_cache_directory, get_desktop, validate_download_folder
+from raphodo.storage import (
+ get_program_cache_directory, get_desktop, validate_download_folder, open_in_file_manager
+)
from raphodo.utilities import (
CacheDirs, make_internationalized_list, format_size_for_user, runs, arrow_locale
)
@@ -1715,9 +1717,13 @@ class ThumbnailView(QListView):
return
else:
uid = uids[0]
- row = model.uid_to_row[uid]
- index = model.index(row, 0)
- self.scrollTo(index, QAbstractItemView.PositionAtTop)
+ try:
+ row = model.uid_to_row[uid]
+ except KeyError:
+ logging.debug("Ignoring scroll request to unknown thumbnail")
+ else:
+ index = model.index(row, 0)
+ self.scrollTo(index, QAbstractItemView.PositionAtTop)
class ThumbnailDelegate(QStyledItemDelegate):
@@ -1766,7 +1772,7 @@ class ThumbnailDelegate(QStyledItemDelegate):
self.contextMenu = QMenu()
self.openInFileBrowserAct = self.contextMenu.addAction(_('Open in File Browser...'))
- self.openInFileBrowserAct.triggered.connect(self.doOpenInFileBrowserAct)
+ self.openInFileBrowserAct.triggered.connect(self.doOpenInFileManagerAct)
self.copyPathAct = self.contextMenu.addAction(_('Copy Path'))
self.copyPathAct.triggered.connect(self.doCopyPathAction)
# Translators: 'File' here applies to a single file. The command allows users to instruct
@@ -1844,14 +1850,15 @@ class ThumbnailDelegate(QStyledItemDelegate):
QApplication.clipboard().setText(path)
@pyqtSlot()
- def doOpenInFileBrowserAct(self) -> None:
+ def doOpenInFileManagerAct(self) -> None:
index = self.clickedIndex
if index:
uri = index.model().data(index, Roles.uri)
- cmd = '{} "{}"'.format(self.rapidApp.file_manager, uri)
- logging.debug("Launching: %s", cmd)
- args = shlex.split(cmd)
- subprocess.Popen(args)
+ open_in_file_manager(
+ file_manager=self.rapidApp.file_manager,
+ file_manager_type=self.rapidApp.file_manager_type,
+ uri=uri
+ )
@pyqtSlot()
def doMarkFileDownloadedAct(self) -> None:
@@ -2072,12 +2079,15 @@ class ThumbnailDelegate(QStyledItemDelegate):
def oneOrMoreNotDownloaded(self) -> Tuple[int, Plural]:
i = 0
selectedIndexes = self.selectedIndexes()
- noSelected = len(selectedIndexes)
- for index in selectedIndexes:
- if not index.data(Roles.previously_downloaded):
- i += 1
- if i == 2:
- break
+ if selectedIndexes is None:
+ noSelected = 0
+ else:
+ noSelected = len(selectedIndexes)
+ for index in selectedIndexes:
+ if not index.data(Roles.previously_downloaded):
+ i += 1
+ if i == 2:
+ break
if i == 0:
return noSelected, Plural.zero
@@ -2208,7 +2218,7 @@ class ThumbnailDelegate(QStyledItemDelegate):
else:
logging.debug("Not applying job code because no files selected")
- def selectedIndexes(self):
+ def selectedIndexes(self) -> Optional[List[QModelIndex]]:
selection = self.rapidApp.thumbnailView.selectionModel() # type: QItemSelectionModel
if selection.hasSelection():
selected = selection.selection() # type: QItemSelection