diff --git a/client/ayon_core/tools/loader/ui/_review_table.py b/client/ayon_core/tools/loader/ui/_review_table.py index 1e6d487d319..1149493641b 100644 --- a/client/ayon_core/tools/loader/ui/_review_table.py +++ b/client/ayon_core/tools/loader/ui/_review_table.py @@ -277,7 +277,6 @@ def _on_display_type_changed(self, display_type: str) -> None: if active is self._table: self._eagerly_enqueue_visible_thumbnails() - self.display_type_changed.emit(active) def _on_group_by_options_changed( diff --git a/client/ayon_core/tools/loader/ui/review_inspector.py b/client/ayon_core/tools/loader/ui/review_inspector.py index 2160fc390e6..62b16b0669c 100644 --- a/client/ayon_core/tools/loader/ui/review_inspector.py +++ b/client/ayon_core/tools/loader/ui/review_inspector.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time from collections import defaultdict from ayon_ui_qt.components.buttons import AYButton @@ -53,6 +54,16 @@ def __init__( self._current_thumb_key: str = "" # Use a dict as an ordered set to track the currently selected indices self._current_selection: dict[QtCore.QModelIndex, None] = {} + # Track if mouse is currently pressed for drag selection + self._mouse_pressed: bool = False + # Timer for throttling updates during drag selection + self._update_timer = QtCore.QTimer() + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._throttled_update) + # Interval for throttling updates (in milliseconds) + self._base_update_time: int = 0 + # Flag to indicate if the first update has been measured + self._first_update_measured: bool = False self._build() @@ -168,6 +179,8 @@ def set_view(self, view: QtWidgets.QAbstractItemView) -> None: self._on_selection_changed ) self._view.model().modelReset.disconnect(self._on_model_reset) + # Remove event filter from previous view's viewport + self._view.viewport().removeEventFilter(self) except (RuntimeError, TypeError): pass @@ -175,12 +188,54 @@ def set_view(self, view: QtWidgets.QAbstractItemView) -> None: self._view.activated.connect(self._on_activated) self._view.selection_changed.connect(self._on_selection_changed) self._view.model().modelReset.connect(self._on_model_reset) + # Install event filter on the viewport to track mouse press/release + self._view.viewport().installEventFilter(self) + + def _throttled_update(self) -> None: + """Throttled update method called by the timer during drag selection.""" + if self._mouse_pressed: + start_time = time.time() + self._update() + elapsed_time_ms = (time.time() - start_time) * 1000 + num_selected = len(self._current_selection) + if not self._first_update_measured and num_selected > 0: + # Calculate the update interval based on the first update time + self._base_update_time = max( + 16, + int(elapsed_time_ms / num_selected), + ) + self._first_update_measured = True + # print(f"base update time: {self._base_update_time} ms") + self._update_timer.start(max(16, self._base_update_time * num_selected)) def _on_model_reset(self) -> None: """Clear the selection when the model is reset.""" self._current_selection.clear() self._update() + def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: + """Track mouse press/release events on the view's viewport. + + Args: + obj: The watched object. + event: The event. + + Returns: + True if the event was handled, False otherwise. + """ + if obj is self._view.viewport(): + if event.type() == QtCore.QEvent.Type.MouseButtonPress: + self._mouse_pressed = True + # Trigger an immediate update on first mouse down + self._throttled_update() + elif event.type() == QtCore.QEvent.Type.MouseButtonRelease: + self._mouse_pressed = False + self._update_timer.stop() + # Trigger update on mouse release for finalized selection + if self.isVisible(): + self._update() + return super().eventFilter(obj, event) + def _on_activated(self, index: QtCore.QModelIndex) -> None: """Activation is typically when the user double-clicks on an item. We want to show the inspector in this case if it's not visible. @@ -206,7 +261,10 @@ def _on_selection_changed( for idx in deselected.indexes(): if idx.column() == 0: self._current_selection.pop(idx, None) - self._update() + # Only update immediately if mouse is not pressed (keyboard selection) + # Otherwise, update will be triggered on mouse release + if not self._mouse_pressed: + self._update() def _update(self) -> None: """Update the inspector with the current selection, if visible.""" @@ -258,6 +316,7 @@ def _update(self) -> None: version_ids.append(vid) if thumb_keys: + thumb_keys = sorted(thumb_keys) # limit cache misses self._load_thumbnail(thumb_keys) else: self._current_thumb_key = ""