summaryrefslogtreecommitdiff
path: root/raphodo/proximity.py
diff options
context:
space:
mode:
Diffstat (limited to 'raphodo/proximity.py')
-rw-r--r--raphodo/proximity.py217
1 files changed, 204 insertions, 13 deletions
diff --git a/raphodo/proximity.py b/raphodo/proximity.py
index 5821624..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
@@ -37,25 +37,27 @@ from gettext import gettext as _
from PyQt5.QtCore import (
QAbstractTableModel, QModelIndex, Qt, QSize, QRect, QItemSelection, QItemSelectionModel,
- QBuffer, QIODevice, pyqtSignal, pyqtSlot, QRectF
+ QBuffer, QIODevice, pyqtSignal, pyqtSlot, QRectF, QPoint,
)
from PyQt5.QtWidgets import (
QTableView, QStyledItemDelegate, QSlider, QLabel, QVBoxLayout, QStyleOptionViewItem, QStyle,
- QAbstractItemView, QWidget, QHBoxLayout, QSizePolicy, QSplitter, QScrollArea, QStackedWidget
+ QAbstractItemView, QWidget, QHBoxLayout, QSizePolicy, QSplitter, QScrollArea, QStackedWidget,
+ QToolButton, QAction
)
from PyQt5.QtGui import (
- QPainter, QFontMetrics, QFont, QColor, QGuiApplication, QPixmap, QPalette, QMouseEvent
+ QPainter, QFontMetrics, QFont, QColor, QGuiApplication, QPixmap, QPalette, QMouseEvent, QIcon
)
-from raphodo.viewutils import QFramedWidget, QFramedLabel
from raphodo.constants import (
FileType, Align, proximity_time_steps, TemporalProximityState, fileTypeColor, CustomColors,
DarkGray, MediumGray, DoubleDarkGray
)
from raphodo.rpdfile import FileTypeCounter
from raphodo.preferences import Preferences
-from raphodo.viewutils import ThumbnailDataForProximity
+from raphodo.viewutils import ThumbnailDataForProximity, QFramedWidget, QFramedLabel
from raphodo.timeutils import locale_time, strip_zero, make_long_date_format, strip_am, strip_pm
+from raphodo.utilities import runs
+from raphodo.constants import Roles
ProximityRow = namedtuple(
'ProximityRow', 'year, month, weekday, day, proximity, new_file, tooltip_date_col0, '
@@ -564,8 +566,9 @@ class MetaUid:
"""
def __init__(self):
- self._uids = tuple({} for i in (0,1,2)) # type: Tuple[Dict[int, List[bytes, ...]]]
- self._no_uids = tuple({} for i in (0,1,2)) # type: Tuple[Dict[int, int]]
+ self._uids = tuple({} for i in (0, 1 ,2)) # type: Tuple[Dict[int, List[bytes, ...]]]
+ self._no_uids = tuple({} for i in (0, 1, 2)) # type: Tuple[Dict[int, int]]
+ self._col2_row_index = dict() # type: Dict[bytes, int]
def __repr__(self):
return 'MetaUid(%r %r)' % (self._no_uids, self._uids)
@@ -575,6 +578,8 @@ class MetaUid:
assert row not in self._uids[col]
self._uids[col][row] = uids
self._no_uids[col][row] = len(uids)
+ for uid in uids:
+ self._col2_row_index[uid] = row
def __getitem__(self, key: Tuple[int, int]) -> List[bytes]:
row, col = key
@@ -583,9 +588,12 @@ class MetaUid:
def trim(self) -> None:
"""
Remove unique ids unnecessary for table viewing.
+
+ Don't, however, remove ids in col 2, as they're useful, e.g.
+ when manually marking a file as previously downloaded
"""
- for col in (0, 1, 2):
+ for col in (0, 1):
for row in self._uids[col]:
uids = self._uids[col][row]
if len(uids) > 1:
@@ -602,6 +610,9 @@ class MetaUid:
def uids(self, column: int) -> Dict[int, List[bytes]]:
return self._uids[column]
+ def uid_to_col2_row(self, uid) -> int:
+ return self._col2_row_index[uid]
+
def validate_rows(self, no_rows) -> Tuple[int]:
"""
Very simple validation test to see if all rows are present
@@ -849,7 +860,7 @@ class TemporalProximityGroups:
thumbnail_index += len(uids_by_day_in_proximity_group[group_no][0])
# For any proximity groups that span more than one Timeline row because they span
- # more than one calander day, add the day to the Timeline, with blank values
+ # more than one calender day, add the day to the Timeline, with blank values
# for the proximity group (column 2).
i = 0
for y_m_d, day in uids_by_day_in_proximity_group[group_no][1:]:
@@ -1005,6 +1016,9 @@ class TemporalProximityGroups:
def __getitem__(self, row_number) -> ProximityRow:
return self.rows[row_number]
+ def __setitem__(self, row_number, proximity_row: ProximityRow) -> None:
+ self.rows[row_number] = proximity_row
+
def __iter__(self):
return iter(self.rows)
@@ -1033,6 +1047,12 @@ class TemporalProximityGroups:
return self.uids.validate_rows(len(self.rows))
+ def uid_to_row(self, uid: bytes) -> int:
+ return self.uids.uid_to_col2_row(uid=uid)
+
+ def row_uids(self, row: int) -> List[bytes]:
+ return self.uids[row, 2]
+
def base64_thumbnail(pixmap: QPixmap, size: QSize) -> str:
"""
@@ -1103,6 +1123,11 @@ class TemporalProximityModel(QAbstractTableModel):
else:
return proximity_row.proximity, proximity_row.new_file, invalid_row, invalid_rows
+ elif role == Roles.uids:
+ prow = self.groups.row_span_for_column_starts_at_row[(row, 2)]
+ uids = self.groups.uids.uids(2)[prow]
+ return uids
+
elif role == Qt.ToolTipRole:
thumbnails = self.rapidApp.thumbnailModel.thumbnails
@@ -1173,6 +1198,34 @@ class TemporalProximityModel(QAbstractTableModel):
files = ', '.join((thumbnailModel.rpd_files[uid].name for uid in uids))
logging.debug('Col {}: {}'.format(col, files))
+ def updatePreviouslyDownloaded(self, uids: List[bytes]) -> None:
+ """
+ Examine Timeline data to see if any Timeline rows should have their column 2
+ formatting updated to reflect that there are no new files to be downloaded in
+ that particular row
+ :param uids: list of uids that have been manually marked as previously downloaded
+ """
+
+ processed_rows = set() # type: Set[int]
+ rows_to_update = []
+ for uid in uids:
+ row = self.groups.uid_to_row(uid=uid)
+ if row not in processed_rows:
+ processed_rows.add(row)
+ row_uids = self.groups.row_uids(row)
+ logging.debug(
+ 'Examining row %s to see if any have not been previously downloaded', row
+ )
+ if not self.rapidApp.thumbnailModel.anyFileNotPreviouslyDownloaded(uids=row_uids):
+ proximity_row = self.groups[row] # type: ProximityRow
+ self.groups[row] = proximity_row._replace(new_file=False)
+ rows_to_update.append(row)
+ logging.debug('Row %s will be updated to show it has no new files')
+
+ if rows_to_update:
+ for first, last in runs(rows_to_update):
+ self.dataChanged.emit(self.index(first, 2), self.index(last, 2))
+
class TemporalProximityDelegate(QStyledItemDelegate):
"""
@@ -1496,6 +1549,10 @@ class TemporalProximityView(QTableView):
case.
"""
+ # auto_scroll = self.temporalProximityWidget.prefs.auto_scroll
+ # if auto_scroll:
+ # self.temporalProximityWidget.setTimelineThumbnailAutoScroll(False)
+
self.selectionModel().blockSignals(True)
model = self.model() # type: TemporalProximityModel
@@ -1522,6 +1579,9 @@ class TemporalProximityView(QTableView):
self.selectionModel().blockSignals(False)
+ # if auto_scroll:
+ # self.temporalProximityWidget.setTimelineThumbnailAutoScroll(True)
+
@pyqtSlot(QMouseEvent)
def mousePressEvent(self, event: QMouseEvent) -> None:
"""
@@ -1539,6 +1599,7 @@ class TemporalProximityView(QTableView):
:param event: the mouse click event
"""
+
do_selection = True
do_selection_confirmed = False
index = self.indexAt(event.pos()) # type: QModelIndex
@@ -1566,17 +1627,39 @@ class TemporalProximityView(QTableView):
self.clearSelection()
self.rapidApp.proximityButton.setHighlighted(False)
do_selection = False
+ thumbnailView = self.rapidApp.thumbnailView
+ model = self.model()
+ uids = model.data(index, Roles.uids)
+ thumbnailView.scrollToUids(uids=uids)
if do_selection:
self.temporalProximityWidget.block_update_device_display = True
super().mousePressEvent(event)
+
@pyqtSlot(QMouseEvent)
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
self.temporalProximityWidget.block_update_device_display = False
self.proximitySelectionHasChanged.emit()
super().mouseReleaseEvent(event)
+ @pyqtSlot(int)
+ def scrollThumbnails(self, value) -> None:
+ index = self.indexAt(QPoint(200, 0)) # type: QModelIndex
+ if index.isValid():
+ if self.selectedIndexes():
+ # It's now possible to scroll the Timeline and there will be
+ # no matching thumbnails to which to scroll to in the display,
+ # because they are not being displayed. Hence this check:
+ if not index in self.selectedIndexes():
+ return
+ thumbnailView = self.rapidApp.thumbnailView
+ thumbnailView.setScrollTogether(False)
+ model = self.model()
+ uids = model.data(index, Roles.uids)
+ thumbnailView.scrollToUids(uids=uids)
+ thumbnailView.setScrollTogether(True)
+
class TemporalValuePicker(QWidget):
"""
@@ -1690,6 +1773,8 @@ class TemporalProximity(QWidget):
self.state = TemporalProximityState.empty
+ self.uids_manually_set_previously_downloaded = [] # type: List[bytes]
+
self.temporalProximityView = TemporalProximityView(self, rapidApp=rapidApp)
self.temporalProximityModel = TemporalProximityModel(rapidApp=rapidApp)
self.temporalProximityView.setModel(self.temporalProximityModel)
@@ -1793,14 +1878,40 @@ class TemporalProximity(QWidget):
TemporalProximityState.generated: 4
}
+ self.autoScrollButton = QToolButton(self)
+ icon = QIcon(':/icons/link.svg')
+ self.autoScrollButton.setIcon(icon)
+ # self.autoScrollButton.setIconSize(QSize(16, 16))
+ self.autoScrollButton.setAutoRaise(True)
+ self.autoScrollButton.setCheckable(True)
+ self.autoScrollButton.setToolTip(
+ _('Toggle synchronizing Timeline and thumbnail scrolling (Ctrl-T)')
+ )
+ self.autoScrollButton.setChecked(not self.prefs.auto_scroll)
+ self.autoScrollAct = QAction(
+ '', self, shortcut="Ctrl+T",
+ triggered=self.autoScrollActed, icon=icon
+ )
+ self.autoScrollButton.addAction(self.autoScrollAct)
+ style = "QToolButton {padding: 2px;} QToolButton::menu-indicator {image: none;}"
+ self.autoScrollButton.setStyleSheet(style)
+ self.autoScrollButton.clicked.connect(self.autoScrollClicked)
+
+ pickerLayout = QHBoxLayout()
+ pickerLayout.setSpacing(0)
+ pickerLayout.addWidget(self.temporalValuePicker)
+ pickerLayout.addWidget(self.autoScrollButton)
+
layout.addWidget(self.stackedWidget)
- layout.addWidget(self.temporalValuePicker)
+ layout.addLayout(pickerLayout)
self.stackedWidget.setCurrentIndex(0)
self.temporalValuePicker.valueChanged.connect(self.temporalValueChanged)
+ if self.prefs.auto_scroll:
+ self.setTimelineThumbnailAutoScroll(self.prefs.auto_scroll)
- self.another_generation_needed = False
+ self.suppress_auto_scroll_after_timeline_select = False
@pyqtSlot(QItemSelection, QItemSelection)
def proximitySelectionChanged(self, current: QItemSelection, previous: QItemSelection) -> None:
@@ -1842,6 +1953,8 @@ class TemporalProximity(QWidget):
if not self.block_update_device_display:
self.proximitySelectionHasChanged.emit()
+ self.suppress_auto_scroll_after_timeline_select = True
+
def clearThumbnailDisplayFilter(self):
self.thumbnailModel.setProximityGroupFilter([],[])
self.rapidApp.proximityButton.setHighlighted(False)
@@ -1880,6 +1993,13 @@ class TemporalProximity(QWidget):
self.state = state
def setGroups(self, proximity_groups: TemporalProximityGroups) -> bool:
+ """
+ Display the Timeline using data from the generated proximity_groups
+ :param proximity_groups: Timeline content and formatting hints
+ :return: True if Timeline was updated, False if not updated due to
+ current state
+ """
+
if self.state == TemporalProximityState.regenerate:
self.rapidApp.generateTemporalProximityTableData(
reason="a change was made while it was already generating"
@@ -1926,6 +2046,15 @@ class TemporalProximity(QWidget):
self.temporalProximityView.setMinimumWidth(min_width + scrollbar_width + frame_width)
self.setState(TemporalProximityState.generated)
+
+ # Has the user manually set any files as previously downloaded while the Timeline was
+ # generating?
+ if self.uids_manually_set_previously_downloaded:
+ self.temporalProximityModel.updatePreviouslyDownloaded(
+ uids=self.uids_manually_set_previously_downloaded
+ )
+ self.uids_manually_set_previously_downloaded = []
+
return True
@pyqtSlot(int)
@@ -1937,3 +2066,65 @@ class TemporalProximity(QWidget):
reason="the duration between consecutive shots has changed")
elif self.state == TemporalProximityState.generating:
self.state = TemporalProximityState.regenerate
+
+ def previouslyDownloadedManuallySet(self, uids: List[bytes]) -> None:
+ """
+ Possibly update the formatting of the Timeline to reflect the user
+ manually setting files to have been previously downloaded
+ """
+
+ logging.debug(
+ "Updating Timeline to reflect %s files manually set as previously downloaded",
+ len(uids)
+ )
+ if self.state != TemporalProximityState.generated:
+ self.uids_manually_set_previously_downloaded.extend(uids)
+ else:
+ self.temporalProximityModel.updatePreviouslyDownloaded(uids=uids)
+
+ def scrollToUid(self, uid: bytes) -> None:
+ """
+ Scroll to this uid in the Timeline.
+
+ :param uid: uid to scroll to
+ """
+
+ if self.state == TemporalProximityState.generated:
+ if self.suppress_auto_scroll_after_timeline_select:
+ self.suppress_auto_scroll_after_timeline_select = False
+ else:
+ view = self.temporalProximityView
+ model = self.temporalProximityModel
+ row = model.groups.uid_to_row(uid=uid)
+ index = model.index(row, 2)
+ view.scrollTo(index, QAbstractItemView.PositionAtTop)
+
+ def setTimelineThumbnailAutoScroll(self, on: bool) -> None:
+ """
+ Turn on or off synchronized scrolling between thumbnails and Timeline
+ :param on: whether to turn on or off
+ """
+
+ self.setScrollTogether(on)
+ self.rapidApp.thumbnailView.setScrollTogether(on)
+
+ def setScrollTogether(self, on: bool) -> None:
+ """
+ Turn on or off the linking of scrolling the Timeline with the Thumbnail display
+ :param on: whether to turn on or off
+ """
+
+ view = self.temporalProximityView
+ if on:
+ view.verticalScrollBar().valueChanged.connect(view.scrollThumbnails)
+ else:
+ view.verticalScrollBar().valueChanged.disconnect(view.scrollThumbnails)
+
+ @pyqtSlot(bool)
+ def autoScrollClicked(self, checked: bool) -> None:
+ self.prefs.auto_scroll = not checked
+ self.setTimelineThumbnailAutoScroll(not checked)
+
+ @pyqtSlot(bool)
+ def autoScrollActed(self, on: bool) -> None:
+ self.autoScrollButton.animateClick()