diff --git a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py index 35bb9dfcc..9fac454e8 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -68,6 +68,7 @@ def __init__(self) -> None: self._cache_dir = Path(tempfile.gettempdir()) / f"rv_thumbnails_{os.getpid()}" self._cache_dir.mkdir(parents=True, exist_ok=True) self._in_flight: set[str] = set() + # Cache key to set of source node names self._cache_key_to_sources: dict[str, set[str]] = {} self._deferred_sources: set[str] = set() self._deferred_jobs: list[tuple[str, str, str, str]] = [] @@ -78,7 +79,7 @@ def __init__(self) -> None: self._loading_active = False self._display_preview = False if os.getenv("RV_SESSION_MANAGER_USE_THUMBNAILS") == "0" else True self._shutting_down = False - self._active_procs: list[subprocess.Popen] = [] + self._active_procs: list[tuple[subprocess.Popen, str]] = [] self._procs_lock = threading.Lock() self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) @@ -106,6 +107,16 @@ def global_bindings(self) -> list[tuple[str, Any, str]]: self._on_session_deletion, "Delete all cached local filmstrips and thumbnails on RV close", ), + ( + "before-clear-session", + self._on_clear_session, + "Cancel in-flight generation and evict cache when the session is cleared", + ), + ( + "before-source-delete", + self._on_source_delete, + "Cancel in-flight generation and evict cache when a media source is removed", + ), ( "play-start", self._on_play_start, @@ -152,6 +163,9 @@ def _get_cached_path(self, event: Any, path_key: str) -> None: return cache_key = self._cache_key(media_path) + + self._cache_key_to_sources.setdefault(cache_key, set()).add(source_node) + cached = self._cache.get(cache_key, {}) path = cached.get(path_key) @@ -159,8 +173,6 @@ def _get_cached_path(self, event: Any, path_key: str) -> None: event.setReturnContent(str(path)) return - self._cache_key_to_sources.setdefault(cache_key, set()).add(source_node) - flight_key = f"{cache_key}_{path_key}" if flight_key not in self._in_flight: self._start_generation(source_node, cache_key, media_path, path_key) @@ -231,6 +243,19 @@ def _get_media_path(self, source_node: str) -> str | None: logger.warning(f"Could not get media path: {e}") return None + def _source_node_of_group(self, group: str) -> str | None: + """ + RVSourceGroup nodes have at most 1 RVFileSource or RVImageSource child (as a leaf), which is the actual media source. + Find it and return its node name. + """ + try: + for node in commands.nodesInGroup(group): + if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): + return node + except Exception: + return + return None + def _get_source_info(self, source_node: str) -> tuple[int, int, int, int] | None: # Skip inactive media representations if not commands.getIntProperty(f"{source_node}.media.active")[0]: @@ -396,7 +421,7 @@ def _write_filmstrip_session( return output_width, output_height - def _run_suspendable(self, cmd: list[str], timeout: int = 120) -> None: + def _run_suspendable(self, cmd: list[str], cache_key: str, timeout: int = 120) -> None: """Run a subprocess that can be suspended/resumed during playback. The timeout counts only non-suspended wall-clock time: while the @@ -406,7 +431,7 @@ def _run_suspendable(self, cmd: list[str], timeout: int = 120) -> None: creationflags = subprocess.CREATE_NO_WINDOW if _IS_WIN32 else 0 proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=creationflags) with self._procs_lock: - self._active_procs.append(proc) + self._active_procs.append((proc, cache_key)) if self._should_defer(): _suspend_proc(proc) deadline = time.monotonic() + timeout @@ -427,7 +452,7 @@ def _run_suspendable(self, cmd: list[str], timeout: int = 120) -> None: finally: with self._procs_lock: try: - self._active_procs.remove(proc) + self._active_procs.remove((proc, cache_key)) except ValueError: logger.warning(f"Process {proc.pid} was not in active processes list") @@ -437,6 +462,7 @@ def _generate_thumbnail(self, cache_key: str, rvio_bin: str, media_path: str, mi try: self._run_suspendable( [rvio_bin, media_path, "-t", str(mid_frame), "-o", str(output_path)], + cache_key, ) except Exception as e: logger.error(f"Thumbnail generation failed: {e}") @@ -477,6 +503,7 @@ def _generate_filmstrip( "-o", str(output_path), ], + cache_key, ) except Exception as e: logger.error(f"Filmstrip generation failed: {e}") @@ -531,7 +558,7 @@ def _on_play_start(self, event: Any) -> None: self._playback_active = True if should_defer: return - for proc in self._active_procs: + for proc, _ in self._active_procs: _suspend_proc(proc) def _on_play_stop(self, event: Any) -> None: @@ -543,18 +570,19 @@ def _on_play_stop(self, event: Any) -> None: with self._procs_lock: self._playback_active = False if not self._should_defer(): - for proc in self._active_procs: + for proc, _ in self._active_procs: _resume_proc(proc) self._drain_one() def _on_loading_start(self, event: Any) -> None: event.reject() + self._shutting_down = False with self._procs_lock: should_defer = self._should_defer() self._loading_active = True if should_defer: return - for proc in self._active_procs: + for proc, _ in self._active_procs: _suspend_proc(proc) def _on_loading_stop(self, event: Any) -> None: @@ -562,7 +590,7 @@ def _on_loading_stop(self, event: Any) -> None: with self._procs_lock: self._loading_active = False if not self._should_defer(): - for proc in self._active_procs: + for proc, _ in self._active_procs: _resume_proc(proc) self._drain_one() @@ -573,7 +601,7 @@ def _on_previews_disabled(self, event: Any) -> None: self._display_preview = False if should_defer: return - for proc in self._active_procs: + for proc, _ in self._active_procs: _suspend_proc(proc) def _on_previews_enabled(self, event: Any) -> None: @@ -581,15 +609,36 @@ def _on_previews_enabled(self, event: Any) -> None: with self._procs_lock: self._display_preview = True if not self._should_defer(): - for proc in self._active_procs: + for proc, _ in self._active_procs: _resume_proc(proc) self._drain_one() + def _on_clear_session(self, event: Any) -> None: + """Cancel in-flight generation and evict all caches when the session is cleared.""" + event.reject() + self._shutting_down = True + self._pool.shutdown(wait=False, cancel_futures=True) + self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) + self._in_flight.clear() + self._deferred_jobs.clear() + self._cache_key_to_sources.clear() + self._deferred_sources.clear() + self._cache.clear() + with self._procs_lock: + procs_to_terminate = list(self._active_procs) + for proc, _ in procs_to_terminate: + _resume_proc(proc) + try: + proc.kill() + proc.wait() + except OSError: + logger.warning(f"Failed to kill process {proc}") + def _on_session_deletion(self, event: Any) -> None: event.reject() self._shutting_down = True with self._procs_lock: - for proc in self._active_procs: + for proc, _ in self._active_procs: _resume_proc(proc) try: proc.terminate() @@ -606,6 +655,58 @@ def _on_session_deletion(self, event: Any) -> None: logger.warning(f"Failed to delete cache directory {self._cache_dir}: {e}") self._cache.clear() + def _on_source_delete(self, event: Any) -> None: + """Cancel generation immediately and evict the cache for a removed media source.""" + event.reject() + + node = event.contents() + + if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): + source_node = node + else: + source_node = self._source_node_of_group(node) + if not source_node: + return + + media_path = self._get_media_path(source_node) + if not media_path: + return + + cache_key = self._cache_key(media_path) + + self._deferred_sources.discard(source_node) + + sources = self._cache_key_to_sources.get(cache_key) + if sources is not None: + sources.discard(source_node) + if sources: + return + self._cache_key_to_sources.pop(cache_key, None) + + # Kill any running rvio proc generating for this media. + with self._procs_lock: + for proc, proc_cache_key in self._active_procs: + if proc_cache_key == cache_key: + # Can't reliably kill a stopped proc, so resume before killing + _resume_proc(proc) + try: + proc.terminate() + except OSError: + logger.warning(f"Failed to terminate process {proc.pid}") + + self._deferred_jobs = [job for job in self._deferred_jobs if job[1] != cache_key] + + self._in_flight.discard(f"{cache_key}_thumbnail_path") + self._in_flight.discard(f"{cache_key}_filmstrip_path") + + cached = self._cache.pop(cache_key, {}) + for path in cached.values(): + if path: + try: + Path(path).unlink(missing_ok=True) + except Exception as e: + logger.warning(f"Failed to delete cached preview {path}: {e}") + def createMode() -> LocalThumbnailGen: global the_mode diff --git a/src/plugins/rv-packages/session_manager/session_manager.mu.in b/src/plugins/rv-packages/session_manager/session_manager.mu.in index b2ff76813..0d71a50bc 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1065,6 +1065,134 @@ class: EventFilter : QObject } } +class: ThumbnailManager +{ + QTimer renderTimer; + string[] renderQueue; + SourcePreviewWidget[] progressiveRenderPreviews; + int renderIdx; + string[] progressiveRenderDeferred; +} + +documentation: """ +ThumbnailRenderer keeps track of the sources and inputs to render in +the session manager. Source and input nodes are rendered progressively +using QT single shot timers, hence the events are placed on the QT event +queue. +"""; +class: ThumbnailRenderer +{ + // source and input each get their own render queue/timer/state + ThumbnailManager _source; + ThumbnailManager _input; + QDockWidget _dockWidget; + QIcon _fallbackSourceIcon; + + + method: ThumbnailRenderer (ThumbnailRenderer;) { + + let m = mainWindowWidget(); + _dockWidget = QDockWidget("Session Manager", m, Qt.Widget); + + _source.renderTimer = QTimer(_dockWidget); + _source.renderQueue = string[](); + _source.progressiveRenderPreviews = SourcePreviewWidget[](); + _source.renderIdx = 0; + _source.progressiveRenderDeferred = string[](); + + _input.renderTimer = QTimer(_dockWidget); + _input.renderQueue = string[](); + _input.progressiveRenderPreviews = SourcePreviewWidget[](); + _input.renderIdx = 0; + _input.progressiveRenderDeferred = string[](); + + _source.renderTimer.setSingleShot(true); + _input.renderTimer.setSingleShot(true); + } + + // Load media metadata, text and placheolder image used before loading actual media + method: displaySourceRowPreview (QWidget; string node, SourcePreviewWidget preview) + { + let widget = QWidget(nil, 0), + layout = QHBoxLayout(widget); + widget.setObjectName("sourceRowWidget"); + layout.setContentsMargins(SOURCE_ROW_MARGIN, 0, SOURCE_ROW_MARGIN, 0); + layout.setSpacing(SOURCE_ROW_SPACING); + + preview.setParent(widget); + preview.setFixedSize(QSize(SOURCE_PREVIEW_WIDTH, SOURCE_PREVIEW_HEIGHT)); + preview.setFallback(_fallbackSourceIcon.pixmap(QSize(SOURCE_PREVIEW_WIDTH, SOURCE_PREVIEW_HEIGHT))); + + layout.addWidget(preview); + + string meta = ""; + try + { + let sourceNode = sourceNodeOfGroup(node); + if (sourceNode neq nil) + { + let mediaPropertyPath = sourceNode + ".media.movie"; + if (propertyExists(mediaPropertyPath)) + { + let movieProperty = getStringProperty(mediaPropertyPath); + if (!movieProperty.empty()) + { + let parts = io.path.basename(movieProperty.front()).split("."); + if (parts.size() > 1) meta = parts.back(); + } + } + } + } + catch (...) {;} + + // Text column + let textWidget = QWidget(widget), + textLayout = QVBoxLayout(textWidget); + textWidget.setObjectName("sourceTextWidget"); + textLayout.setSpacing(SOURCE_TEXT_SPACING); + + let nameLabel = QLabel(uiName(node), textWidget); + nameLabel.setObjectName("sourceNameLabel"); + textLayout.addWidget(nameLabel); + + let metaLabel = QLabel(if meta == "" then "—" else meta, textWidget); + metaLabel.setObjectName("sourceMetaLabel"); + textLayout.addWidget(metaLabel); + textLayout.addStretch(1); + + layout.addWidget(textWidget, 1); + + widget; + } + + // Try to load or decode media, + method: makeSourceRowWidget (void; string node, SourcePreviewWidget preview) + { + string sourceNode = nil; + try { sourceNode = sourceNodeOfGroup(node); } + catch (exception exc) + { + print("WARNING: Could not get source node for %s - %s\n" % (uiName(node), exc)); + } + if (sourceNode eq nil) return; + + // Fetch filmstrip/thumbnail paths. The local plugin has a lower priority of + // 10 for ordering. This means any custom plugin of higher priority will be used first. + // This allows users to override the local plugin with a custom plugin by making sure + // the ordering is less than 10 and using event.accept() to prevent the local plugin from running. + let thumbnailPath = sendInternalEvent("session-manager-get-thumbnail-path", sourceNode); + if (thumbnailPath != "" && io.path.exists(thumbnailPath)) + { + preview.loadThumbnail(thumbnailPath); + + let filmstripPath = sendInternalEvent("session-manager-get-filmstrip-path", sourceNode); + if (filmstripPath != "" && io.path.exists(filmstripPath)) + preview.loadStrip(filmstripPath); + } + } + +} + documentation: """ SessionManagerMode controls UI for managing viewable nodes in the session and a user interface to edit the currently viewed node. New @@ -1073,7 +1201,6 @@ viewables can be created and their inputs changed and reordered. class: SessionManagerMode : MinorMode { - QDockWidget _dockWidget; EventFilter _eventFilter; QWidget _baseWidget; QSplitter _splitter; @@ -1103,7 +1230,6 @@ class: SessionManagerMode : MinorMode QIcon _layerIcon; QIcon _channelIcon; QIcon _videoIcon; - QIcon _fallbackSourceIcon; bool _inputOrderLock; bool _disableUpdates; bool _previewsEnabled; @@ -1121,6 +1247,8 @@ class: SessionManagerMode : MinorMode QAction[] _viewContextMenuActions; QMenu _createMenu; QMenu _folderMenu; + + ThumbnailRenderer thumbnailRenderer; QDialog _newNodeDialog; QComboBox _nodeTypeCombo; @@ -1267,7 +1395,7 @@ class: SessionManagerMode : MinorMode method: activate (void;) { - if (_dockWidget neq nil) _dockWidget.installEventFilter(_eventFilter); + if (thumbnailRenderer._dockWidget neq nil) thumbnailRenderer._dockWidget.installEventFilter(_eventFilter); use SettingsValue; @@ -1286,14 +1414,14 @@ class: SessionManagerMode : MinorMode writeSetting("Tools", "show_session_manager", Bool(false)); } - _dockWidget.show(); + thumbnailRenderer._dockWidget.show(); updateTree(); sendInternalEvent("session-manager-load-ui", viewNode()); } method: deactivate (void;) { - if (_dockWidget neq nil) _dockWidget.removeEventFilter(_eventFilter); + if (thumbnailRenderer._dockWidget neq nil) thumbnailRenderer._dockWidget.removeEventFilter(_eventFilter); use SettingsValue; @@ -1314,7 +1442,7 @@ class: SessionManagerMode : MinorMode _lazySetInputsTimer.stop(); _lazyUpdateTimer.stop(); - _dockWidget.hide(); + thumbnailRenderer._dockWidget.hide(); } method: setNodeStatus (void; string node, string status) @@ -1485,6 +1613,11 @@ class: SessionManagerMode : MinorMode if (_disableUpdates || _progressiveLoadingInProgress) return; _inputOrderLock = true; + thumbnailRenderer._input.renderTimer.stop(); + thumbnailRenderer._input.renderQueue = string[](); + thumbnailRenderer._input.progressiveRenderDeferred = string[](); + thumbnailRenderer._input.progressiveRenderPreviews = SourcePreviewWidget[](); + thumbnailRenderer._input.renderIdx = 0; _inputsModel.clear(); let connections = nodeInputs(node); @@ -1508,9 +1641,18 @@ class: SessionManagerMode : MinorMode _inputsModel.appendRow(item); if (isSource && _previewsEnabled) - _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), makeSourceRowWidget(innode)); + { + let preview = SourcePreviewWidget(nil); + let w = thumbnailRenderer.displaySourceRowPreview(innode, preview); + _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), w); + thumbnailRenderer._input.renderQueue.push_back(innode); + thumbnailRenderer._input.progressiveRenderPreviews.push_back(preview); + } } + if (!thumbnailRenderer._input.renderQueue.empty()) + thumbnailRenderer._input.renderTimer.start(0); + _inputOrderLock = false; } @@ -1797,77 +1939,6 @@ class: SessionManagerMode : MinorMode item; } - method: makeSourceRowWidget (QWidget; string node) - { - string sourceNode = nil; - try { sourceNode = sourceNodeOfGroup(node); } - catch (exception exc) - { - print("WARNING: Could not get source node for %s - %s\n" % (uiName(node), exc)); - } - - let widget = QWidget(nil, 0), - layout = QHBoxLayout(widget); - widget.setObjectName("sourceRowWidget"); - layout.setContentsMargins(SOURCE_ROW_MARGIN, 0, SOURCE_ROW_MARGIN, 0); - layout.setSpacing(SOURCE_ROW_SPACING); - - let preview = SourcePreviewWidget(widget); - preview.setFixedSize(QSize(SOURCE_PREVIEW_WIDTH, SOURCE_PREVIEW_HEIGHT)); - preview.setFallback(_fallbackSourceIcon.pixmap(QSize(SOURCE_PREVIEW_WIDTH, SOURCE_PREVIEW_HEIGHT))); - - string meta = ""; - - if (sourceNode neq nil) - { - // Fetch filmstrip/thumbnail paths. The local plugin has a lower priority of - // 10 for ordering. This means any custom plugin of higher priority will be used first. - // This allows users to override the local plugin with a custom plugin by making sure - // the ordering is less than 10 and using event.accept() to prevent the local plugin from running. - let thumbnailPath = sendInternalEvent("session-manager-get-thumbnail-path", sourceNode); - if (thumbnailPath != "" && io.path.exists(thumbnailPath)) - { - preview.loadThumbnail(thumbnailPath); - - let filmstripPath = sendInternalEvent("session-manager-get-filmstrip-path", sourceNode); - if (filmstripPath != "" && io.path.exists(filmstripPath)) - preview.loadStrip(filmstripPath); - } - - let mediaPropertyPath = sourceNode + ".media.movie"; - if (propertyExists(mediaPropertyPath)) - { - let movieProperty = getStringProperty(mediaPropertyPath); - if (!movieProperty.empty()) - { - let parts = io.path.basename(movieProperty.front()).split("."); - if (parts.size() > 1) meta = parts.back(); - } - } - } - - layout.addWidget(preview); - - // Text column - let textWidget = QWidget(widget), - textLayout = QVBoxLayout(textWidget); - textWidget.setObjectName("sourceTextWidget"); - textLayout.setSpacing(SOURCE_TEXT_SPACING); - - let nameLabel = QLabel(uiName(node), textWidget); - nameLabel.setObjectName("sourceNameLabel"); - textLayout.addWidget(nameLabel); - - let metaLabel = QLabel(if meta == "" then "—" else meta, textWidget); - metaLabel.setObjectName("sourceMetaLabel"); - textLayout.addWidget(metaLabel); - textLayout.addStretch(1); - - layout.addWidget(textWidget, 1); - - widget; - } - method: newNodeRow (void; QStandardItem parentItem, string node, @@ -1902,7 +1973,10 @@ class: SessionManagerMode : MinorMode { item.setText(""); item.setSizeHint(QSize(-1, SOURCE_ROW_HEIGHT)); - _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), makeSourceRowWidget(node)); + let preview = SourcePreviewWidget(nil); + _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), thumbnailRenderer.displaySourceRowPreview(node, preview)); + thumbnailRenderer._source.renderQueue.push_back(node); + thumbnailRenderer._source.progressiveRenderPreviews.push_back(preview); } // @@ -2058,9 +2132,74 @@ class: SessionManagerMode : MinorMode } } + // Try to load the first thumbnail and filmstrip from _source.renderQueue or decode it first, then + // restart the timer to queue the next preview to be processed + method: processSourceQueue (void;) + { + if (thumbnailRenderer._source.renderIdx >= thumbnailRenderer._source.renderQueue.size()) return; + + let node = thumbnailRenderer._source.renderQueue[thumbnailRenderer._source.renderIdx]; + let preview = thumbnailRenderer._source.progressiveRenderPreviews[thumbnailRenderer._source.renderIdx]; + thumbnailRenderer._source.renderIdx = thumbnailRenderer._source.renderIdx + 1; + thumbnailRenderer.makeSourceRowWidget(node, preview); + + if (thumbnailRenderer._source.renderIdx < thumbnailRenderer._source.renderQueue.size()) + { + thumbnailRenderer._source.renderTimer.start(0); + } + else if (!thumbnailRenderer._source.progressiveRenderDeferred.empty()) + { + thumbnailRenderer._source.renderQueue = thumbnailRenderer._source.progressiveRenderDeferred; + thumbnailRenderer._source.progressiveRenderDeferred = string[](); + thumbnailRenderer._source.renderIdx = 0; + + thumbnailRenderer._source.progressiveRenderPreviews = SourcePreviewWidget[](); + for_each (dnode; thumbnailRenderer._source.renderQueue) + { + let item = itemOfNode(_viewModel, dnode); + let preview = SourcePreviewWidget(nil); + let w = thumbnailRenderer.displaySourceRowPreview(dnode, preview); + _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), w); + thumbnailRenderer._source.progressiveRenderPreviews.push_back(preview); + } + + thumbnailRenderer._source.renderTimer.start(0); + } + } + + // Try to load the first thumbnail and filmstrip from _input.renderQueue or decode it first, then + // restart the timer to queue the next preview to be processed + method: processInputQueue (void;) + { + if (thumbnailRenderer._input.renderIdx >= thumbnailRenderer._input.renderQueue.size()) return; + + let node = thumbnailRenderer._input.renderQueue[thumbnailRenderer._input.renderIdx]; + let preview = thumbnailRenderer._input.progressiveRenderPreviews[thumbnailRenderer._input.renderIdx]; + thumbnailRenderer._input.renderIdx = thumbnailRenderer._input.renderIdx + 1; + + thumbnailRenderer.makeSourceRowWidget(node, preview); + + if (thumbnailRenderer._input.renderIdx < thumbnailRenderer._input.renderQueue.size()) + { + thumbnailRenderer._input.renderTimer.start(0); + } + else if (!thumbnailRenderer._input.progressiveRenderDeferred.empty()) + { + thumbnailRenderer._input.renderQueue = thumbnailRenderer._input.progressiveRenderDeferred; + thumbnailRenderer._input.progressiveRenderDeferred = string[](); + thumbnailRenderer._input.renderIdx = 0; + thumbnailRenderer._input.renderTimer.start(0); + } + } + method: updateTree (void;) { if (_disableUpdates) return; + thumbnailRenderer._source.renderTimer.stop(); + thumbnailRenderer._source.renderQueue = string[](); + thumbnailRenderer._source.progressiveRenderPreviews = SourcePreviewWidget[](); + thumbnailRenderer._source.renderIdx = 0; + thumbnailRenderer._source.progressiveRenderDeferred = string[](); _srcNodeKeys = string[](); _grpNodeValues = string[](); _viewModel.clear(); @@ -2154,6 +2293,8 @@ class: SessionManagerMode : MinorMode selectViewableNode(); resizeColumns(_viewTreeView, _viewModel); + + thumbnailRenderer._source.renderTimer.start(0); } catch (exception exc) { @@ -2181,13 +2322,32 @@ class: SessionManagerMode : MinorMode } if (node eq nil) return; + bool inThumbnailQueue = false; + for (int i = thumbnailRenderer._source.renderIdx; i < thumbnailRenderer._source.renderQueue.size(); i++) + if (thumbnailRenderer._source.renderQueue[i] == node) { inThumbnailQueue = true; break; } + if (inThumbnailQueue) + { + thumbnailRenderer._source.progressiveRenderDeferred.push_back(node); + return; + } + let item = itemOfNode(_viewModel, node); if (item neq nil) - _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), makeSourceRowWidget(node)); + { + let preview = SourcePreviewWidget(nil); + let w = thumbnailRenderer.displaySourceRowPreview(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); + _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), w); + } let inputItem = itemOfNode(_inputsModel, node); if (inputItem neq nil) - _inputsView.setIndexWidget(_inputsModel.indexFromItem(inputItem), makeSourceRowWidget(node)); + { + let preview = SourcePreviewWidget(nil); + let w = thumbnailRenderer.displaySourceRowPreview(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); + _inputsView.setIndexWidget(_inputsModel.indexFromItem(inputItem), w); + } } method: beforeProgressiveLoading (void; Event event) @@ -3048,8 +3208,8 @@ class: SessionManagerMode : MinorMode // if (mainWindowWidget().minimized()) return; - if (!_dockWidget.visible() && _active) toggle(); - if ( _dockWidget.visible() && !_active) toggle(); + if (!thumbnailRenderer._dockWidget.visible() && _active) toggle(); + if ( thumbnailRenderer._dockWidget.visible() && !_active) toggle(); } method: visibilityChanged (void; bool vis) @@ -3094,6 +3254,8 @@ class: SessionManagerMode : MinorMode _disableUpdates = false; _srcNodeKeys = string[](); _grpNodeValues = string[](); + thumbnailRenderer = ThumbnailRenderer(); + thumbnailRenderer._fallbackSourceIcon = QIcon(auxFilePath("fallback_thumbnail.png")); let previewsEnv = system.getenv("RV_SESSION_MANAGER_USE_THUMBNAILS", nil); if (previewsEnv neq nil && previewsEnv == "0") @@ -3132,7 +3294,6 @@ class: SessionManagerMode : MinorMode let m = mainWindowWidget(); - _dockWidget = QDockWidget("Session Manager", m, Qt.Widget); _baseWidget = loadUIFile(auxFilePath("session_manager.ui"), m); _treeViewBase = _baseWidget.findChild("treeView"); _addButton = _baseWidget.findChild("addButton"); @@ -3154,9 +3315,9 @@ class: SessionManagerMode : MinorMode _prevViewButton = _baseWidget.findChild("prevViewButton"); _nextViewButton = _baseWidget.findChild("nextViewButton"); - _lazySetInputsTimer = QTimer(_dockWidget); - _lazyUpdateTimer = QTimer(_dockWidget); - _mainWinVisTimer = QTimer(_dockWidget); + _lazySetInputsTimer = QTimer(thumbnailRenderer._dockWidget); + _lazyUpdateTimer = QTimer(thumbnailRenderer._dockWidget); + _mainWinVisTimer = QTimer(thumbnailRenderer._dockWidget); _lazySetInputsTimer.setSingleShot(true); _lazyUpdateTimer.setSingleShot(true); @@ -3174,12 +3335,12 @@ class: SessionManagerMode : MinorMode _inputsView.setObjectName("inputsViewList"); if (_css neq nil) _baseWidget.setStyleSheet(_css); - _dockWidget.setWidget(_baseWidget); - //_dockWidget.setTitleBarWidget(QWidget(m,0)); - _dockWidget.setTitleBarWidget(_baseWidget.findChild("navPanel")); - _dockWidget.setObjectName(name); + thumbnailRenderer._dockWidget.setWidget(_baseWidget); + //thumbnailRenderer._dockWidget.setTitleBarWidget(QWidget(m,0)); + thumbnailRenderer._dockWidget.setTitleBarWidget(_baseWidget.findChild("navPanel")); + thumbnailRenderer._dockWidget.setObjectName(name); _eventFilter = EventFilter(mainWindowWidget()); - _dockWidget.installEventFilter(_eventFilter); + thumbnailRenderer._dockWidget.installEventFilter(_eventFilter); //_viewModel = QStandardItemModel(m); _viewModel = NodeModel(m); @@ -3215,7 +3376,7 @@ class: SessionManagerMode : MinorMode //_inputsView.setDragDropOverwriteMode(true); - m.addDockWidget(Qt.LeftDockWidgetArea, _dockWidget); + m.addDockWidget(Qt.LeftDockWidgetArea, thumbnailRenderer._dockWidget); let addAction = QAction(auxIcon("add_48x48.png", true), "Create View", _addButton), folderAction = QAction(auxIcon("foldr_48x48.png", true), "Create Folder", _folderButton), @@ -3259,8 +3420,6 @@ class: SessionManagerMode : MinorMode _channelIcon = auxIcon("channel.png", true); _layerIcon = auxIcon("layer.png", true); _unknownTypeIcon = auxIcon("new_48x48.png", true); - _fallbackSourceIcon = QIcon(auxFilePath("fallback_thumbnail.png")); - _addButton.setDefaultAction(addAction); _deleteButton.setDefaultAction(deleteAction); _editViewInfoButton.setDefaultAction(editInfoAction); @@ -3480,7 +3639,7 @@ class: SessionManagerMode : MinorMode //print(document_symbol("qt.QItemSelectionModel")); - _dockWidget.show(); + thumbnailRenderer._dockWidget.show(); m.show(); let sprop = "#Session.sm_window.splitter"; @@ -3494,7 +3653,10 @@ class: SessionManagerMode : MinorMode State state = data(); state.sessionManager = this; - connect(_dockWidget, QDockWidget.visibilityChanged, visibilityChanged); + connect(thumbnailRenderer._dockWidget, QDockWidget.visibilityChanged, visibilityChanged); + + connect(thumbnailRenderer._source.renderTimer, QTimer.timeout, processSourceQueue); + connect(thumbnailRenderer._input.renderTimer, QTimer.timeout, processInputQueue); updateNavUI(); }