diff --git a/src/plugins/rv-packages/session_manager/fallback_thumbnail.png b/src/plugins/rv-packages/session_manager/fallback_thumbnail.png new file mode 100644 index 000000000..e6591d42a Binary files /dev/null and b/src/plugins/rv-packages/session_manager/fallback_thumbnail.png differ 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 f660c1fa1..4aed51b6e 100755 --- a/src/plugins/rv-packages/session_manager/session_manager.mu.in +++ b/src/plugins/rv-packages/session_manager/session_manager.mu.in @@ -20,6 +20,10 @@ ViewSubComponent := 2; LayerSubComponent := 3; ChannelSubComponent := 4; +FILMSTRIP_FRAME_WIDTH := 240; +PREVIEW_WIDTH := 80; +PREVIEW_HEIGHT := 45; + \: itemNode (string; QStandardItem item) { let d = item.data(Qt.UserRole + 2); @@ -601,6 +605,120 @@ documentation: """ QStandardItemModel with modified drag and drop mime types. """; +// Displays a static thumbnail image. Falls back to a placeholder pixmap if none loaded. +class: ThumbnailWidget : QLabel +{ + method: ThumbnailWidget (ThumbnailWidget; QWidget parent) + { + QLabel.QLabel(this, parent); + setScaledContents(true); + } + + method: setFallback (void; QPixmap px) { setPixmap(px); } + + method: load (void; string path) + { + let px = QPixmap.fromImage(QImage(path, ""), Qt.AutoColor); + if (!px.isNull()) setPixmap(px); + } +} + +// Displays a scrubable filmstrip that will show the corresponding frame +// based on mouse position +class: FilmstripWidget : QLabel +{ + QImage _strip; + int _frameWidth; + bool _loaded; + + method: FilmstripWidget (FilmstripWidget; QWidget parent) + { + QLabel.QLabel(this, parent); + _frameWidth = FILMSTRIP_FRAME_WIDTH; + _loaded = false; + setScaledContents(true); + setMouseTracking(true); + } + + method: showFrameAtX (void; int mouseX) + { + if (!_loaded) return; + let nat_w = _strip.width(), + prop_x = float(mouseX) / float(width()), + fx = int(prop_x * float(nat_w) / float(_frameWidth) + 0.5) * _frameWidth, + clamped = if fx > nat_w - _frameWidth then nat_w - _frameWidth + else if fx < 0 then 0 + else fx; + let frame = _strip.copy(QRect(clamped, 0, _frameWidth, _strip.height())); + setPixmap(QPixmap.fromImage(frame, Qt.AutoColor)); + } + + method: isLoaded (bool;) { _loaded; } + + method: load (void; string path) + { + let img = QImage(path, ""); + if (!img.isNull()) + { + _strip = img; + _loaded = true; + } + } + + method: mouseMoveEvent (void; QMouseEvent event) + { + showFrameAtX(event.position().toPoint().x()); + QWidget.mouseMoveEvent(this, event); + } +} + +// Stacks a FilmstripWidget and ThumbnailWidget. Shows thumbnail by default; +// on hover enter shows the filmstrip scrubbed to the cursor position. +class: SourcePreviewWidget : QWidget +{ + FilmstripWidget _filmstrip; + ThumbnailWidget _thumbnail; + + method: SourcePreviewWidget (SourcePreviewWidget; QWidget parent) + { + QWidget.QWidget(this, parent); + setAttribute(Qt.WA_Hover, true); + + _thumbnail = ThumbnailWidget(this); + _thumbnail.setGeometry(QRect(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT)); + _thumbnail.show(); + + _filmstrip = FilmstripWidget(this); + _filmstrip.setGeometry(QRect(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT)); + _filmstrip.hide(); + } + + method: setFallback (void; QPixmap px) { _thumbnail.setFallback(px); } + method: loadStrip (void; string path) { _filmstrip.load(path); } + method: loadThumbnail (void; string path) { _thumbnail.load(path); } + + method: event (bool; QEvent e) + { + if (e.type() == QEvent.HoverEnter) + { + if (_filmstrip.isLoaded()) + { + _filmstrip.showFrameAtX(mapFromGlobal(QCursor.pos()).x()); + _filmstrip.show(); + _thumbnail.hide(); + } + return true; + } + else if (e.type() == QEvent.HoverLeave) + { + _filmstrip.hide(); + _thumbnail.show(); + return true; + } + return QWidget.event(this, e); + } +} + class: NodeModel : QStandardItemModel { method: NodeModel (NodeModel; QObject parent) @@ -980,6 +1098,7 @@ class: SessionManagerMode : MinorMode QIcon _layerIcon; QIcon _channelIcon; QIcon _videoIcon; + QIcon _fallbackSourceIcon; bool _inputOrderLock; bool _disableUpdates; bool _progressiveLoadingInProgress; @@ -1360,20 +1479,28 @@ class: SessionManagerMode : MinorMode _inputOrderLock = true; _inputsModel.clear(); - let connections = nodeInputs(node), - vnodes = viewNodes(); + let connections = nodeInputs(node); for_index (i; connections) { - let innode = connections[i], - item = QStandardItem(iconForNode(innode), uiName(innode)), - vindex = indexOfItem(vnodes, innode); + let innode = connections[i], + isSource = nodeType(innode) == "RVSourceGroup", + item = QStandardItem(iconForNode(innode), uiName(innode)); item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled); item.setData(QVariant(innode), Qt.UserRole + 2); item.setEditable(false); - + + if (isSource) + { + item.setText(""); + item.setSizeHint(QSize(-1, 55)); + } + _inputsModel.appendRow(item); + + if (isSource) + _inputsView.setIndexWidget(_inputsModel.indexFromItem(item), makeSourceRowWidget(innode)); } _inputOrderLock = false; @@ -1662,6 +1789,72 @@ class: SessionManagerMode : MinorMode item; } + method: makeSourceRowWidget (QWidget; string node) + { + let widget = QWidget(nil, 0), + layout = QHBoxLayout(widget); + widget.setStyleSheet("background: transparent;"); + layout.setContentsMargins(8, 0, 8, 0); + layout.setSpacing(5); + + // Source preview: stacks thumbnail (default) and filmstrip (on hover) + let preview = SourcePreviewWidget(widget); + preview.setFixedSize(QSize(PREVIEW_WIDTH, PREVIEW_HEIGHT)); + preview.setFallback(_fallbackSourceIcon.pixmap(QSize(PREVIEW_WIDTH, PREVIEW_HEIGHT))); + + try + { + let sn = sourceNodeOfGroup(node); + + // Fire events that fetch filmstrip/thumbnail and return file paths + let filmstripPath = sendInternalEvent("session_manager-get-filmstrip-path", sn); + let thumbnailPath = sendInternalEvent("session_manager-get-thumbnail-path", sn); + + if (filmstripPath != "" && io.path.exists(filmstripPath)) + preview.loadStrip(filmstripPath); + if (thumbnailPath != "" && io.path.exists(thumbnailPath)) + preview.loadThumbnail(thumbnailPath); + } + catch (...) { ; } + + layout.addWidget(preview); + + // Text column + let textWidget = QWidget(widget), + textLayout = QVBoxLayout(textWidget); + textWidget.setStyleSheet("background: transparent;"); + textLayout.setSpacing(3); + + let nameLabel = QLabel(uiName(node), textWidget); + nameLabel.setStyleSheet("color: #d0d0d0; background: transparent;"); + textLayout.addWidget(nameLabel); + + string meta = ""; + try + { + let sn = sourceNodeOfGroup(node), + mprop = getStringProperty(sn + ".media.movie"); + if (!mprop.empty()) + { + let parts = io.path.basename(mprop.front()).split("."); + if (parts.size() > 1) meta = parts.back(); + } + } + catch (exception exc) + { + print("WARNING: Could not get media type info for %s - %s\n" % (uiName(node), exc)); + } + + let metaLabel = QLabel(if meta == "" then "—" else meta, textWidget); + metaLabel.setStyleSheet("color: #888888; background: transparent; font-size: 9px;"); + textLayout.addWidget(metaLabel); + textLayout.addStretch(1); + + layout.addWidget(textWidget, 1); + + widget; + } + method: newNodeRow (void; QStandardItem parentItem, string node, @@ -1692,6 +1885,13 @@ class: SessionManagerMode : MinorMode if (node == viewNode()) head(tail(statusItems)).setText("\u2714"); addRow(parentItem, item : statusItems); + if (source) + { + item.setText(""); + item.setSizeHint(QSize(-1, 55)); + _viewTreeView.setIndexWidget(_viewModel.indexFromItem(item), makeSourceRowWidget(node)); + } + // // Tabs in tooltips make win32 Qt crash. // @@ -2929,6 +3129,7 @@ class: SessionManagerMode : MinorMode _viewTreeView.setDragDropMode(QAbstractItemView.DragDrop); _viewTreeView.setDefaultDropAction(Qt.MoveAction); _viewTreeView.setExpandsOnDoubleClick(false); + _viewTreeView.setIndentation(10); _inputsView.setModel(_inputsModel); _inputsView.setDragEnabled(true); @@ -2986,6 +3187,7 @@ 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);