summaryrefslogtreecommitdiff
path: root/raphodo/thumbnaildisplay.py
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo/thumbnaildisplay.py')
-rw-r--r--raphodo/thumbnaildisplay.py349
1 files changed, 275 insertions, 74 deletions
diff --git a/raphodo/thumbnaildisplay.py b/raphodo/thumbnaildisplay.py
index ddfb526..e7a3f9b 100644
--- a/raphodo/thumbnaildisplay.py
+++ b/raphodo/thumbnaildisplay.py
@@ -38,13 +38,18 @@ import arrow.arrow
from dateutil.tz import tzlocal
from colour import Color
-from PyQt5.QtCore import (QAbstractListModel, QModelIndex, Qt, pyqtSignal, QSize, QRect, QEvent,
- QPoint, QMargins, QItemSelectionModel,
- QAbstractItemModel, pyqtSlot, QItemSelection, QTimeLine)
-from PyQt5.QtWidgets import (QListView, QStyledItemDelegate, QStyleOptionViewItem, QApplication,
- QStyle, QStyleOptionButton, QMenu, QWidget, QAbstractItemView)
-from PyQt5.QtGui import (QPixmap, QImage, QPainter, QColor, QBrush, QFontMetrics,
- QGuiApplication, QPen, QMouseEvent, QFont)
+from PyQt5.QtCore import (
+ QAbstractListModel, QModelIndex, Qt, pyqtSignal, QSize, QRect, QEvent, QPoint,
+ QItemSelectionModel, QAbstractItemModel, pyqtSlot, QItemSelection, QTimeLine,
+)
+from PyQt5.QtWidgets import (
+ QListView, QStyledItemDelegate, QStyleOptionViewItem, QApplication, QStyle, QStyleOptionButton,
+ QMenu, QWidget, QAbstractItemView,
+)
+from PyQt5.QtGui import (
+ QPixmap, QImage, QPainter, QColor, QBrush, QFontMetrics, QGuiApplication, QPen, QMouseEvent,
+ QFont,
+)
from raphodo.rpdfile import RPDFile, FileTypeCounter, ALL_USER_VISIBLE_EXTENSIONS, MUST_CACHE_VIDEOS
from raphodo.interprocess import GenerateThumbnailsArguments, Device, GenerateThumbnailsResults
@@ -52,9 +57,11 @@ from raphodo.constants import (
DownloadStatus, Downloaded, FileType, DownloadingFileTypes, ThumbnailSize,
ThumbnailCacheStatus, Roles, DeviceType, CustomColors, Show, Sort, ThumbnailBackgroundName,
Desktop, DeviceState, extensionColor, FadeSteps, FadeMilliseconds, PaleGray, DarkGray,
- DoubleDarkGray
+ DoubleDarkGray, Plural, manually_marked_previously_downloaded, thumbnail_margin
+)
+from raphodo.storage import (
+ get_program_cache_directory, get_desktop, validate_download_folder, open_in_file_manager
)
-from raphodo.storage import get_program_cache_directory, get_desktop, validate_download_folder
from raphodo.utilities import (
CacheDirs, make_internationalized_list, format_size_for_user, runs, arrow_locale
)
@@ -62,6 +69,7 @@ from raphodo.thumbnailer import Thumbnailer
from raphodo.rpdsql import ThumbnailRowsSQL, ThumbnailRow
from raphodo.viewutils import ThumbnailDataForProximity
from raphodo.proximity import TemporalProximityState
+from raphodo.rpdsql import DownloadedSQL
DownloadFiles = namedtuple(
@@ -231,9 +239,9 @@ class ThumbnailListModel(QAbstractListModel):
logging.debug("-- Thumbnail Model --")
db_length = self.tsql.get_count()
- db_length_and_buffer_length= db_length + len(self.add_buffer)
+ db_length_and_buffer_length = db_length + len(self.add_buffer)
if (len(self.thumbnails) != db_length_and_buffer_length or
- db_length_and_buffer_length != len(self.rpd_files)):
+ db_length_and_buffer_length != len(self.rpd_files)):
logging.error("Conflicting values: %s thumbnails; %s database rows; %s rpd_files",
len(self.thumbnails), db_length, len(self.rpd_files))
else:
@@ -293,9 +301,11 @@ class ThumbnailListModel(QAbstractListModel):
if not suppress_signal:
self.layoutAboutToBeChanged.emit()
- self.rows = self.tsql.get_view(sort_by=self.sort_by, sort_order=self.sort_order,
- show=self.show, proximity_col1=self.proximity_col1,
- proximity_col2=self.proximity_col2)
+ self.rows = self.tsql.get_view(
+ sort_by=self.sort_by, sort_order=self.sort_order,
+ show=self.show, proximity_col1=self.proximity_col1,
+ proximity_col2=self.proximity_col2
+ )
self.uid_to_row = {row[0]: idx for idx, row in enumerate(self.rows)}
if not suppress_signal:
@@ -385,7 +395,7 @@ class ThumbnailListModel(QAbstractListModel):
elif role == Roles.filename:
return rpd_file.name
elif role == Roles.previously_downloaded:
- return rpd_file.previously_downloaded()
+ return rpd_file.previously_downloaded
elif role == Roles.extension:
return rpd_file.extension, rpd_file.extension_type
elif role == Roles.download_status:
@@ -401,7 +411,7 @@ class ThumbnailListModel(QAbstractListModel):
return 'LOG'
else:
return None
- elif role== Roles.path:
+ elif role == Roles.path:
if rpd_file.status in Downloaded:
return rpd_file.download_full_file_name
else:
@@ -475,7 +485,7 @@ class ThumbnailListModel(QAbstractListModel):
filename=rpd_file.download_name, path=path, downloaded_as=downloaded_as
)
- if rpd_file.previously_downloaded():
+ if rpd_file.previously_downloaded:
prev_datetime = arrow.get(rpd_file.prev_datetime, tzlocal())
prev_date = _('%(date_time)s (%(human_readable)s)') % dict(
@@ -483,11 +493,16 @@ class ThumbnailListModel(QAbstractListModel):
human_readable=prev_datetime.humanize(locale=self.arrow_locale)
)
- path, prev_file_name = os.path.split(rpd_file.prev_full_name)
- path += os.sep
- msg += _(
- '<br><br>Previous download:<br>%(filename)s<br>%(path)s<br>%(date)s'
- ) % dict(date=prev_date, filename=prev_file_name, path=path)
+ if rpd_file.prev_full_name != manually_marked_previously_downloaded:
+ path, prev_file_name = os.path.split(rpd_file.prev_full_name)
+ path += os.sep
+ msg += _(
+ '<br><br>Previous download:<br>%(filename)s<br>%(path)s<br>%(date)s'
+ ) % dict(date=prev_date, filename=prev_file_name, path=path)
+ else:
+ msg += _(
+ '<br><br><i>Manually set as previously downloaded on %(date)s</i>'
+ ) % dict(date=prev_date)
return msg
def setData(self, index: QModelIndex, value, role: int) -> bool:
@@ -510,6 +525,47 @@ class ThumbnailListModel(QAbstractListModel):
return True
return False
+ def setDataRange(self, indexes: Tuple[QModelIndex], value, role: int) -> bool:
+ """
+ Modify a range of indexes simultaneously
+ :param indexes: the indexes
+ :param value: new value to assign
+ :param role: the role the value is associated with
+ :return: True
+ """
+ valid_rows = (index.row() for index in indexes if index.isValid())
+ rows = [row for row in valid_rows if 0 <= row < len(self.rows)]
+ rows.sort()
+ uids = [self.rows[row][0] for row in rows]
+
+ if role == Roles.previously_downloaded:
+ logging.debug("Manually setting %s files as previously downloaded", len(uids))
+ # Set the files as unmarked
+ self.tsql.set_list_marked(uids=uids, marked=False)
+ for row, uid in zip(rows, uids):
+ self.rows[row] = (uid, False)
+ # Set the files as previously downloaded
+ self.tsql.set_list_previously_downloaded(uids=uids, previously_downloaded=value)
+ d = DownloadedSQL()
+ now = datetime.datetime.now()
+ for uid in uids:
+ rpd_file = self.rpd_files[uid]
+ rpd_file.previously_downloaded = value
+ rpd_file.prev_full_name = manually_marked_previously_downloaded
+ rpd_file.prev_datetime = now
+ d.add_downloaded_file(
+ name=rpd_file.name, size=rpd_file.size,
+ modification_time=rpd_file.modification_time,
+ download_full_file_name=manually_marked_previously_downloaded
+ )
+ # Update Timeline formatting, if needed
+ self.rapidApp.temporalProximity.previouslyDownloadedManuallySet(uids=uids)
+
+ # Indicate to the list view that the rows have changed
+ for first, last in runs(rows):
+ self.dataChanged.emit(self.index(first, 0), self.index(last, 0))
+ return True
+
def assignJobCodesToMarkedFilesWithNoJobCode(self, job_code: str) -> None:
"""
Called when assigning job codes when a download is initiated and not all
@@ -573,18 +629,20 @@ class ThumbnailListModel(QAbstractListModel):
self.total_thumbs_to_generate += 1
self.no_thumbnails_by_scan[rpd_file.scan_id] += 1
- tr = ThumbnailRow(uid=uid,
- scan_id=rpd_file.scan_id,
- mtime=rpd_file.modification_time,
- marked=not rpd_file.previously_downloaded(),
- file_name=rpd_file.name,
- extension=rpd_file.extension,
- file_type=rpd_file.file_type,
- downloaded=False,
- previously_downloaded=rpd_file.previously_downloaded(),
- job_code=False,
- proximity_col1=-1,
- proximity_col2=-1)
+ tr = ThumbnailRow(
+ uid=uid,
+ scan_id=rpd_file.scan_id,
+ mtime=rpd_file.modification_time,
+ marked=not rpd_file.previously_downloaded,
+ file_name=rpd_file.name,
+ extension=rpd_file.extension,
+ file_type=rpd_file.file_type,
+ downloaded=False,
+ previously_downloaded=rpd_file.previously_downloaded,
+ job_code=False,
+ proximity_col1=-1,
+ proximity_col2=-1
+ )
thumbnail_rows.append(tr)
@@ -1228,24 +1286,6 @@ class ThumbnailListModel(QAbstractListModel):
self.rapidApp.displayMessageInStatusBar()
self.rapidApp.setDownloadCapabilities()
- def visibleRows(self):
- """
- Yield rows visible in viewport. Currently not used.
- """
-
- view = self.rapidApp.thumbnailView
- rect = view.viewport().contentsRect()
- width = view.itemDelegate().width
- last_row = rect.bottomRight().x() // width * width
- top = view.indexAt(rect.topLeft())
- if top.isValid():
- bottom = view.indexAt(QPoint(last_row, rect.bottomRight().y()))
- if not bottom.isValid():
- # take a guess with an arbitrary figure
- bottom = self.index(top.row() + 15)
- for row in range(top.row(), bottom.row() + 1):
- yield row
-
def getTypeCountForProximityCell(self, col1id: Optional[int]=None,
col2id: Optional[int]=None) -> str:
"""
@@ -1270,6 +1310,14 @@ class ThumbnailListModel(QAbstractListModel):
proximity_col2=self.proximity_col2,
marked=marked, file_type=file_type)
+ def getFirstUidFromUidList(self, uids: List[bytes]) -> Optional[bytes]:
+ return self.tsql.get_first_uid_from_uid_list(
+ sort_by=self.sort_by, sort_order=self.sort_order,
+ show=self.show, proximity_col1=self.proximity_col1,
+ proximity_col2=self.proximity_col2,
+ uids=uids
+ )
+
def getDisplayedCount(self, scan_id: Optional[int] = None,
marked: Optional[bool] = None) -> int:
return self.tsql.get_count(scan_id=scan_id, downloaded=False, show=self.show,
@@ -1473,7 +1521,7 @@ class ThumbnailListModel(QAbstractListModel):
return [ThumbnailDataForProximity(uid=rpd_file.uid,
ctime=rpd_file.ctime,
file_type=rpd_file.file_type,
- previously_downloaded=rpd_file.previously_downloaded())
+ previously_downloaded=rpd_file.previously_downloaded)
for rpd_file in self.rpd_files.values()]
def assignProximityGroups(self, col1_col2_uid: List[Tuple[int, int, bytes]]) -> None:
@@ -1507,6 +1555,9 @@ class ThumbnailListModel(QAbstractListModel):
return self.tsql.get_count(marked=True) != self.getDisplayedCount(marked=True)
+ def anyFileNotPreviouslyDownloaded(self, uids: List[bytes]) -> bool:
+ return self.tsql.any_not_previously_downloaded(uids=uids)
+
def getFileDownloadsCompleted(self) -> FileTypeCounter:
"""
:return: counter for how many photos and videos have their downloads completed
@@ -1563,6 +1614,32 @@ class ThumbnailView(QListView):
self.setSpacing(8)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ def setScrollTogether(self, on: bool) -> None:
+ """
+ Turn on or off the linking of scrolling the Timeline with the Thumbnail display.
+
+ Called from the Proximity (Timeline) widget
+
+ :param on: whether to turn on or off
+ """
+
+ if on:
+ self.verticalScrollBar().valueChanged.connect(self.scrollTimeline)
+ else:
+ self.verticalScrollBar().valueChanged.disconnect(self.scrollTimeline)
+
+ def _scrollTemporalProximity(self, row: Optional[int]=None,
+ index: Optional[QModelIndex]=None) -> None:
+ temporalProximity = self.rapidApp.temporalProximity
+ temporalProximity.setScrollTogether(False)
+ if row is None:
+ row = index.row()
+ model = self.model()
+ rows = model.rows
+ uid = rows[row][0]
+ temporalProximity.scrollToUid(uid=uid)
+ temporalProximity.setScrollTogether(True)
+
@pyqtSlot(QMouseEvent)
def mousePressEvent(self, event: QMouseEvent) -> None:
"""
@@ -1582,7 +1659,8 @@ class ThumbnailView(QListView):
checkbox_clicked = False
index = self.indexAt(event.pos())
- if index.row() >= 0:
+ row = index.row()
+ if row >= 0:
rect = self.visualRect(index) # type: QRect
delegate = self.itemDelegate(index) # type: ThumbnailDelegate
checkboxRect = delegate.getCheckBoxRect(rect)
@@ -1592,8 +1670,61 @@ class ThumbnailView(QListView):
checkbox_clicked = status not in Downloaded
if not checkbox_clicked:
+ if self.rapidApp.prefs.auto_scroll and row >= 0:
+ self._scrollTemporalProximity(row=row)
super().mousePressEvent(event)
+ @pyqtSlot(int)
+ def scrollTimeline(self, value) -> None:
+ index = self.indexAt(self.topLeft()) # type: QModelIndex
+ if index.isValid():
+ self._scrollTemporalProximity(index=index)
+
+ def topLeft(self) -> QPoint:
+ return QPoint(thumbnail_margin, thumbnail_margin)
+
+ def visibleRows(self):
+ """
+ Yield rows visible in viewport. Not currently used or properly tested.
+ """
+
+ rect = self.viewport().contentsRect()
+ width = self.itemDelegate().width
+ last_row = rect.bottomRight().x() // width * width
+ topLeft = rect.topLeft() + QPoint(10, 10)
+ top = self.indexAt(topLeft)
+ if top.isValid():
+ bottom = self.indexAt(QPoint(last_row, rect.bottomRight().y()))
+ if not bottom.isValid():
+ # take a guess with an arbitrary figure
+ bottom = self.index(top.row() + 15)
+ for row in range(top.row(), bottom.row() + 1):
+ yield row
+
+ def scrollToUids(self, uids: List[bytes]) -> None:
+ """
+ Scroll the Thumbnail Display to the first visible uid from the list of uids.
+
+ Remember not all uids are necessarily visible in the Thumbnail Display,
+ because of filtering.
+
+ :param uids: list of uids to scroll to
+ """
+ model = self.model() # type: ThumbnailListModel
+ if self.rapidApp.showOnlyNewFiles():
+ uid = model.getFirstUidFromUidList(uids=uids)
+ if uid is None:
+ return
+ else:
+ uid = uids[0]
+ 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):
"""
@@ -1619,8 +1750,8 @@ class ThumbnailDelegate(QStyledItemDelegate):
self.image_width = max(ThumbnailSize.width, ThumbnailSize.height)
self.image_height = self.image_width
- self.horizontal_margin = 10
- self.vertical_margin = 10
+ self.horizontal_margin = thumbnail_margin
+ self.vertical_margin = thumbnail_margin
self.image_footer = self.checkbox_size
self.footer_padding = 5
@@ -1641,9 +1772,17 @@ 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
+ # Rapid Photo Downloader that photos and videos have been previously downloaded by
+ # another application.
+ self.markFileDownloadedAct = self.contextMenu.addAction(_('Mark File as Downloaded'))
+ self.markFileDownloadedAct.triggered.connect(self.doMarkFileDownloadedAct)
+ # Translators: 'Files' here applies to two or more files
+ self.markFilesDownloadedAct = self.contextMenu.addAction(_('Mark Files as Downloaded'))
+ self.markFilesDownloadedAct.triggered.connect(self.doMarkFileDownloadedAct)
# store the index in which the user right clicked
self.clickedIndex = None # type: QModelIndex
@@ -1711,14 +1850,26 @@ 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:
+ selectedIndexes = self.selectedIndexes()
+ if selectedIndexes is None:
+ return
+ not_downloaded = tuple(
+ index for index in selectedIndexes if not index.data(Roles.previously_downloaded)
+ ) # type: Tuple[QModelIndex]
+ thumbnailModel = self.rapidApp.thumbnailModel # type: ThumbnailListModel
+ thumbnailModel.setDataRange(not_downloaded, True, Roles.previously_downloaded)
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
if index is None:
@@ -1745,7 +1896,7 @@ class ThumbnailDelegate(QStyledItemDelegate):
x = option.rect.x()
y = option.rect.y()
- # Draw recentangle in which the individual items will be placed
+ # Draw rectangle in which the individual items will be placed
boxRect = QRect(x, y, self.width, self.height)
shadowRect = QRect(x + self.shadow_size, y + self.shadow_size,
self.width, self.height)
@@ -1761,10 +1912,12 @@ class ThumbnailDelegate(QStyledItemDelegate):
painter.fillRect(boxRect, self.paleGray)
if is_selected:
- hightlightRect = QRect(boxRect.left() + self.highlight_offset,
- boxRect.top() + self.highlight_offset,
- boxRect.width() - self.highlight_size,
- boxRect.height() - self.highlight_size)
+ hightlightRect = QRect(
+ boxRect.left() + self.highlight_offset,
+ boxRect.top() + self.highlight_offset,
+ boxRect.width() - self.highlight_size,
+ boxRect.height() - self.highlight_size
+ )
painter.setPen(self.highlightPen)
painter.drawRect(hightlightRect)
@@ -1919,8 +2072,29 @@ class ThumbnailDelegate(QStyledItemDelegate):
painter.restore()
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
- return QSize(self.width + self.shadow_size, self.height
- + self.shadow_size)
+ return QSize(
+ self.width + self.shadow_size, self.height + self.shadow_size
+ )
+
+ def oneOrMoreNotDownloaded(self) -> Tuple[int, Plural]:
+ i = 0
+ selectedIndexes = self.selectedIndexes()
+ 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
+ elif i == 1:
+ return noSelected, Plural.two_form_single
+ else:
+ return noSelected, Plural.two_form_plural
def editorEvent(self, event: QEvent,
model: QAbstractItemModel,
@@ -1938,6 +2112,28 @@ class ThumbnailDelegate(QStyledItemDelegate):
QEvent.MouseButtonDblClick):
if event.button() == Qt.RightButton:
self.clickedIndex = index
+
+ # Determine if user can manually mark file or files as previously downloaded
+ noSelected, noDownloaded = self.oneOrMoreNotDownloaded()
+ if noDownloaded == Plural.two_form_single:
+ self.markFilesDownloadedAct.setVisible(False)
+ self.markFileDownloadedAct.setVisible(True)
+ self.markFileDownloadedAct.setEnabled(True)
+ elif noDownloaded == Plural.two_form_plural:
+ self.markFilesDownloadedAct.setVisible(True)
+ self.markFilesDownloadedAct.setEnabled(True)
+ self.markFileDownloadedAct.setVisible(False)
+ else:
+ assert noDownloaded == Plural.zero
+ if noSelected == 1:
+ self.markFilesDownloadedAct.setVisible(False)
+ self.markFileDownloadedAct.setVisible(True)
+ self.markFileDownloadedAct.setEnabled(False)
+ else:
+ self.markFilesDownloadedAct.setVisible(True)
+ self.markFilesDownloadedAct.setEnabled(False)
+ self.markFileDownloadedAct.setVisible(False)
+
globalPos = self.rapidApp.thumbnailView.viewport().mapToGlobal(event.pos())
# libgphoto2 needs exclusive access to the camera, so there are times when "open
# in file browswer" should be disabled:
@@ -2014,12 +2210,17 @@ class ThumbnailDelegate(QStyledItemDelegate):
def applyJobCode(self, job_code: str) -> None:
thumbnailModel = self.rapidApp.thumbnailModel # type: ThumbnailListModel
- selection = self.rapidApp.thumbnailView.selectionModel() # type: QItemSelectionModel
- if selection.hasSelection():
- selected = selection.selection() # type: QItemSelection
- selectedIndexes = selected.indexes()
+ selectedIndexes = self.selectedIndexes()
+ if selectedIndexes is not None:
logging.debug("Applying job code to %s files", len(selectedIndexes))
for i in selectedIndexes:
thumbnailModel.setData(i, job_code, Roles.job_code)
else:
- logging.debug("Not applying job code because no files selected") \ No newline at end of file
+ logging.debug("Not applying job code because no files selected")
+
+ def selectedIndexes(self) -> Optional[List[QModelIndex]]:
+ selection = self.rapidApp.thumbnailView.selectionModel() # type: QItemSelectionModel
+ if selection.hasSelection():
+ selected = selection.selection() # type: QItemSelection
+ return selected.indexes()
+ return None \ No newline at end of file