From dc62876e14fb0b2f783282c8af3deef991fafb73 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Wed, 3 Jun 2026 16:15:38 -0400 Subject: [PATCH 01/21] thumbnail generation cleanup on deletion Signed-off-by: deltag0 --- .../session_manager/local_thumbnail_gen.py | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) 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..bb8313d96 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -78,7 +78,9 @@ 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] = [] + # Each entry pairs a running rvio process with the cache_key it is + # generating for, so a source deletion can cancel the matching proc. + self._active_procs: list[tuple[subprocess.Popen, str]] = [] self._procs_lock = threading.Lock() self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) @@ -106,6 +108,11 @@ def global_bindings(self) -> list[tuple[str, Any, str]]: self._on_session_deletion, "Delete all cached local filmstrips and thumbnails on RV close", ), + ( + "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, @@ -396,7 +403,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 @@ -427,7 +434,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 +444,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 +485,7 @@ def _generate_filmstrip( "-o", str(output_path), ], + cache_key, ) except Exception as e: logger.error(f"Filmstrip generation failed: {e}") @@ -531,7 +540,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: @@ -589,7 +598,7 @@ 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 +615,51 @@ 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() + + source_node = event.contents() + media_path = self._get_media_path(source_node) + if not media_path: + return + + cache_key = self._cache_key(media_path) + + 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") + + self._deferred_sources.discard(source_node) + + 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 From fea5d559d4e312de6970b45eebc91c29cb3aa40c Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 4 Jun 2026 10:44:31 -0400 Subject: [PATCH 02/21] thumbnail cleanup with source node lookup Signed-off-by: deltag0 --- .../session_manager/local_thumbnail_gen.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) 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 bb8313d96..7208eb963 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -235,9 +235,21 @@ def _get_media_path(self, source_node: str) -> str | None: try: return commands.getStringProperty(f"{source_node}.media.movie")[0] except Exception as e: - logger.warning(f"Could not get media path: {e}") + logger.debug(f"No media path for {source_node}: {e}") return None + def _source_node_of_group(self, group: str) -> str | None: + """ + Find the first RVFileSource or RVImageSource node in the group and return its node name. + """ + try: + for node in commands.nodesInGroup(group): + if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): + return node + except Exception as e: + logger.debug(f"Could not resolve source node of group {group}: {e}") + 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]: @@ -620,7 +632,12 @@ def _on_source_delete(self, event: Any) -> None: """ event.reject() - source_node = event.contents() + node = event.contents() + + 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 From 5cf6d2cfe3de99c25ea76b05ea3456479b042b2f Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 4 Jun 2026 10:46:31 -0400 Subject: [PATCH 03/21] format Signed-off-by: deltag0 --- src/plugins/rv-packages/session_manager/local_thumbnail_gen.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 7208eb963..7b04eb698 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -628,8 +628,7 @@ def _on_session_deletion(self, event: Any) -> None: self._cache.clear() def _on_source_delete(self, event: Any) -> None: - """Cancel generation immediately and evict the cache for a removed media source. - """ + """Cancel generation immediately and evict the cache for a removed media source.""" event.reject() node = event.contents() From faf6b47c4b3493b6ae52b4d13ec5093a6d5925c5 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 4 Jun 2026 11:05:10 -0400 Subject: [PATCH 04/21] docs to ensure correctness Signed-off-by: deltag0 --- src/plugins/rv-packages/session_manager/local_thumbnail_gen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7b04eb698..714067889 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -240,7 +240,8 @@ def _get_media_path(self, source_node: str) -> str | None: def _source_node_of_group(self, group: str) -> str | None: """ - Find the first RVFileSource or RVImageSource node in the group and return its node name. + 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): From 01c9b5e53633e28680fc9e31a50d095434834e97 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 4 Jun 2026 11:43:05 -0400 Subject: [PATCH 05/21] change logger level Signed-off-by: deltag0 --- .../rv-packages/session_manager/local_thumbnail_gen.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 714067889..9bc89b795 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -78,8 +78,6 @@ 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 - # Each entry pairs a running rvio process with the cache_key it is - # generating for, so a source deletion can cancel the matching proc. self._active_procs: list[tuple[subprocess.Popen, str]] = [] self._procs_lock = threading.Lock() self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) @@ -235,7 +233,7 @@ def _get_media_path(self, source_node: str) -> str | None: try: return commands.getStringProperty(f"{source_node}.media.movie")[0] except Exception as e: - logger.debug(f"No media path for {source_node}: {e}") + logger.warning(f"Could not get media path: {e}") return None def _source_node_of_group(self, group: str) -> str | None: @@ -248,7 +246,7 @@ def _source_node_of_group(self, group: str) -> str | None: if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): return node except Exception as e: - logger.debug(f"Could not resolve source node of group {group}: {e}") + return return None def _get_source_info(self, source_node: str) -> tuple[int, int, int, int] | None: From ebbcb70cb0b0fcac62fb20ff1144c19b1f49a665 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Fri, 5 Jun 2026 11:35:08 -0400 Subject: [PATCH 06/21] keep track of all sources Signed-off-by: deltag0 --- .../rv-packages/session_manager/local_thumbnail_gen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 9bc89b795..233a3df85 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]] = [] @@ -157,6 +158,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) @@ -164,8 +168,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) From 4de2e8547f9befeec314ba7b3786304de9100567 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Fri, 5 Jun 2026 13:30:46 -0400 Subject: [PATCH 07/21] robustness improvement Signed-off-by: deltag0 --- .../rv-packages/session_manager/local_thumbnail_gen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 233a3df85..fb80cdda2 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -644,6 +644,8 @@ def _on_source_delete(self, event: Any) -> None: 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) @@ -667,8 +669,6 @@ def _on_source_delete(self, event: Any) -> None: self._in_flight.discard(f"{cache_key}_thumbnail_path") self._in_flight.discard(f"{cache_key}_filmstrip_path") - self._deferred_sources.discard(source_node) - cached = self._cache.pop(cache_key, {}) for path in cached.values(): if path: From 2c8b9bd078757545fd739fe768cfaa0bb98fffde Mon Sep 17 00:00:00 2001 From: deltag0 Date: Fri, 5 Jun 2026 13:53:27 -0400 Subject: [PATCH 08/21] format Signed-off-by: deltag0 --- src/plugins/rv-packages/session_manager/local_thumbnail_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fb80cdda2..79641f428 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -247,7 +247,7 @@ def _source_node_of_group(self, group: str) -> str | None: for node in commands.nodesInGroup(group): if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): return node - except Exception as e: + except Exception: return return None From 354f1242176617e956092e804c6e7a96519a9c83 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Mon, 8 Jun 2026 09:38:31 -0400 Subject: [PATCH 09/21] add direct node deletion check Signed-off-by: deltag0 --- .../rv-packages/session_manager/local_thumbnail_gen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 79641f428..08c833e45 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -634,7 +634,10 @@ def _on_source_delete(self, event: Any) -> None: node = event.contents() - source_node = self._source_node_of_group(node) + if commands.nodeType(node) in ("RVFileSource", "RVImageSource"): + source_node = node + else: + source_node = self._source_node_of_group(node) if not source_node: return From 1fa362b75914ed7887d037a568cfd3ed8d6e88fa Mon Sep 17 00:00:00 2001 From: deltag0 Date: Tue, 9 Jun 2026 16:51:37 -0400 Subject: [PATCH 10/21] on session clear event Signed-off-by: deltag0 --- .../session_manager/local_thumbnail_gen.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 08c833e45..7d313206d 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -107,6 +107,11 @@ 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, @@ -607,6 +612,24 @@ def _on_previews_enabled(self, event: Any) -> None: _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._pool.shutdown(wait=False, cancel_futures=True) + self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) + with self._procs_lock: + for proc, _ in self._active_procs: + _resume_proc(proc) + try: + proc.terminate() + except OSError: + logger.warning(f"Failed to terminate process {proc}") + self._in_flight.clear() + self._deferred_jobs.clear() + self._cache_key_to_sources.clear() + self._deferred_sources.clear() + self._cache.clear() + def _on_session_deletion(self, event: Any) -> None: event.reject() self._shutting_down = True From d04c1a85875549a20ad40dfa3427170c9699c2a1 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 11 Jun 2026 12:33:14 -0400 Subject: [PATCH 11/21] optimize locking Signed-off-by: deltag0 --- .../session_manager/local_thumbnail_gen.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 7d313206d..c0b3b0276 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -618,12 +618,13 @@ def _on_clear_session(self, event: Any) -> None: self._pool.shutdown(wait=False, cancel_futures=True) self._pool = ThreadPoolExecutor(max_workers=MAX_WORKERS) with self._procs_lock: - for proc, _ in self._active_procs: - _resume_proc(proc) - try: - proc.terminate() - except OSError: - logger.warning(f"Failed to terminate process {proc}") + procs_to_terminate = list(self._active_procs) + for proc, _ in procs_to_terminate: + _resume_proc(proc) + try: + proc.terminate() + except OSError: + logger.warning(f"Failed to terminate process {proc}") self._in_flight.clear() self._deferred_jobs.clear() self._cache_key_to_sources.clear() From 714646129f291948c63ec0c774e1122c4dc9a3d7 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Fri, 12 Jun 2026 12:57:33 -0400 Subject: [PATCH 12/21] stop thumbnail regeneration after clearing session Signed-off-by: deltag0 --- .../session_manager/local_thumbnail_gen.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 c0b3b0276..3c6fb8498 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -615,21 +615,23 @@ def _on_previews_enabled(self, event: Any) -> None: 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.terminate() + proc.kill() + proc.wait() except OSError: - logger.warning(f"Failed to terminate process {proc}") - self._in_flight.clear() - self._deferred_jobs.clear() - self._cache_key_to_sources.clear() - self._deferred_sources.clear() - self._cache.clear() + logger.warning(f"Failed to kill process {proc}") def _on_session_deletion(self, event: Any) -> None: event.reject() From d0e9e01e7e2c305b7b695ac212fa191549145cbe Mon Sep 17 00:00:00 2001 From: deltag0 Date: Fri, 12 Jun 2026 16:19:49 -0400 Subject: [PATCH 13/21] merge with main Signed-off-by: deltag0 --- src/plugins/rv-packages/session_manager/local_thumbnail_gen.py | 1 + 1 file changed, 1 insertion(+) 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 3c6fb8498..830eace4f 100644 --- a/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py +++ b/src/plugins/rv-packages/session_manager/local_thumbnail_gen.py @@ -576,6 +576,7 @@ def _on_play_stop(self, event: Any) -> None: 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 From 67b55159e5346fd5233bb04a8b55518adc8a6971 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Mon, 8 Jun 2026 16:17:25 -0400 Subject: [PATCH 14/21] update thumbnails with hydration Signed-off-by: deltag0 --- .../session_manager/session_manager.mu.in | 171 ++++++++++++------ 1 file changed, 116 insertions(+), 55 deletions(-) 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..7b6a8bfd3 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1113,6 +1113,14 @@ class: SessionManagerMode : MinorMode QTimer _lazySetInputsTimer; QTimer _lazyUpdateTimer; QTimer _mainWinVisTimer; + QTimer _thumbnailHydrationTimer; + string[] _thumbnailHydrationQueue; + SourcePreviewWidget[] _thumbnailHydrationPreviews; + int _thumbnailHydrationIndex; + QTimer _inputHydrationTimer; + string[] _inputHydrationQueue; + SourcePreviewWidget[] _inputHydrationPreviews; + int _inputHydrationIndex; string _css; bool _darkUI; QDialog _createImageDialog; @@ -1485,6 +1493,10 @@ class: SessionManagerMode : MinorMode if (_disableUpdates || _progressiveLoadingInProgress) return; _inputOrderLock = true; + _inputHydrationTimer.stop(); + _inputHydrationQueue = string[](); + _inputHydrationPreviews = SourcePreviewWidget[](); + _inputHydrationIndex = 0; _inputsModel.clear(); let connections = nodeInputs(node); @@ -1508,9 +1520,18 @@ class: SessionManagerMode : MinorMode _inputsModel.appendRow(item); if (isSource && _previewsEnabled) - _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), makeSourceRowWidget(innode)); + { + let preview = SourcePreviewWidget(nil); + let w = displaySourceRowPreview(innode, preview); + _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), w); + _inputHydrationQueue.push_back(innode); + _inputHydrationPreviews.push_back(preview); + } } + if (!_inputHydrationQueue.empty()) + _inputHydrationTimer.start(0); + _inputOrderLock = false; } @@ -1797,55 +1818,18 @@ class: SessionManagerMode : MinorMode item; } - method: makeSourceRowWidget (QWidget; string node) + method: displaySourceRowPreview (QWidget; 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)); - } - 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.setParent(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 @@ -1858,7 +1842,7 @@ class: SessionManagerMode : MinorMode nameLabel.setObjectName("sourceNameLabel"); textLayout.addWidget(nameLabel); - let metaLabel = QLabel(if meta == "" then "—" else meta, textWidget); + let metaLabel = QLabel("—", textWidget); metaLabel.setObjectName("sourceMetaLabel"); textLayout.addWidget(metaLabel); textLayout.addStretch(1); @@ -1868,6 +1852,31 @@ class: SessionManagerMode : MinorMode widget; } + 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); + } + } + method: newNodeRow (void; QStandardItem parentItem, string node, @@ -1902,7 +1911,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), displaySourceRowPreview(node, preview)); + _thumbnailHydrationQueue.push_back(node); + _thumbnailHydrationPreviews.push_back(preview); } // @@ -2058,9 +2070,41 @@ class: SessionManagerMode : MinorMode } } + method: hydrateThumbnailQueue (void;) + { + if (_thumbnailHydrationIndex >= _thumbnailHydrationQueue.size()) return; + + let node = _thumbnailHydrationQueue[_thumbnailHydrationIndex]; + _thumbnailHydrationIndex = _thumbnailHydrationIndex + 1; + + let preview = _thumbnailHydrationPreviews[_thumbnailHydrationIndex - 1]; + makeSourceRowWidget(node, preview); + + if (_thumbnailHydrationIndex < _thumbnailHydrationQueue.size()) + _thumbnailHydrationTimer.start(0); + } + + method: hydrateInputQueue (void;) + { + if (_inputHydrationIndex >= _inputHydrationQueue.size()) return; + + let node = _inputHydrationQueue[_inputHydrationIndex]; + let preview = _inputHydrationPreviews[_inputHydrationIndex]; + _inputHydrationIndex = _inputHydrationIndex + 1; + + makeSourceRowWidget(node, preview); + + if (_inputHydrationIndex < _inputHydrationQueue.size()) + _inputHydrationTimer.start(0); + } + method: updateTree (void;) { if (_disableUpdates) return; + _thumbnailHydrationTimer.stop(); + _thumbnailHydrationQueue = string[](); + _thumbnailHydrationPreviews = SourcePreviewWidget[](); + _thumbnailHydrationIndex = 0; _srcNodeKeys = string[](); _grpNodeValues = string[](); _viewModel.clear(); @@ -2154,6 +2198,9 @@ class: SessionManagerMode : MinorMode selectViewableNode(); resizeColumns(_viewTreeView, _viewModel); + + if (!_thumbnailHydrationQueue.empty()) + _thumbnailHydrationTimer.start(0); } catch (exception exc) { @@ -2183,11 +2230,21 @@ class: SessionManagerMode : MinorMode let item = itemOfNode(_viewModel, node); if (item neq nil) - _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), makeSourceRowWidget(node)); + { + let preview = SourcePreviewWidget(nil); + let w = displaySourceRowPreview(node, preview); + 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 = displaySourceRowPreview(node, preview); + makeSourceRowWidget(node, preview); + _inputsView.setIndexWidget(_inputsModel.indexFromItem(inputItem), w); + } } method: beforeProgressiveLoading (void; Event event) @@ -3074,14 +3131,6 @@ class: SessionManagerMode : MinorMode _previewsEnabled = checked; use SettingsValue; writeSetting("SessionManager", "previewsEnabled", Bool(checked)); - if (!checked) - { - sendInternalEvent("session-manager-previews-disabled", ""); - } - else - { - sendInternalEvent("session-manager-previews-enabled", ""); - } updateTree(); } @@ -3154,13 +3203,23 @@ class: SessionManagerMode : MinorMode _prevViewButton = _baseWidget.findChild("prevViewButton"); _nextViewButton = _baseWidget.findChild("nextViewButton"); - _lazySetInputsTimer = QTimer(_dockWidget); - _lazyUpdateTimer = QTimer(_dockWidget); - _mainWinVisTimer = QTimer(_dockWidget); + _lazySetInputsTimer = QTimer(_dockWidget); + _lazyUpdateTimer = QTimer(_dockWidget); + _mainWinVisTimer = QTimer(_dockWidget); + _thumbnailHydrationTimer = QTimer(_dockWidget); + _thumbnailHydrationQueue = string[](); + _thumbnailHydrationPreviews = SourcePreviewWidget[](); + _thumbnailHydrationIndex = 0; + _inputHydrationTimer = QTimer(_dockWidget); + _inputHydrationQueue = string[](); + _inputHydrationPreviews = SourcePreviewWidget[](); + _inputHydrationIndex = 0; _lazySetInputsTimer.setSingleShot(true); _lazyUpdateTimer.setSingleShot(true); _mainWinVisTimer.setSingleShot(true); + _thumbnailHydrationTimer.setSingleShot(true); + _inputHydrationTimer.setSingleShot(true); let vbox = QVBoxLayout(_treeViewBase); vbox.setContentsMargins(0, 0, 0, 0); @@ -3455,6 +3514,8 @@ class: SessionManagerMode : MinorMode connect(_lazySetInputsTimer, QTimer.timeout, rebuildInputsFromList); connect(_lazyUpdateTimer, QTimer.timeout, updateTree); connect(_mainWinVisTimer, QTimer.timeout, mainWinVisTimeout); + connect(_thumbnailHydrationTimer, QTimer.timeout, hydrateThumbnailQueue); + connect(_inputHydrationTimer, QTimer.timeout, hydrateInputQueue); connect(_colorDialog, QColorDialog.currentColorChanged, newColorSlot); connect(_splitter, QSplitter.splitterMoved, splitterMoved); connect(configAlwaysOn, QAction.triggered, configSlot(,"yes",true)); From 19afb8ebee146e5657bfed267e8331277c73cb64 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Mon, 8 Jun 2026 16:30:00 -0400 Subject: [PATCH 15/21] undo internal event removal Signed-off-by: deltag0 --- .../rv-packages/session_manager/session_manager.mu.in | 8 ++++++++ 1 file changed, 8 insertions(+) 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 7b6a8bfd3..78c05b62a 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -3131,6 +3131,14 @@ class: SessionManagerMode : MinorMode _previewsEnabled = checked; use SettingsValue; writeSetting("SessionManager", "previewsEnabled", Bool(checked)); + if (!checked) + { + sendInternalEvent("session-manager-previews-disabled", ""); + } + else + { + sendInternalEvent("session-manager-previews-enabled", ""); + } updateTree(); } From f9fe72112a594c62b2f01a0081b1ba7456622868 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Thu, 11 Jun 2026 10:04:18 -0400 Subject: [PATCH 16/21] thumbnail displaying improvement Signed-off-by: deltag0 --- .../session_manager/session_manager.mu.in | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 78c05b62a..dcf52494a 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1117,10 +1117,12 @@ class: SessionManagerMode : MinorMode string[] _thumbnailHydrationQueue; SourcePreviewWidget[] _thumbnailHydrationPreviews; int _thumbnailHydrationIndex; + string[] _thumbnailHydrationDeferred; QTimer _inputHydrationTimer; string[] _inputHydrationQueue; SourcePreviewWidget[] _inputHydrationPreviews; int _inputHydrationIndex; + string[] _inputHydrationDeferred; string _css; bool _darkUI; QDialog _createImageDialog; @@ -1495,6 +1497,7 @@ class: SessionManagerMode : MinorMode _inputOrderLock = true; _inputHydrationTimer.stop(); _inputHydrationQueue = string[](); + _inputHydrationDeferred = string[](); _inputHydrationPreviews = SourcePreviewWidget[](); _inputHydrationIndex = 0; @@ -2081,7 +2084,16 @@ class: SessionManagerMode : MinorMode makeSourceRowWidget(node, preview); if (_thumbnailHydrationIndex < _thumbnailHydrationQueue.size()) + { + _thumbnailHydrationTimer.start(0); + } + else if (!_thumbnailHydrationDeferred.empty()) + { + _thumbnailHydrationQueue = _thumbnailHydrationDeferred; + _thumbnailHydrationDeferred = string[](); + _thumbnailHydrationIndex = 0; _thumbnailHydrationTimer.start(0); + } } method: hydrateInputQueue (void;) @@ -2095,7 +2107,16 @@ class: SessionManagerMode : MinorMode makeSourceRowWidget(node, preview); if (_inputHydrationIndex < _inputHydrationQueue.size()) + { + _inputHydrationTimer.start(0); + } + else if (!_inputHydrationDeferred.empty()) + { + _inputHydrationQueue = _inputHydrationDeferred; + _inputHydrationDeferred = string[](); + _inputHydrationIndex = 0; _inputHydrationTimer.start(0); + } } method: updateTree (void;) @@ -2105,6 +2126,7 @@ class: SessionManagerMode : MinorMode _thumbnailHydrationQueue = string[](); _thumbnailHydrationPreviews = SourcePreviewWidget[](); _thumbnailHydrationIndex = 0; + _thumbnailHydrationDeferred = string[](); _srcNodeKeys = string[](); _grpNodeValues = string[](); _viewModel.clear(); @@ -2228,6 +2250,15 @@ class: SessionManagerMode : MinorMode } if (node eq nil) return; + bool inThumbnailQueue = false; + for (int i = _thumbnailHydrationIndex; i < _thumbnailHydrationQueue.size(); i++) + if (_thumbnailHydrationQueue[i] == node) { inThumbnailQueue = true; break; } + if (inThumbnailQueue) + { + _thumbnailHydrationDeferred.push_back(node); + return; + } + let item = itemOfNode(_viewModel, node); if (item neq nil) { @@ -3218,10 +3249,12 @@ class: SessionManagerMode : MinorMode _thumbnailHydrationQueue = string[](); _thumbnailHydrationPreviews = SourcePreviewWidget[](); _thumbnailHydrationIndex = 0; + _thumbnailHydrationDeferred = string[](); _inputHydrationTimer = QTimer(_dockWidget); _inputHydrationQueue = string[](); _inputHydrationPreviews = SourcePreviewWidget[](); _inputHydrationIndex = 0; + _inputHydrationDeferred = string[](); _lazySetInputsTimer.setSingleShot(true); _lazyUpdateTimer.setSingleShot(true); From 3c96a08c5c0704e4e0cd9970093ac8e5e0b24129 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Tue, 16 Jun 2026 14:22:13 -0400 Subject: [PATCH 17/21] organize changes in class Signed-off-by: deltag0 --- .../session_manager/session_manager.mu.in | 213 ++++++++++-------- 1 file changed, 118 insertions(+), 95 deletions(-) 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 0f01d3a7e..e6f1b20b1 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,52 @@ class: EventFilter : QObject } } +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 +{ + QTimer _sourceProgressiveRenderTimer; + string[] _sourceProgressiveRenderQueue; + SourcePreviewWidget[] _sourceProgressiveRenderPreviews; + int _sourceProgressiveRenderIndex; + // This queue is used if the sessionManager receives a previewEvent (usually from filmstrips) + // but if the node's thumbnail hasn't been guaranteed to be displayed first, we defer showing the filmstrip + string[] _sourceProgressiveRenderDeferred; + QTimer _inputProgressiveRenderTimer; + string[] _inputProgressiveRenderQueue; + SourcePreviewWidget[] _inputProgressiveRenderPreviews; + int _inputProgressiveRenderIndex; + string[] _inputProgressiveRenderDeferred; + QDockWidget _dockWidget; + + + method: ThumbnailRenderer (ThumbnailRenderer;) { + + let m = mainWindowWidget(); + _dockWidget = QDockWidget("Session Manager", m, Qt.Widget); + + _sourceProgressiveRenderTimer = QTimer(_dockWidget); + _sourceProgressiveRenderQueue = string[](); + _sourceProgressiveRenderPreviews = SourcePreviewWidget[](); + _sourceProgressiveRenderIndex = 0; + + _sourceProgressiveRenderDeferred = string[](); + _inputProgressiveRenderTimer = QTimer(_dockWidget); + _inputProgressiveRenderQueue = string[](); + _inputProgressiveRenderPreviews = SourcePreviewWidget[](); + _inputProgressiveRenderIndex = 0; + _inputProgressiveRenderDeferred = string[](); + + _sourceProgressiveRenderTimer.setSingleShot(true); + _inputProgressiveRenderTimer.setSingleShot(true); + } + +} + documentation: """ SessionManagerMode controls UI for managing viewable nodes in the session and a user interface to edit the currently viewed node. New @@ -1073,7 +1119,6 @@ viewables can be created and their inputs changed and reordered. class: SessionManagerMode : MinorMode { - QDockWidget _dockWidget; EventFilter _eventFilter; QWidget _baseWidget; QSplitter _splitter; @@ -1113,16 +1158,6 @@ class: SessionManagerMode : MinorMode QTimer _lazySetInputsTimer; QTimer _lazyUpdateTimer; QTimer _mainWinVisTimer; - QTimer _thumbnailHydrationTimer; - string[] _thumbnailHydrationQueue; - SourcePreviewWidget[] _thumbnailHydrationPreviews; - int _thumbnailHydrationIndex; - string[] _thumbnailHydrationDeferred; - QTimer _inputHydrationTimer; - string[] _inputHydrationQueue; - SourcePreviewWidget[] _inputHydrationPreviews; - int _inputHydrationIndex; - string[] _inputHydrationDeferred; string _css; bool _darkUI; QDialog _createImageDialog; @@ -1131,6 +1166,8 @@ class: SessionManagerMode : MinorMode QAction[] _viewContextMenuActions; QMenu _createMenu; QMenu _folderMenu; + + ThumbnailRenderer thumbnailRenderer; QDialog _newNodeDialog; QComboBox _nodeTypeCombo; @@ -1277,7 +1314,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; @@ -1296,14 +1333,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; @@ -1324,7 +1361,7 @@ class: SessionManagerMode : MinorMode _lazySetInputsTimer.stop(); _lazyUpdateTimer.stop(); - _dockWidget.hide(); + thumbnailRenderer._dockWidget.hide(); } method: setNodeStatus (void; string node, string status) @@ -1495,11 +1532,11 @@ class: SessionManagerMode : MinorMode if (_disableUpdates || _progressiveLoadingInProgress) return; _inputOrderLock = true; - _inputHydrationTimer.stop(); - _inputHydrationQueue = string[](); - _inputHydrationDeferred = string[](); - _inputHydrationPreviews = SourcePreviewWidget[](); - _inputHydrationIndex = 0; + thumbnailRenderer._inputProgressiveRenderTimer.stop(); + thumbnailRenderer._inputProgressiveRenderQueue = string[](); + thumbnailRenderer._inputProgressiveRenderDeferred = string[](); + thumbnailRenderer._inputProgressiveRenderPreviews = SourcePreviewWidget[](); + thumbnailRenderer._inputProgressiveRenderIndex = 0; _inputsModel.clear(); let connections = nodeInputs(node); @@ -1527,13 +1564,13 @@ class: SessionManagerMode : MinorMode let preview = SourcePreviewWidget(nil); let w = displaySourceRowPreview(innode, preview); _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), w); - _inputHydrationQueue.push_back(innode); - _inputHydrationPreviews.push_back(preview); + thumbnailRenderer._inputProgressiveRenderQueue.push_back(innode); + thumbnailRenderer._inputProgressiveRenderPreviews.push_back(preview); } } - if (!_inputHydrationQueue.empty()) - _inputHydrationTimer.start(0); + if (!thumbnailRenderer._inputProgressiveRenderQueue.empty()) + thumbnailRenderer._inputProgressiveRenderTimer.start(0); _inputOrderLock = false; } @@ -1883,7 +1920,6 @@ class: SessionManagerMode : MinorMode { print("WARNING: Could not get source node for %s - %s\n" % (uiName(node), exc)); } - if (sourceNode eq nil) { print("DEBUG makeSourceRowWidget: sourceNode nil for %s\n" % node); 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. @@ -1937,9 +1973,8 @@ class: SessionManagerMode : MinorMode item.setSizeHint(QSize(-1, SOURCE_ROW_HEIGHT)); let preview = SourcePreviewWidget(nil); _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), displaySourceRowPreview(node, preview)); - print("DEBUG newNodeRow: queued node=%s\n" % node); - _thumbnailHydrationQueue.push_back(node); - _thumbnailHydrationPreviews.push_back(preview); + thumbnailRenderer._sourceProgressiveRenderQueue.push_back(node); + thumbnailRenderer._sourceProgressiveRenderPreviews.push_back(preview); } // @@ -2095,72 +2130,72 @@ class: SessionManagerMode : MinorMode } } - method: hydrateThumbnailQueue (void;) + method: processSourceQueue (void;) { - if (_thumbnailHydrationIndex >= _thumbnailHydrationQueue.size()) return; + if (thumbnailRenderer._sourceProgressiveRenderIndex >= thumbnailRenderer._sourceProgressiveRenderQueue.size()) return; - let node = _thumbnailHydrationQueue[_thumbnailHydrationIndex]; - _thumbnailHydrationIndex = _thumbnailHydrationIndex + 1; + let node = thumbnailRenderer._sourceProgressiveRenderQueue[thumbnailRenderer._sourceProgressiveRenderIndex]; + thumbnailRenderer._sourceProgressiveRenderIndex = thumbnailRenderer._sourceProgressiveRenderIndex + 1; - let preview = _thumbnailHydrationPreviews[_thumbnailHydrationIndex - 1]; - print("DEBUG hydrateThumbnailQueue: node=%s preview=%s\n" % (node, string(preview))); + let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex - 1]; + print("WARNING processSourceQueue: node=%s preview=%s\n" % (node, string(preview))); makeSourceRowWidget(node, preview); - if (_thumbnailHydrationIndex < _thumbnailHydrationQueue.size()) + if (thumbnailRenderer._sourceProgressiveRenderIndex < thumbnailRenderer._sourceProgressiveRenderQueue.size()) { - _thumbnailHydrationTimer.start(0); + thumbnailRenderer._sourceProgressiveRenderTimer.start(0); } - else if (!_thumbnailHydrationDeferred.empty()) + else if (!thumbnailRenderer._sourceProgressiveRenderDeferred.empty()) { - _thumbnailHydrationQueue = _thumbnailHydrationDeferred; - _thumbnailHydrationDeferred = string[](); - _thumbnailHydrationIndex = 0; + thumbnailRenderer._sourceProgressiveRenderQueue = thumbnailRenderer._sourceProgressiveRenderDeferred; + thumbnailRenderer._sourceProgressiveRenderDeferred = string[](); + thumbnailRenderer._sourceProgressiveRenderIndex = 0; - _thumbnailHydrationPreviews = SourcePreviewWidget[](); - for_each (dnode; _thumbnailHydrationQueue) + thumbnailRenderer._sourceProgressiveRenderPreviews = SourcePreviewWidget[](); + for_each (dnode; thumbnailRenderer._sourceProgressiveRenderQueue) { let item = itemOfNode(_viewModel, dnode); let preview = SourcePreviewWidget(nil); let w = displaySourceRowPreview(dnode, preview); _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), w); - _thumbnailHydrationPreviews.push_back(preview); + thumbnailRenderer._sourceProgressiveRenderPreviews.push_back(preview); } - _thumbnailHydrationTimer.start(0); + thumbnailRenderer._sourceProgressiveRenderTimer.start(0); } } - method: hydrateInputQueue (void;) + method: processInputQueue (void;) { - if (_inputHydrationIndex >= _inputHydrationQueue.size()) return; + if (thumbnailRenderer._inputProgressiveRenderIndex >= thumbnailRenderer._inputProgressiveRenderQueue.size()) return; - let node = _inputHydrationQueue[_inputHydrationIndex]; - let preview = _inputHydrationPreviews[_inputHydrationIndex]; - _inputHydrationIndex = _inputHydrationIndex + 1; + let node = thumbnailRenderer._inputProgressiveRenderQueue[thumbnailRenderer._inputProgressiveRenderIndex]; + let preview = thumbnailRenderer._inputProgressiveRenderPreviews[thumbnailRenderer._inputProgressiveRenderIndex]; + thumbnailRenderer._inputProgressiveRenderIndex = thumbnailRenderer._inputProgressiveRenderIndex + 1; makeSourceRowWidget(node, preview); - if (_inputHydrationIndex < _inputHydrationQueue.size()) + if (thumbnailRenderer._inputProgressiveRenderIndex < thumbnailRenderer._inputProgressiveRenderQueue.size()) { - _inputHydrationTimer.start(0); + thumbnailRenderer._inputProgressiveRenderTimer.start(0); } - else if (!_inputHydrationDeferred.empty()) + else if (!thumbnailRenderer._inputProgressiveRenderDeferred.empty()) { - _inputHydrationQueue = _inputHydrationDeferred; - _inputHydrationDeferred = string[](); - _inputHydrationIndex = 0; - _inputHydrationTimer.start(0); + thumbnailRenderer._inputProgressiveRenderQueue = thumbnailRenderer._inputProgressiveRenderDeferred; + thumbnailRenderer._inputProgressiveRenderDeferred = string[](); + thumbnailRenderer._inputProgressiveRenderIndex = 0; + thumbnailRenderer._inputProgressiveRenderTimer.start(0); } } method: updateTree (void;) { if (_disableUpdates) return; - _thumbnailHydrationTimer.stop(); - _thumbnailHydrationQueue = string[](); - _thumbnailHydrationPreviews = SourcePreviewWidget[](); - _thumbnailHydrationIndex = 0; - _thumbnailHydrationDeferred = string[](); + thumbnailRenderer._sourceProgressiveRenderTimer.stop(); + thumbnailRenderer._sourceProgressiveRenderQueue = string[](); + thumbnailRenderer._sourceProgressiveRenderPreviews = SourcePreviewWidget[](); + thumbnailRenderer._sourceProgressiveRenderIndex = 0; + thumbnailRenderer._sourceProgressiveRenderDeferred = string[](); _srcNodeKeys = string[](); _grpNodeValues = string[](); _viewModel.clear(); @@ -2255,9 +2290,8 @@ class: SessionManagerMode : MinorMode resizeColumns(_viewTreeView, _viewModel); - print("DEBUG updateTree: queue size=%d, starting timer=%s\n" % (_thumbnailHydrationQueue.size(), string(!_thumbnailHydrationQueue.empty()))); - if (!_thumbnailHydrationQueue.empty()) - _thumbnailHydrationTimer.start(0); + if (!thumbnailRenderer._sourceProgressiveRenderQueue.empty()) + thumbnailRenderer._sourceProgressiveRenderTimer.start(0); } catch (exception exc) { @@ -2287,11 +2321,11 @@ class: SessionManagerMode : MinorMode if (node eq nil) return; bool inThumbnailQueue = false; - for (int i = _thumbnailHydrationIndex; i < _thumbnailHydrationQueue.size(); i++) - if (_thumbnailHydrationQueue[i] == node) { inThumbnailQueue = true; break; } + for (int i = thumbnailRenderer._sourceProgressiveRenderIndex; i < thumbnailRenderer._sourceProgressiveRenderQueue.size(); i++) + if (thumbnailRenderer._sourceProgressiveRenderQueue[i] == node) { inThumbnailQueue = true; break; } if (inThumbnailQueue) { - _thumbnailHydrationDeferred.push_back(node); + thumbnailRenderer._sourceProgressiveRenderDeferred.push_back(node); return; } @@ -3172,8 +3206,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) @@ -3218,6 +3252,7 @@ class: SessionManagerMode : MinorMode _disableUpdates = false; _srcNodeKeys = string[](); _grpNodeValues = string[](); + thumbnailRenderer = ThumbnailRenderer(); let previewsEnv = system.getenv("RV_SESSION_MANAGER_USE_THUMBNAILS", nil); if (previewsEnv neq nil && previewsEnv == "0") @@ -3256,7 +3291,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"); @@ -3278,25 +3312,13 @@ class: SessionManagerMode : MinorMode _prevViewButton = _baseWidget.findChild("prevViewButton"); _nextViewButton = _baseWidget.findChild("nextViewButton"); - _lazySetInputsTimer = QTimer(_dockWidget); - _lazyUpdateTimer = QTimer(_dockWidget); - _mainWinVisTimer = QTimer(_dockWidget); - _thumbnailHydrationTimer = QTimer(_dockWidget); - _thumbnailHydrationQueue = string[](); - _thumbnailHydrationPreviews = SourcePreviewWidget[](); - _thumbnailHydrationIndex = 0; - _thumbnailHydrationDeferred = string[](); - _inputHydrationTimer = QTimer(_dockWidget); - _inputHydrationQueue = string[](); - _inputHydrationPreviews = SourcePreviewWidget[](); - _inputHydrationIndex = 0; - _inputHydrationDeferred = string[](); + _lazySetInputsTimer = QTimer(thumbnailRenderer._dockWidget); + _lazyUpdateTimer = QTimer(thumbnailRenderer._dockWidget); + _mainWinVisTimer = QTimer(thumbnailRenderer._dockWidget); _lazySetInputsTimer.setSingleShot(true); _lazyUpdateTimer.setSingleShot(true); _mainWinVisTimer.setSingleShot(true); - _thumbnailHydrationTimer.setSingleShot(true); - _inputHydrationTimer.setSingleShot(true); let vbox = QVBoxLayout(_treeViewBase); vbox.setContentsMargins(0, 0, 0, 0); @@ -3310,12 +3332,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); @@ -3351,7 +3373,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), @@ -3591,8 +3613,6 @@ class: SessionManagerMode : MinorMode connect(_lazySetInputsTimer, QTimer.timeout, rebuildInputsFromList); connect(_lazyUpdateTimer, QTimer.timeout, updateTree); connect(_mainWinVisTimer, QTimer.timeout, mainWinVisTimeout); - connect(_thumbnailHydrationTimer, QTimer.timeout, hydrateThumbnailQueue); - connect(_inputHydrationTimer, QTimer.timeout, hydrateInputQueue); connect(_colorDialog, QColorDialog.currentColorChanged, newColorSlot); connect(_splitter, QSplitter.splitterMoved, splitterMoved); connect(configAlwaysOn, QAction.triggered, configSlot(,"yes",true)); @@ -3618,7 +3638,7 @@ class: SessionManagerMode : MinorMode //print(document_symbol("qt.QItemSelectionModel")); - _dockWidget.show(); + thumbnailRenderer._dockWidget.show(); m.show(); let sprop = "#Session.sm_window.splitter"; @@ -3632,7 +3652,10 @@ class: SessionManagerMode : MinorMode State state = data(); state.sessionManager = this; - connect(_dockWidget, QDockWidget.visibilityChanged, visibilityChanged); + connect(thumbnailRenderer._dockWidget, QDockWidget.visibilityChanged, visibilityChanged); + + connect(thumbnailRenderer._sourceProgressiveRenderTimer, QTimer.timeout, processSourceQueue); + connect(thumbnailRenderer._inputProgressiveRenderTimer, QTimer.timeout, processInputQueue); updateNavUI(); } From decc92319c39e606810de86dfbb04f5c38c17369 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Tue, 16 Jun 2026 15:23:47 -0400 Subject: [PATCH 18/21] remove logs Signed-off-by: deltag0 --- src/plugins/rv-packages/session_manager/session_manager.mu.in | 3 --- 1 file changed, 3 deletions(-) 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 e6f1b20b1..10f924677 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1926,7 +1926,6 @@ class: SessionManagerMode : MinorMode // 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); - print("DEBUG makeSourceRowWidget: node=%s sourceNode=%s thumbnailPath='%s'\n" % (node, sourceNode, thumbnailPath)); if (thumbnailPath != "" && io.path.exists(thumbnailPath)) { preview.loadThumbnail(thumbnailPath); @@ -2138,7 +2137,6 @@ class: SessionManagerMode : MinorMode thumbnailRenderer._sourceProgressiveRenderIndex = thumbnailRenderer._sourceProgressiveRenderIndex + 1; let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex - 1]; - print("WARNING processSourceQueue: node=%s preview=%s\n" % (node, string(preview))); makeSourceRowWidget(node, preview); if (thumbnailRenderer._sourceProgressiveRenderIndex < thumbnailRenderer._sourceProgressiveRenderQueue.size()) @@ -2317,7 +2315,6 @@ class: SessionManagerMode : MinorMode { if (_srcNodeKeys[i] == sourceNode) { node = _grpNodeValues[i]; break; } } - print("DEBUG updateNodePreviewEvent: sourceNode=%s -> node=%s srcNodeKeys.size=%d\n" % (sourceNode, string(node), _srcNodeKeys.size())); if (node eq nil) return; bool inThumbnailQueue = false; From 174585b899c9c1fc957a48a153be971702bfebd4 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Tue, 16 Jun 2026 15:56:16 -0400 Subject: [PATCH 19/21] minor improvements Signed-off-by: deltag0 --- .../rv-packages/session_manager/session_manager.mu.in | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 10f924677..2e60943d4 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1920,6 +1920,7 @@ class: SessionManagerMode : MinorMode { 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. @@ -2134,9 +2135,8 @@ class: SessionManagerMode : MinorMode if (thumbnailRenderer._sourceProgressiveRenderIndex >= thumbnailRenderer._sourceProgressiveRenderQueue.size()) return; let node = thumbnailRenderer._sourceProgressiveRenderQueue[thumbnailRenderer._sourceProgressiveRenderIndex]; + let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex]; thumbnailRenderer._sourceProgressiveRenderIndex = thumbnailRenderer._sourceProgressiveRenderIndex + 1; - - let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex - 1]; makeSourceRowWidget(node, preview); if (thumbnailRenderer._sourceProgressiveRenderIndex < thumbnailRenderer._sourceProgressiveRenderQueue.size()) @@ -2288,8 +2288,7 @@ class: SessionManagerMode : MinorMode resizeColumns(_viewTreeView, _viewModel); - if (!thumbnailRenderer._sourceProgressiveRenderQueue.empty()) - thumbnailRenderer._sourceProgressiveRenderTimer.start(0); + thumbnailRenderer._sourceProgressiveRenderTimer.start(0); } catch (exception exc) { From 26b6344efa624cb0bba7ffc91c74b24ac5c53c3c Mon Sep 17 00:00:00 2001 From: deltag0 Date: Tue, 16 Jun 2026 16:47:31 -0400 Subject: [PATCH 20/21] documentation and organization Signed-off-by: deltag0 --- .../session_manager/session_manager.mu.in | 193 +++++++++--------- 1 file changed, 99 insertions(+), 94 deletions(-) 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 2e60943d4..9a6542f4d 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -1077,8 +1077,8 @@ class: ThumbnailRenderer string[] _sourceProgressiveRenderQueue; SourcePreviewWidget[] _sourceProgressiveRenderPreviews; int _sourceProgressiveRenderIndex; - // This queue is used if the sessionManager receives a previewEvent (usually from filmstrips) - // but if the node's thumbnail hasn't been guaranteed to be displayed first, we defer showing the filmstrip + // This queue is used if when the sessionManager receives a previewEvent (usually from filmstrips) + // but haven't yet displayed the current node in _sourceProgressiveRenderQueue string[] _sourceProgressiveRenderDeferred; QTimer _inputProgressiveRenderTimer; string[] _inputProgressiveRenderQueue; @@ -1086,6 +1086,7 @@ class: ThumbnailRenderer int _inputProgressiveRenderIndex; string[] _inputProgressiveRenderDeferred; QDockWidget _dockWidget; + QIcon _fallbackSourceIcon; method: ThumbnailRenderer (ThumbnailRenderer;) { @@ -1097,7 +1098,7 @@ class: ThumbnailRenderer _sourceProgressiveRenderQueue = string[](); _sourceProgressiveRenderPreviews = SourcePreviewWidget[](); _sourceProgressiveRenderIndex = 0; - + _sourceProgressiveRenderDeferred = string[](); _inputProgressiveRenderTimer = QTimer(_dockWidget); _inputProgressiveRenderQueue = string[](); @@ -1109,6 +1110,87 @@ class: ThumbnailRenderer _inputProgressiveRenderTimer.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, usually used after displaySourceRowPreview + 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: """ @@ -1148,7 +1230,6 @@ class: SessionManagerMode : MinorMode QIcon _layerIcon; QIcon _channelIcon; QIcon _videoIcon; - QIcon _fallbackSourceIcon; bool _inputOrderLock; bool _disableUpdates; bool _previewsEnabled; @@ -1562,7 +1643,7 @@ class: SessionManagerMode : MinorMode if (isSource && _previewsEnabled) { let preview = SourcePreviewWidget(nil); - let w = displaySourceRowPreview(innode, preview); + let w = thumbnailRenderer.displaySourceRowPreview(innode, preview); _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), w); thumbnailRenderer._inputProgressiveRenderQueue.push_back(innode); thumbnailRenderer._inputProgressiveRenderPreviews.push_back(preview); @@ -1858,85 +1939,6 @@ class: SessionManagerMode : MinorMode item; } - 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; - } - - 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); - } - } - method: newNodeRow (void; QStandardItem parentItem, string node, @@ -1972,7 +1974,7 @@ class: SessionManagerMode : MinorMode item.setText(""); item.setSizeHint(QSize(-1, SOURCE_ROW_HEIGHT)); let preview = SourcePreviewWidget(nil); - _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), displaySourceRowPreview(node, preview)); + _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), thumbnailRenderer.displaySourceRowPreview(node, preview)); thumbnailRenderer._sourceProgressiveRenderQueue.push_back(node); thumbnailRenderer._sourceProgressiveRenderPreviews.push_back(preview); } @@ -2130,6 +2132,8 @@ class: SessionManagerMode : MinorMode } } + // Try to load the first thumbnail and filmstrip from _sourceProgressiveRenderQueue or decode it first, then + // restart the timer to queue the next preview to be processed method: processSourceQueue (void;) { if (thumbnailRenderer._sourceProgressiveRenderIndex >= thumbnailRenderer._sourceProgressiveRenderQueue.size()) return; @@ -2137,7 +2141,7 @@ class: SessionManagerMode : MinorMode let node = thumbnailRenderer._sourceProgressiveRenderQueue[thumbnailRenderer._sourceProgressiveRenderIndex]; let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex]; thumbnailRenderer._sourceProgressiveRenderIndex = thumbnailRenderer._sourceProgressiveRenderIndex + 1; - makeSourceRowWidget(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); if (thumbnailRenderer._sourceProgressiveRenderIndex < thumbnailRenderer._sourceProgressiveRenderQueue.size()) { @@ -2154,7 +2158,7 @@ class: SessionManagerMode : MinorMode { let item = itemOfNode(_viewModel, dnode); let preview = SourcePreviewWidget(nil); - let w = displaySourceRowPreview(dnode, preview); + let w = thumbnailRenderer.displaySourceRowPreview(dnode, preview); _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), w); thumbnailRenderer._sourceProgressiveRenderPreviews.push_back(preview); } @@ -2163,6 +2167,8 @@ class: SessionManagerMode : MinorMode } } + // Try to load the first thumbnail and filmstrip from _inputProgressiveRenderQueue or decode it first, then + // restart the timer to queue the next preview to be processed method: processInputQueue (void;) { if (thumbnailRenderer._inputProgressiveRenderIndex >= thumbnailRenderer._inputProgressiveRenderQueue.size()) return; @@ -2171,7 +2177,7 @@ class: SessionManagerMode : MinorMode let preview = thumbnailRenderer._inputProgressiveRenderPreviews[thumbnailRenderer._inputProgressiveRenderIndex]; thumbnailRenderer._inputProgressiveRenderIndex = thumbnailRenderer._inputProgressiveRenderIndex + 1; - makeSourceRowWidget(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); if (thumbnailRenderer._inputProgressiveRenderIndex < thumbnailRenderer._inputProgressiveRenderQueue.size()) { @@ -2329,8 +2335,8 @@ class: SessionManagerMode : MinorMode if (item neq nil) { let preview = SourcePreviewWidget(nil); - let w = displaySourceRowPreview(node, preview); - makeSourceRowWidget(node, preview); + let w = thumbnailRenderer.displaySourceRowPreview(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), w); } @@ -2338,8 +2344,8 @@ class: SessionManagerMode : MinorMode if (inputItem neq nil) { let preview = SourcePreviewWidget(nil); - let w = displaySourceRowPreview(node, preview); - makeSourceRowWidget(node, preview); + let w = thumbnailRenderer.displaySourceRowPreview(node, preview); + thumbnailRenderer.makeSourceRowWidget(node, preview); _inputsView.setIndexWidget(_inputsModel.indexFromItem(inputItem), w); } } @@ -3249,6 +3255,7 @@ class: SessionManagerMode : MinorMode _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") @@ -3413,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); From 1220f86ef004816ccb7b0a91dab969d84c0103d5 Mon Sep 17 00:00:00 2001 From: deltag0 Date: Wed, 17 Jun 2026 12:23:17 -0400 Subject: [PATCH 21/21] class for common variables Signed-off-by: deltag0 --- .../session_manager/session_manager.mu.in | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) 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 9a6542f4d..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,15 @@ 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 @@ -1073,18 +1082,9 @@ queue. """; class: ThumbnailRenderer { - QTimer _sourceProgressiveRenderTimer; - string[] _sourceProgressiveRenderQueue; - SourcePreviewWidget[] _sourceProgressiveRenderPreviews; - int _sourceProgressiveRenderIndex; - // This queue is used if when the sessionManager receives a previewEvent (usually from filmstrips) - // but haven't yet displayed the current node in _sourceProgressiveRenderQueue - string[] _sourceProgressiveRenderDeferred; - QTimer _inputProgressiveRenderTimer; - string[] _inputProgressiveRenderQueue; - SourcePreviewWidget[] _inputProgressiveRenderPreviews; - int _inputProgressiveRenderIndex; - string[] _inputProgressiveRenderDeferred; + // source and input each get their own render queue/timer/state + ThumbnailManager _source; + ThumbnailManager _input; QDockWidget _dockWidget; QIcon _fallbackSourceIcon; @@ -1094,20 +1094,20 @@ class: ThumbnailRenderer let m = mainWindowWidget(); _dockWidget = QDockWidget("Session Manager", m, Qt.Widget); - _sourceProgressiveRenderTimer = QTimer(_dockWidget); - _sourceProgressiveRenderQueue = string[](); - _sourceProgressiveRenderPreviews = SourcePreviewWidget[](); - _sourceProgressiveRenderIndex = 0; + _source.renderTimer = QTimer(_dockWidget); + _source.renderQueue = string[](); + _source.progressiveRenderPreviews = SourcePreviewWidget[](); + _source.renderIdx = 0; + _source.progressiveRenderDeferred = string[](); - _sourceProgressiveRenderDeferred = string[](); - _inputProgressiveRenderTimer = QTimer(_dockWidget); - _inputProgressiveRenderQueue = string[](); - _inputProgressiveRenderPreviews = SourcePreviewWidget[](); - _inputProgressiveRenderIndex = 0; - _inputProgressiveRenderDeferred = string[](); + _input.renderTimer = QTimer(_dockWidget); + _input.renderQueue = string[](); + _input.progressiveRenderPreviews = SourcePreviewWidget[](); + _input.renderIdx = 0; + _input.progressiveRenderDeferred = string[](); - _sourceProgressiveRenderTimer.setSingleShot(true); - _inputProgressiveRenderTimer.setSingleShot(true); + _source.renderTimer.setSingleShot(true); + _input.renderTimer.setSingleShot(true); } // Load media metadata, text and placheolder image used before loading actual media @@ -1165,7 +1165,7 @@ class: ThumbnailRenderer widget; } - // Try to load or decode media, usually used after displaySourceRowPreview + // Try to load or decode media, method: makeSourceRowWidget (void; string node, SourcePreviewWidget preview) { string sourceNode = nil; @@ -1613,11 +1613,11 @@ class: SessionManagerMode : MinorMode if (_disableUpdates || _progressiveLoadingInProgress) return; _inputOrderLock = true; - thumbnailRenderer._inputProgressiveRenderTimer.stop(); - thumbnailRenderer._inputProgressiveRenderQueue = string[](); - thumbnailRenderer._inputProgressiveRenderDeferred = string[](); - thumbnailRenderer._inputProgressiveRenderPreviews = SourcePreviewWidget[](); - thumbnailRenderer._inputProgressiveRenderIndex = 0; + 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); @@ -1645,13 +1645,13 @@ class: SessionManagerMode : MinorMode let preview = SourcePreviewWidget(nil); let w = thumbnailRenderer.displaySourceRowPreview(innode, preview); _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), w); - thumbnailRenderer._inputProgressiveRenderQueue.push_back(innode); - thumbnailRenderer._inputProgressiveRenderPreviews.push_back(preview); + thumbnailRenderer._input.renderQueue.push_back(innode); + thumbnailRenderer._input.progressiveRenderPreviews.push_back(preview); } } - if (!thumbnailRenderer._inputProgressiveRenderQueue.empty()) - thumbnailRenderer._inputProgressiveRenderTimer.start(0); + if (!thumbnailRenderer._input.renderQueue.empty()) + thumbnailRenderer._input.renderTimer.start(0); _inputOrderLock = false; } @@ -1975,8 +1975,8 @@ class: SessionManagerMode : MinorMode item.setSizeHint(QSize(-1, SOURCE_ROW_HEIGHT)); let preview = SourcePreviewWidget(nil); _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), thumbnailRenderer.displaySourceRowPreview(node, preview)); - thumbnailRenderer._sourceProgressiveRenderQueue.push_back(node); - thumbnailRenderer._sourceProgressiveRenderPreviews.push_back(preview); + thumbnailRenderer._source.renderQueue.push_back(node); + thumbnailRenderer._source.progressiveRenderPreviews.push_back(preview); } // @@ -2132,74 +2132,74 @@ class: SessionManagerMode : MinorMode } } - // Try to load the first thumbnail and filmstrip from _sourceProgressiveRenderQueue or decode it first, then + // 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._sourceProgressiveRenderIndex >= thumbnailRenderer._sourceProgressiveRenderQueue.size()) return; + if (thumbnailRenderer._source.renderIdx >= thumbnailRenderer._source.renderQueue.size()) return; - let node = thumbnailRenderer._sourceProgressiveRenderQueue[thumbnailRenderer._sourceProgressiveRenderIndex]; - let preview = thumbnailRenderer._sourceProgressiveRenderPreviews[thumbnailRenderer._sourceProgressiveRenderIndex]; - thumbnailRenderer._sourceProgressiveRenderIndex = thumbnailRenderer._sourceProgressiveRenderIndex + 1; + 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._sourceProgressiveRenderIndex < thumbnailRenderer._sourceProgressiveRenderQueue.size()) + if (thumbnailRenderer._source.renderIdx < thumbnailRenderer._source.renderQueue.size()) { - thumbnailRenderer._sourceProgressiveRenderTimer.start(0); + thumbnailRenderer._source.renderTimer.start(0); } - else if (!thumbnailRenderer._sourceProgressiveRenderDeferred.empty()) + else if (!thumbnailRenderer._source.progressiveRenderDeferred.empty()) { - thumbnailRenderer._sourceProgressiveRenderQueue = thumbnailRenderer._sourceProgressiveRenderDeferred; - thumbnailRenderer._sourceProgressiveRenderDeferred = string[](); - thumbnailRenderer._sourceProgressiveRenderIndex = 0; + thumbnailRenderer._source.renderQueue = thumbnailRenderer._source.progressiveRenderDeferred; + thumbnailRenderer._source.progressiveRenderDeferred = string[](); + thumbnailRenderer._source.renderIdx = 0; - thumbnailRenderer._sourceProgressiveRenderPreviews = SourcePreviewWidget[](); - for_each (dnode; thumbnailRenderer._sourceProgressiveRenderQueue) + 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._sourceProgressiveRenderPreviews.push_back(preview); + thumbnailRenderer._source.progressiveRenderPreviews.push_back(preview); } - thumbnailRenderer._sourceProgressiveRenderTimer.start(0); + thumbnailRenderer._source.renderTimer.start(0); } } - // Try to load the first thumbnail and filmstrip from _inputProgressiveRenderQueue or decode it first, then + // 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._inputProgressiveRenderIndex >= thumbnailRenderer._inputProgressiveRenderQueue.size()) return; + if (thumbnailRenderer._input.renderIdx >= thumbnailRenderer._input.renderQueue.size()) return; - let node = thumbnailRenderer._inputProgressiveRenderQueue[thumbnailRenderer._inputProgressiveRenderIndex]; - let preview = thumbnailRenderer._inputProgressiveRenderPreviews[thumbnailRenderer._inputProgressiveRenderIndex]; - thumbnailRenderer._inputProgressiveRenderIndex = thumbnailRenderer._inputProgressiveRenderIndex + 1; + 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._inputProgressiveRenderIndex < thumbnailRenderer._inputProgressiveRenderQueue.size()) + if (thumbnailRenderer._input.renderIdx < thumbnailRenderer._input.renderQueue.size()) { - thumbnailRenderer._inputProgressiveRenderTimer.start(0); + thumbnailRenderer._input.renderTimer.start(0); } - else if (!thumbnailRenderer._inputProgressiveRenderDeferred.empty()) + else if (!thumbnailRenderer._input.progressiveRenderDeferred.empty()) { - thumbnailRenderer._inputProgressiveRenderQueue = thumbnailRenderer._inputProgressiveRenderDeferred; - thumbnailRenderer._inputProgressiveRenderDeferred = string[](); - thumbnailRenderer._inputProgressiveRenderIndex = 0; - thumbnailRenderer._inputProgressiveRenderTimer.start(0); + 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._sourceProgressiveRenderTimer.stop(); - thumbnailRenderer._sourceProgressiveRenderQueue = string[](); - thumbnailRenderer._sourceProgressiveRenderPreviews = SourcePreviewWidget[](); - thumbnailRenderer._sourceProgressiveRenderIndex = 0; - thumbnailRenderer._sourceProgressiveRenderDeferred = string[](); + 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(); @@ -2294,7 +2294,7 @@ class: SessionManagerMode : MinorMode resizeColumns(_viewTreeView, _viewModel); - thumbnailRenderer._sourceProgressiveRenderTimer.start(0); + thumbnailRenderer._source.renderTimer.start(0); } catch (exception exc) { @@ -2323,11 +2323,11 @@ class: SessionManagerMode : MinorMode if (node eq nil) return; bool inThumbnailQueue = false; - for (int i = thumbnailRenderer._sourceProgressiveRenderIndex; i < thumbnailRenderer._sourceProgressiveRenderQueue.size(); i++) - if (thumbnailRenderer._sourceProgressiveRenderQueue[i] == node) { inThumbnailQueue = true; break; } + for (int i = thumbnailRenderer._source.renderIdx; i < thumbnailRenderer._source.renderQueue.size(); i++) + if (thumbnailRenderer._source.renderQueue[i] == node) { inThumbnailQueue = true; break; } if (inThumbnailQueue) { - thumbnailRenderer._sourceProgressiveRenderDeferred.push_back(node); + thumbnailRenderer._source.progressiveRenderDeferred.push_back(node); return; } @@ -3655,8 +3655,8 @@ class: SessionManagerMode : MinorMode connect(thumbnailRenderer._dockWidget, QDockWidget.visibilityChanged, visibilityChanged); - connect(thumbnailRenderer._sourceProgressiveRenderTimer, QTimer.timeout, processSourceQueue); - connect(thumbnailRenderer._inputProgressiveRenderTimer, QTimer.timeout, processInputQueue); + connect(thumbnailRenderer._source.renderTimer, QTimer.timeout, processSourceQueue); + connect(thumbnailRenderer._input.renderTimer, QTimer.timeout, processInputQueue); updateNavUI(); }