change: UI and migrate to gtest

This commit is contained in:
2026-07-27 05:27:32 +08:00
parent dd5508e571
commit a84e75ef47
56 changed files with 4484 additions and 4949 deletions
@@ -207,7 +207,10 @@ void NodeParamViewConnectedLabel::update_label()
void NodeParamViewConnectedLabel::update_value_tree()
{
if (value_tree_ && viewer_ && value_tree_->isVisible()) {
value_tree_->set_node(input_, viewer_->get_playhead());
int64_t pn, pd;
oakengine_viewer_get_playhead(
reinterpret_cast<OakEngineNode *>(viewer_), &pn, &pd);
value_tree_->set_node(input_, Rational(pn, pd));
}
}
+22
View File
@@ -45,6 +45,22 @@ NodeViewToolBar::NodeViewToolBar(QWidget *parent)
&NodeViewToolBar::mini_map_enabled_toggled);
layout->addWidget(minimap_btn_);
// Zoom controls per the UI design reference: [+] [-] [Fit]
zoom_out_btn_ = new QPushButton();
connect(zoom_out_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::zoom_out_clicked);
layout->addWidget(zoom_out_btn_);
zoom_in_btn_ = new QPushButton();
connect(zoom_in_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::zoom_in_clicked);
layout->addWidget(zoom_in_btn_);
fit_btn_ = new QPushButton();
connect(fit_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::fit_clicked);
layout->addWidget(fit_btn_);
layout->addStretch();
retranslate();
@@ -65,12 +81,18 @@ void NodeViewToolBar::retranslate()
{
add_node_btn_->setToolTip(tr("Add Node"));
minimap_btn_->setToolTip(tr("Toggle Mini-Map"));
zoom_in_btn_->setToolTip(tr("Zoom In"));
zoom_out_btn_->setToolTip(tr("Zoom Out"));
fit_btn_->setToolTip(tr("Fit to Content"));
fit_btn_->setText(tr("Fit"));
}
void NodeViewToolBar::update_icons()
{
add_node_btn_->setIcon(icon::add);
minimap_btn_->setIcon(icon::mini_map);
zoom_in_btn_->setIcon(icon::zoom_in);
zoom_out_btn_->setIcon(icon::zoom_out);
}
}
+12
View File
@@ -41,6 +41,12 @@ signals:
void mini_map_enabled_toggled(bool e);
void zoom_in_clicked();
void zoom_out_clicked();
void fit_clicked();
protected:
virtual void changeEvent(QEvent *e) override;
@@ -52,6 +58,12 @@ private:
QPushButton *add_node_btn_;
QPushButton *minimap_btn_;
QPushButton *zoom_in_btn_;
QPushButton *zoom_out_btn_;
QPushButton *fit_btn_;
};
}
+6
View File
@@ -44,6 +44,12 @@ NodeWidget::NodeWidget(QWidget *parent)
&NodeView::set_mini_map_enabled);
connect(toolbar_, &NodeViewToolBar::add_node_clicked, node_view_,
&NodeView::show_add_menu);
connect(toolbar_, &NodeViewToolBar::zoom_in_clicked, node_view_,
&NodeView::zoom_in);
connect(toolbar_, &NodeViewToolBar::zoom_out_clicked, node_view_,
&NodeView::zoom_out);
connect(toolbar_, &NodeViewToolBar::fit_clicked, node_view_,
&NodeView::center_on_items_bounding_rect);
// Set defaults
toolbar_->set_mini_map_enabled(true);
+5 -2
View File
@@ -166,7 +166,7 @@ void TimeBasedView::set_viewer_node(ViewerOutput *v)
if (viewer_) {
viewer_sub_ = oakengine_event_subscribe(
reinterpret_cast<OakEngineNode *>(viewer_),
viewer_,
OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED,
[](const oakengine_event *, void *userdata) {
static_cast<TimeBasedView *>(userdata)->viewport()->update();
@@ -280,7 +280,10 @@ bool TimeBasedView::playhead_release(QMouseEvent *)
qreal TimeBasedView::get_playhead_x()
{
if (viewer_) {
return time_to_scene(viewer_->get_playhead());
int64_t pn, pd;
oakengine_viewer_get_playhead(
reinterpret_cast<OakEngineNode *>(viewer_), &pn, &pd);
return time_to_scene(Rational(pn, pd));
} else {
return 0;
}
@@ -50,7 +50,8 @@ TimelineAndTrackView::TimelineAndTrackView(Qt::Alignment vertical_alignment,
connect(track_view_->verticalScrollBar(), &QScrollBar::valueChanged, this,
&TimelineAndTrackView::tracks_value_changed);
splitter_->setSizes({ 1, width() });
// Default track header width: 180px (per UI design reference)
splitter_->setSizes({ 180, width() });
}
QSplitter *TimelineAndTrackView::splitter() const
+107 -8
View File
@@ -31,6 +31,7 @@
#include <QFileInfo>
#include <QInputDialog>
#include <QProcess>
#include <QSlider>
#include <QSplitter>
#include <QUrl>
#include <QVBoxLayout>
@@ -80,6 +81,7 @@
#include "widget/menu/menushared.h"
#include "widget/nodeparamview/nodeparamview.h"
#include "widget/timeruler/timeruler.h"
#include "widget/toolbar/toolbar.h"
#include "widget/viewer/vieweroutpututils.h"
namespace olive
@@ -159,6 +161,26 @@ QVector<Footage *> get_selected_proxy_footage(const QVector<Block *> &blocks)
return footage;
}
// Zoom slider <-> scale mapping (logarithmic: slider 0..1000 -> 0.1..10000 px/s)
constexpr double k_zoom_scale_min = 0.1;
constexpr double k_zoom_scale_max = 10000.0;
double zoom_slider_to_scale(int v)
{
return k_zoom_scale_min *
std::pow(k_zoom_scale_max / k_zoom_scale_min, v / 1000.0);
}
int scale_to_zoom_slider(double s)
{
if (s <= k_zoom_scale_min)
return 0;
if (s >= k_zoom_scale_max)
return 1000;
return qRound(1000.0 * std::log(s / k_zoom_scale_min) /
std::log(k_zoom_scale_max / k_zoom_scale_min));
}
} // namespace
TimelineWidget::TimelineWidget(QWidget *parent)
@@ -173,6 +195,66 @@ TimelineWidget::TimelineWidget(QWidget *parent)
vert_layout->setSpacing(0);
vert_layout->setContentsMargins(0, 0, 0, 0);
// Application toolbar row (31px) — replaces the dockable ToolPanel by
// default. The dockable ToolPanel remains available via the Window menu.
QHBoxLayout *toolbar_row = new QHBoxLayout();
toolbar_row->setContentsMargins(0, 0, 0, 0);
toolbar_row->setSpacing(4);
Toolbar *toolbar = new Toolbar(this);
toolbar->setFixedHeight(31);
toolbar->set_tool(Core::instance()->tool());
toolbar->set_snapping(Core::instance()->snapping());
toolbar_row->addWidget(toolbar);
connect(toolbar, &Toolbar::tool_changed, Core::instance(),
&Core::set_tool);
connect(Core::instance(), &Core::tool_changed, toolbar,
&Toolbar::set_tool);
connect(toolbar, &Toolbar::snapping_changed, Core::instance(),
&Core::set_snapping);
connect(Core::instance(), &Core::snapping_changed, toolbar,
&Toolbar::set_snapping);
connect(toolbar, &Toolbar::selected_transition_changed, Core::instance(),
&Core::set_selected_transition_object);
toolbar_row->addStretch();
// Zoom slider (logarithmic)
zoom_slider_ = new QSlider(Qt::Horizontal, this);
zoom_slider_->setRange(0, 1000);
zoom_slider_->setFixedWidth(120);
zoom_slider_->setToolTip(tr("Zoom"));
zoom_slider_->setValue(scale_to_zoom_slider(get_scale()));
toolbar_row->addWidget(zoom_slider_);
connect(zoom_slider_, &QSlider::valueChanged, this, [this](int v) {
set_scale(zoom_slider_to_scale(v));
});
// Track height slider
track_height_slider_ = new QSlider(Qt::Horizontal, this);
track_height_slider_->setRange(0, 8);
track_height_slider_->setFixedWidth(80);
track_height_slider_->setToolTip(tr("Track Height"));
track_height_slider_->setValue(3);
toolbar_row->addWidget(track_height_slider_);
connect(track_height_slider_, &QSlider::valueChanged, this, [this](int v) {
if (!get_connected_node()) {
return;
}
double h = oakengine_track_height_minimum() +
v * oakengine_track_height_interval();
foreach (Track *t, sequence()->get_tracks()) {
oakengine_track_set_height(
reinterpret_cast<OakEngineSequence *>(sequence()), t->type(),
t->index(), h);
}
});
vert_layout->addLayout(toolbar_row);
QHBoxLayout *ruler_and_time_layout = new QHBoxLayout();
vert_layout->addLayout(ruler_and_time_layout);
@@ -396,6 +478,11 @@ void TimelineWidget::ScaleChangedEvent(const double &scale)
view->view()->set_scale(scale);
}
// Keep zoom slider in sync when scale changes elsewhere (ctrl+wheel etc.)
zoom_slider_->blockSignals(true);
zoom_slider_->setValue(scale_to_zoom_slider(scale));
zoom_slider_->blockSignals(false);
if (rubberband_.isVisible()) {
QMetaObject::invokeMethod(this, &TimelineWidget::force_update_rubber_band,
Qt::QueuedConnection);
@@ -448,17 +535,20 @@ void TimelineWidget::ConnectNodeEvent(ViewerOutput *n)
// Subscribe to track-level events via bridge (subscriptions in add_track)
connect(bridge_, &EngineEventBridge::track_index_changed, this,
[this](OakEngineTrack *source, int old_index, int new_index) {
Track *track = reinterpret_cast<Track *>(source);
track_updated(track->type());
track_index_changed(track, old_index, new_index);
track_updated(static_cast<Track::Type>(
oakengine_track_type(source)));
track_index_changed(reinterpret_cast<Track *>(source),
old_index, new_index);
});
connect(bridge_, &EngineEventBridge::track_height_changed, this,
[this](OakEngineTrack *source, double) {
track_updated(reinterpret_cast<Track *>(source)->type());
track_updated(static_cast<Track::Type>(
oakengine_track_type(source)));
});
connect(bridge_, &EngineEventBridge::track_blocks_refreshed, this,
[this](OakEngineTrack *source) {
track_updated(reinterpret_cast<Track *>(source)->type());
track_updated(static_cast<Track::Type>(
oakengine_track_type(source)));
});
connect(bridge_, &EngineEventBridge::track_block_added, this,
[this](OakEngineBlock *block, qint64, qint64) {
@@ -519,7 +609,10 @@ void TimelineWidget::ConnectNodeEvent(ViewerOutput *n)
{
oak_video_params vp;
oakengine_viewer_get_video_params(handle, 0, &vp);
SetTimebase(Rational(vp.time_base_den, vp.time_base_num));
// time_base is the frame duration (e.g. 1/25); reconstruct it as
// Rational(num, den). (Previously num/den were swapped here, which
// produced the frame rate and triggered "INVALID TIMEBASE".)
SetTimebase(Rational(vp.time_base_num, vp.time_base_den));
}
for (int i = 0; i < views_.size(); i++) {
@@ -1954,9 +2047,15 @@ void TimelineWidget::track_updated(Track::Type type)
update_viewports(type);
}
void TimelineWidget::block_updated()
void TimelineWidget::block_updated(OakEngineBlock *)
{
update_viewports(static_cast<Block *>(sender())->track()->type());
// The old implementation used sender() to obtain the Block and then
// queried its track type to update only one viewport. With the bridge
// approach sender() is the EngineEventBridge (not a Block), and the
// OakEngineBlock handle is opaque -- there is no C API to retrieve its
// track type. Simply refresh all viewports; the cost is negligible
// (just a repaint request).
update_viewports();
}
void TimelineWidget::update_horizontal_splitters()
+5 -1
View File
@@ -25,6 +25,7 @@
#include <QHash>
#include <QScrollBar>
#include <QRubberBand>
#include <QSlider>
#include <QWidget>
#include "core.h"
@@ -361,6 +362,9 @@ private:
QSplitter *view_splitter_;
QSlider *zoom_slider_;
QSlider *track_height_slider_;
void *subtitle_show_command_;
OakEngineTrack *subtitle_tentative_track_;
@@ -422,7 +426,7 @@ private slots:
void track_updated(Track::Type type);
void block_updated();
void block_updated(OakEngineBlock *block = nullptr);
void update_horizontal_splitters();
@@ -161,18 +161,38 @@ void TrackViewItem::line_edit_cancelled()
void TrackViewItem::update_label()
{
// Per the UI design reference, tracks are identified by an NLE-style
// type+number prefix (V1/V2 for video, A1/A2 for audio, S1 for subtitle)
// followed by the track's custom label or default name.
QString prefix;
switch (track_->type()) {
case Track::k_video:
prefix = QStringLiteral("V%1").arg(track_->index() + 1);
break;
case Track::k_audio:
prefix = QStringLiteral("A%1").arg(track_->index() + 1);
break;
case Track::k_subtitle:
prefix = QStringLiteral("S%1").arg(track_->index() + 1);
break;
}
char label_buf[256];
oakengine_node_get_label(
reinterpret_cast<OakEngineNode *>(track_),
label_buf, sizeof(label_buf));
QString display;
if (label_buf[0]) {
label_->setText(QString::fromUtf8(label_buf));
display = QString::fromUtf8(label_buf);
} else {
oakengine_node_get_name(
reinterpret_cast<OakEngineNode *>(track_),
label_buf, sizeof(label_buf));
label_->setText(QString::fromUtf8(label_buf));
display = QString::fromUtf8(label_buf);
}
label_->setText(prefix.isEmpty() ? display
: QStringLiteral("%1 %2").arg(prefix, display));
}
void TrackViewItem::show_context_menu(const QPoint &p)
+155 -2
View File
@@ -176,6 +176,10 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
, multicam_panel_(nullptr)
, bridge_(new EngineEventBridge(this))
, audio_processor_(oakengine_audio_processor_create())
, overlay_(nullptr)
, info_chip_(nullptr)
, safe_frame_btn_(nullptr)
, overlay_zoom_index_(5)
{
// Set up main layout
QVBoxLayout *layout = new QVBoxLayout(this);
@@ -224,6 +228,9 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
&ViewerSizer::hand_drag_move);
sizer_->set_widget(display_widget_);
// Create the design-reference overlay (info chip + zoom/safe-frame buttons)
create_overlay();
// Make the display widget the first tabbable widget. While the viewer display cannot actually
// be interacted with by tabbing, it prevents the actual first tabbable widget (the playhead
// slider in `controls_`) from getting auto-focused any time the panel is maximized (with `)
@@ -234,8 +241,13 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
connect_timeline_view(waveform_view_);
layout->addWidget(waveform_view_);
// Create time ruler
layout->addWidget(ruler());
// Per the Oak UI design reference the viewer no longer shows its own time
// ruler: it duplicates the timeline's ruler and clutters the viewer. The
// ruler object still exists (created by TimeBasedWidget) and continues to
// back marker/work-area/snapping logic and the scrollbar; we simply do not
// add it to the visible layout. Scrubbing is provided by the transport
// controls and the scrollbar below.
// layout->addWidget(ruler());
// Create scrollbar
layout->addWidget(scrollbar());
@@ -493,6 +505,8 @@ void ViewerWidget::resizeEvent(QResizeEvent *event)
super::resizeEvent(event);
update_minimum_scale();
position_overlay();
}
OakEnginePreviewRequest *ViewerWidget::get_single_frame(const Rational &t,
@@ -2260,6 +2274,8 @@ void ViewerWidget::set_viewer_resolution(int width, int height)
foreach (ViewerWindow *vw, windows_) {
vw->set_resolution(width, height);
}
update_info_chip();
}
void ViewerWidget::set_viewer_pixel_aspect(const Rational &ratio)
@@ -2323,6 +2339,143 @@ void ViewerWidget::set_zoom_from_menu(QAction *action)
s.height() / 2);
}
void ViewerWidget::create_overlay()
{
// A thin transparent strip laid over the top of the viewer display. Left
// side carries an info chip (resolution + frame rate); right side carries
// zoom in/out/fit and a safe-frame toggle, per the UI design reference.
overlay_ = new QWidget(sizer_);
overlay_->setAttribute(Qt::WA_TransparentForMouseEvents, false);
overlay_->setAutoFillBackground(false);
auto *overlay_layout = new QHBoxLayout(overlay_);
overlay_layout->setContentsMargins(6, 4, 6, 4);
overlay_layout->setSpacing(4);
info_chip_ = new QLabel(overlay_);
info_chip_->setStyleSheet(QStringLiteral(
"QLabel { background-color: rgba(0,0,0,160); color: white; "
"border-radius: 3px; padding: 2px 6px; }"));
info_chip_->setVisible(false);
overlay_layout->addWidget(info_chip_);
overlay_layout->addStretch();
const QString btn_style = QStringLiteral(
"QToolButton { background-color: rgba(0,0,0,160); color: white; "
"border: none; border-radius: 3px; padding: 2px 6px; } "
"QToolButton:hover { background-color: rgba(0,0,0,200); } "
"QToolButton:checked { background-color: rgba(0,120,215,200); }");
auto make_btn = [this, &btn_style](const QString &text,
const QString &tooltip) {
auto *b = new QToolButton(overlay_);
b->setText(text);
b->setToolTip(tooltip);
b->setStyleSheet(btn_style);
b->setAutoRaise(false);
return b;
};
QToolButton *zoom_out_btn = make_btn(QStringLiteral("\u2212"), tr("Zoom Out"));
QToolButton *zoom_in_btn = make_btn(QStringLiteral("+"), tr("Zoom In"));
QToolButton *zoom_fit_btn = make_btn(QStringLiteral("\u2922"), tr("Fit to Window"));
safe_frame_btn_ = make_btn(QStringLiteral("\u25A2"), tr("Safe Margins"));
safe_frame_btn_->setCheckable(true);
overlay_layout->addWidget(zoom_out_btn);
overlay_layout->addWidget(zoom_in_btn);
overlay_layout->addWidget(zoom_fit_btn);
overlay_layout->addWidget(safe_frame_btn_);
connect(zoom_out_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_out);
connect(zoom_in_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_in);
connect(zoom_fit_btn, &QToolButton::clicked, this,
&ViewerWidget::overlay_zoom_fit);
connect(safe_frame_btn_, &QToolButton::toggled, this,
[this](bool) { overlay_toggle_safe_frame(); });
overlay_->raise();
}
void ViewerWidget::position_overlay()
{
if (!overlay_ || !sizer_) {
return;
}
// Span the full width of the sizer, hugging the top edge.
overlay_->setGeometry(0, 0, sizer_->width(), overlay_->sizeHint().height());
overlay_->raise();
}
void ViewerWidget::update_info_chip()
{
if (!info_chip_) {
return;
}
if (!get_connected_node()) {
info_chip_->setVisible(false);
return;
}
VideoParams vp = viewer_output_video_params(get_connected_node());
if (vp.width() <= 0 || vp.height() <= 0) {
info_chip_->setVisible(false);
return;
}
const double fps = vp.frame_rate().to_double();
const double fps_rounded = qRound(fps);
QString fps_str = (qFuzzyCompare(fps, fps_rounded))
? QString::number(static_cast<int>(fps_rounded))
: QString::number(fps, 'f', 2);
info_chip_->setText(QStringLiteral("%1\u00d7%2 \u00b7 %3 FPS")
.arg(vp.width())
.arg(vp.height())
.arg(fps_str));
info_chip_->setVisible(true);
}
void ViewerWidget::overlay_zoom_in()
{
if (overlay_zoom_index_ < ViewerSizer::k_zoom_level_count - 1) {
overlay_zoom_index_++;
}
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(ViewerSizer::k_zoom_levels[overlay_zoom_index_],
s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_zoom_out()
{
if (overlay_zoom_index_ > 0) {
overlay_zoom_index_--;
}
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(ViewerSizer::k_zoom_levels[overlay_zoom_index_],
s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_zoom_fit()
{
// The zoom menu uses -1 to mean "fit to window".
auto s = sizer_->get_container_size();
sizer_->set_zoom_anchored(-1, s.width() / 2, s.height() / 2);
}
void ViewerWidget::overlay_toggle_safe_frame()
{
if (!safe_frame_btn_) {
return;
}
display_widget_->set_safe_margins(
ViewerSafeMarginInfo(safe_frame_btn_->isChecked()));
}
void ViewerWidget::viewer_invalidated_video_range(const TimeRange &range)
{
// If our current frame is within this range, we need to update
+17
View File
@@ -27,6 +27,7 @@
#include <QPushButton>
#include <QScrollBar>
#include <QTimer>
#include <QToolButton>
#include <QWidget>
#include "oakengine/audio.h"
@@ -219,6 +220,11 @@ protected:
return display_widget_;
}
// Overlay (info chip + zoom/safe-frame buttons) per the UI design reference
void create_overlay();
void position_overlay();
void update_info_chip();
void IgnoreNextScrubEvent()
{
ignore_scrub_++;
@@ -228,6 +234,12 @@ protected:
void set_waveform_mode(WaveformMode wf);
private slots:
void overlay_zoom_in();
void overlay_zoom_out();
void overlay_zoom_fit();
void overlay_toggle_safe_frame();
private:
int64_t get_timestamp() const
{
@@ -285,6 +297,11 @@ private:
ViewerSizer *sizer_;
QWidget *overlay_;
QLabel *info_chip_;
QToolButton *safe_frame_btn_;
int overlay_zoom_index_;
int playback_speed_;
Rational last_time_;