change: UI and migrate to gtest
This commit is contained in:
@@ -45,7 +45,8 @@ QFrame *QtUtils::create_vertical_line()
|
||||
|
||||
QString QtUtils::get_formatted_date_time(const QDateTime &dt)
|
||||
{
|
||||
return dt.toString(Qt::TextDate);
|
||||
// ISO-style date/time (e.g. "2026-06-03 20:25") per the UI design reference
|
||||
return dt.toString(QStringLiteral("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
|
||||
QStringList QtUtils::word_wrap_string(const QString &s, const QFontMetrics &fm,
|
||||
|
||||
@@ -64,7 +64,7 @@ void ParamPanel::set_contexts(const QVector<Node *> &contexts)
|
||||
|
||||
void ParamPanel::retranslate()
|
||||
{
|
||||
set_title(tr("Parameter Editor"));
|
||||
set_title(tr("Inspector"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-5
@@ -3303,7 +3303,7 @@ This setting is equivalent to the `preset` setting in libx264.</source>
|
||||
<message>
|
||||
<location filename="../panel/history/historypanel.cpp" line="+39" />
|
||||
<source>History</source>
|
||||
<translation type="unfinished" />
|
||||
<translation>历史记录</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -5583,8 +5583,8 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0.</sour
|
||||
<name>olive::ParamPanel</name>
|
||||
<message>
|
||||
<location filename="../panel/param/param.cpp" line="+67" />
|
||||
<source>Parameter Editor</source>
|
||||
<translation>参数编辑器</translation>
|
||||
<source>Inspector</source>
|
||||
<translation>检查器</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -5854,6 +5854,14 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0.</sour
|
||||
<source>Use glFinish</source>
|
||||
<translation>使用glFinish</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Behavior</source>
|
||||
<translation>行为</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable hover focus</source>
|
||||
<translation>启用悬停聚焦</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>olive::PreferencesDialog</name>
|
||||
@@ -7290,8 +7298,8 @@ Do you wish to continue?</source>
|
||||
<message numerus="yes">
|
||||
<location filename="../dialog/preferences/tabs/preferencesgeneraltab.cpp" line="-20" />
|
||||
<source>%n minute(s)</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform />
|
||||
<translation numerus="yes">
|
||||
<numerusform>%n 分钟</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
</context>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -44,12 +44,44 @@ MainStatusBar::MainStatusBar(QWidget *parent)
|
||||
bar_->setMaximum(100);
|
||||
bar_->setVisible(false);
|
||||
|
||||
// Permanent right-hand sequence info chips (resolution + frame rate)
|
||||
resolution_label_ = new QLabel();
|
||||
fps_label_ = new QLabel();
|
||||
resolution_label_->setContentsMargins(6, 0, 6, 0);
|
||||
fps_label_->setContentsMargins(6, 0, 6, 0);
|
||||
addPermanentWidget(resolution_label_);
|
||||
addPermanentWidget(fps_label_);
|
||||
resolution_label_->setVisible(false);
|
||||
fps_label_->setVisible(false);
|
||||
|
||||
showMessage(tr("Welcome to %1 %2")
|
||||
.arg(QCoreApplication::applicationName(),
|
||||
QCoreApplication::applicationVersion()),
|
||||
10000);
|
||||
}
|
||||
|
||||
void MainStatusBar::set_sequence_info(int width, int height, double fps)
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
resolution_label_->setVisible(false);
|
||||
fps_label_->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
resolution_label_->setText(QStringLiteral("%1\u00d7%2").arg(width).arg(height));
|
||||
|
||||
// Show the frame rate as an integer when it is whole, otherwise keep one
|
||||
// decimal place (e.g. 23.976 FPS).
|
||||
const double rounded = qRound(fps);
|
||||
QString fps_str = (qFuzzyCompare(fps, rounded))
|
||||
? QString::number(static_cast<int>(rounded))
|
||||
: QString::number(fps, 'f', 2);
|
||||
fps_label_->setText(tr("%1 FPS").arg(fps_str));
|
||||
|
||||
resolution_label_->setVisible(true);
|
||||
fps_label_->setVisible(true);
|
||||
}
|
||||
|
||||
void MainStatusBar::connect_task_manager(EngineEventBridge *bridge)
|
||||
{
|
||||
bridge_ = bridge;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#ifndef OAK_MAINSTATUSBAR_H
|
||||
#define OAK_MAINSTATUSBAR_H
|
||||
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QStatusBar>
|
||||
|
||||
@@ -34,6 +35,10 @@ class EngineEventBridge;
|
||||
|
||||
/**
|
||||
* @brief Shows abbreviated information from the global TaskManager
|
||||
*
|
||||
* Per the Oak UI design reference the status bar is a global information
|
||||
* strip. The transient task message is shown on the left while the active
|
||||
* sequence's resolution and frame rate are shown permanently on the right.
|
||||
*/
|
||||
class MainStatusBar : public QStatusBar {
|
||||
Q_OBJECT
|
||||
@@ -42,6 +47,14 @@ public:
|
||||
|
||||
void connect_task_manager(EngineEventBridge *bridge);
|
||||
|
||||
/**
|
||||
* @brief Update the permanent right-hand sequence info chips.
|
||||
*
|
||||
* Plain (non-slot) method taking primitives so no engine C++ type leaks
|
||||
* into the MOC-processed header. Pass width <= 0 to clear the chips.
|
||||
*/
|
||||
void set_sequence_info(int width, int height, double fps);
|
||||
|
||||
signals:
|
||||
void double_clicked();
|
||||
|
||||
@@ -60,6 +73,9 @@ private:
|
||||
|
||||
QProgressBar *bar_;
|
||||
|
||||
QLabel *resolution_label_;
|
||||
QLabel *fps_label_;
|
||||
|
||||
OakEngineTask *connected_task_;
|
||||
|
||||
int64_t task_progress_sub_;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QScreen>
|
||||
#include <QToolBar>
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QOffscreenSurface>
|
||||
@@ -44,7 +43,6 @@
|
||||
#include "oakengine/undo.h"
|
||||
|
||||
#include "widget/viewer/vieweroutpututils.h"
|
||||
#include "widget/toolbar/toolbar.h"
|
||||
#include "core.h"
|
||||
namespace olive
|
||||
{
|
||||
@@ -84,34 +82,11 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
// Create and set status bar
|
||||
event_bridge_ = new EngineEventBridge(this);
|
||||
MainStatusBar *status_bar = new MainStatusBar(this);
|
||||
status_bar->connect_task_manager(event_bridge_);
|
||||
connect(status_bar, &MainStatusBar::double_clicked, this,
|
||||
status_bar_ = new MainStatusBar(this);
|
||||
status_bar_->connect_task_manager(event_bridge_);
|
||||
connect(status_bar_, &MainStatusBar::double_clicked, this,
|
||||
&MainWindow::status_bar_double_clicked);
|
||||
setStatusBar(status_bar);
|
||||
|
||||
// Create application toolbar (31px) — replaces the dockable ToolPanel by default
|
||||
tool_bar_ = new QToolBar(this);
|
||||
tool_bar_->setObjectName(QStringLiteral("AppToolbar"));
|
||||
tool_bar_->setFixedHeight(31);
|
||||
tool_bar_->setMovable(false);
|
||||
tool_bar_->setFloatable(false);
|
||||
Toolbar *toolbar_widget = new Toolbar(tool_bar_);
|
||||
toolbar_widget->set_tool(Core::instance()->tool());
|
||||
toolbar_widget->set_snapping(Core::instance()->snapping());
|
||||
tool_bar_->addWidget(toolbar_widget);
|
||||
addToolBar(Qt::TopToolBarArea, tool_bar_);
|
||||
|
||||
connect(toolbar_widget, &Toolbar::tool_changed, Core::instance(),
|
||||
&Core::set_tool);
|
||||
connect(Core::instance(), &Core::tool_changed, toolbar_widget,
|
||||
&Toolbar::set_tool);
|
||||
connect(toolbar_widget, &Toolbar::snapping_changed, Core::instance(),
|
||||
&Core::set_snapping);
|
||||
connect(Core::instance(), &Core::snapping_changed, toolbar_widget,
|
||||
&Toolbar::set_snapping);
|
||||
connect(toolbar_widget, &Toolbar::selected_transition_changed,
|
||||
Core::instance(), &Core::set_selected_transition_object);
|
||||
setStatusBar(status_bar_);
|
||||
|
||||
// Create standard panels
|
||||
node_panel_ = new NodePanel();
|
||||
@@ -772,6 +747,15 @@ void MainWindow::timeline_focused(ViewerOutput *viewer)
|
||||
multicam_panel_->connect_viewer_node(viewer);
|
||||
param_panel_->connect_viewer_node(viewer);
|
||||
curve_panel_->connect_viewer_node(viewer);
|
||||
|
||||
// Update the global status bar info chips (resolution + frame rate)
|
||||
if (viewer) {
|
||||
VideoParams vp = viewer_output_video_params(viewer);
|
||||
status_bar_->set_sequence_info(vp.width(), vp.height(),
|
||||
vp.frame_rate().to_double());
|
||||
} else {
|
||||
status_bar_->set_sequence_info(0, 0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
QString MainWindow::get_custom_shortcuts_file()
|
||||
@@ -947,36 +931,56 @@ void MainWindow::focused_panel_changed(PanelWidget *panel)
|
||||
|
||||
void MainWindow::set_default_layout()
|
||||
{
|
||||
// Default layout per the Oak UI design reference (design/*.png):
|
||||
//
|
||||
// ┌─────────────┬───────────────────────────┬──────────────┐
|
||||
// │ 素材查看器 │ 序列查看器 | 节点编辑器 │ 检查器|历史 │
|
||||
// ├─────────────┤ │ │
|
||||
// │ 项目 │ │ │
|
||||
// ├─────────────┴───────────────────────────┴──────────────┤
|
||||
// │ 工具条(31px) + 时间线 (full width) │
|
||||
// ├────────────────────────────────────────────────────────┤
|
||||
// │ 音频监视器 (26px thin strip) │
|
||||
// └────────────────────────────────────────────────────────┘
|
||||
|
||||
KDDockWidgets::InitialOption o;
|
||||
o.preferredSize = QSize(0, centralAreaGeometry().height());
|
||||
|
||||
// Top left - Tabify footage viewer, param panel, and node panel
|
||||
addDockWidget(footage_viewer_panel_, KDDockWidgets::Location_OnTop, nullptr,
|
||||
o);
|
||||
footage_viewer_panel_->addDockWidgetAsTab(param_panel_);
|
||||
footage_viewer_panel_->addDockWidgetAsTab(node_panel_);
|
||||
param_panel_->raise();
|
||||
|
||||
// Top right - sequence viewer
|
||||
addDockWidget(sequence_viewer_panel_, KDDockWidgets::Location_OnRight,
|
||||
footage_viewer_panel_, o);
|
||||
|
||||
// Bottom center - timelines
|
||||
// Timeline at the very bottom, spanning the full window width
|
||||
addDockWidget(timeline_panels_.first(), KDDockWidgets::Location_OnBottom);
|
||||
|
||||
// Right of timeline - audio monitor
|
||||
o.preferredSize = QSize(320, 0);
|
||||
addDockWidget(audio_monitor_panel_, KDDockWidgets::Location_OnRight,
|
||||
// Audio monitor: 26px ultra-thin strip below the timeline
|
||||
o.preferredSize = QSize(0, 26);
|
||||
addDockWidget(audio_monitor_panel_, KDDockWidgets::Location_OnBottom,
|
||||
timeline_panels_.first(), o);
|
||||
|
||||
// Bottom left - project panel (tool panel is now the top toolbar;
|
||||
// the dockable ToolPanel remains available via Window menu)
|
||||
addDockWidget(project_panel_, KDDockWidgets::Location_OnLeft,
|
||||
timeline_panels_.first());
|
||||
project_panel_->addDockWidgetAsTab(history_panel_);
|
||||
// Three-column workspace above the timeline. Establish the horizontal
|
||||
// (left -> right) structure first so the column separators span the full
|
||||
// workspace height, then subdivide the left column vertically.
|
||||
o = KDDockWidgets::InitialOption();
|
||||
o.preferredSize = QSize(0, centralAreaGeometry().height());
|
||||
|
||||
// Left column - footage viewer (top-left anchor)
|
||||
addDockWidget(footage_viewer_panel_, KDDockWidgets::Location_OnTop, nullptr,
|
||||
o);
|
||||
|
||||
// Center column - sequence viewer tabified with node editor
|
||||
addDockWidget(sequence_viewer_panel_, KDDockWidgets::Location_OnRight,
|
||||
footage_viewer_panel_, o);
|
||||
sequence_viewer_panel_->addDockWidgetAsTab(node_panel_);
|
||||
sequence_viewer_panel_->raise();
|
||||
|
||||
// Right column - inspector (param panel) with history
|
||||
addDockWidget(param_panel_, KDDockWidgets::Location_OnRight,
|
||||
sequence_viewer_panel_, o);
|
||||
param_panel_->addDockWidgetAsTab(history_panel_);
|
||||
param_panel_->raise();
|
||||
|
||||
// Left column bottom - project panel (below footage viewer)
|
||||
addDockWidget(project_panel_, KDDockWidgets::Location_OnBottom,
|
||||
footage_viewer_panel_);
|
||||
project_panel_->raise();
|
||||
|
||||
// Hidden panels
|
||||
// Hidden panels (all remain accessible via the Window menu)
|
||||
tool_panel_->close();
|
||||
pixel_sampler_panel_->close();
|
||||
task_man_panel_->close();
|
||||
@@ -991,9 +995,8 @@ void MainWindow::set_default_layout()
|
||||
}
|
||||
for (auto it = timeline_panels_.cbegin(); it != timeline_panels_.cend();
|
||||
it++) {
|
||||
auto p = *it;
|
||||
if (p != timeline_panels_.first()) {
|
||||
p->addDockWidgetAsTab(p);
|
||||
if (*it != timeline_panels_.first()) {
|
||||
(*it)->close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/MainWindow.h>
|
||||
|
||||
#include <QToolBar>
|
||||
|
||||
#include "node/project/serializer/serializedlayoutinfo.h"
|
||||
#include "node/project.h"
|
||||
#include "panel/multicam/multicampanel.h"
|
||||
@@ -55,6 +53,7 @@ namespace olive
|
||||
{
|
||||
|
||||
class EngineEventBridge;
|
||||
class MainStatusBar;
|
||||
|
||||
/**
|
||||
* @brief Olive's main window responsible for docking widgets and the main menu bar.
|
||||
@@ -154,9 +153,6 @@ private:
|
||||
|
||||
QByteArray premaximized_state_;
|
||||
|
||||
// Application toolbar (31px, replaces the dockable ToolPanel by default)
|
||||
QToolBar *tool_bar_;
|
||||
|
||||
// Standard panels
|
||||
ProjectPanel *project_panel_;
|
||||
NodePanel *node_panel_;
|
||||
@@ -170,6 +166,7 @@ private:
|
||||
AudioMonitorPanel *audio_monitor_panel_;
|
||||
TaskManagerPanel *task_man_panel_;
|
||||
EngineEventBridge *event_bridge_;
|
||||
MainStatusBar *status_bar_;
|
||||
PixelSamplerPanel *pixel_sampler_panel_;
|
||||
ScopePanel *scope_panel_;
|
||||
QList<ViewerPanel *> viewer_panels_;
|
||||
|
||||
Reference in New Issue
Block a user