From dc37389d02cedfa608ef67cdcd6dc688c749ce24 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 8 Mar 2020 01:06:45 +1100 Subject: [PATCH 01/43] timeline: implemented in/out points --- app/common/rational.h | 1 + app/panel/timebased/timebased.cpp | 25 +++++ app/panel/timebased/timebased.h | 10 ++ app/project/item/footage/footage.h | 3 +- app/project/item/sequence/sequence.h | 3 +- app/timeline/CMakeLists.txt | 6 ++ app/timeline/timelinemarker.cpp | 61 +++++++++++ app/timeline/timelinemarker.h | 56 ++++++++++ app/timeline/timelinepoints.cpp | 21 ++++ app/timeline/timelinepoints.h | 25 +++++ app/timeline/timelineworkarea.cpp | 41 ++++++++ app/timeline/timelineworkarea.h | 37 +++++++ app/widget/menu/menushared.cpp | 47 +++++++-- app/widget/menu/menushared.h | 16 ++- app/widget/panel/panel.h | 10 ++ app/widget/timebased/timebased.cpp | 105 +++++++++++++++++++ app/widget/timebased/timebased.h | 31 ++++++ app/widget/timelinewidget/timelinewidget.cpp | 4 +- app/widget/timelinewidget/timelinewidget.h | 2 +- app/widget/timelinewidget/undo/undo.cpp | 42 +++++++- app/widget/timelinewidget/undo/undo.h | 39 ++++++- app/widget/timeruler/timeruler.cpp | 41 +++++++- app/widget/timeruler/timeruler.h | 8 ++ 23 files changed, 609 insertions(+), 25 deletions(-) create mode 100644 app/timeline/timelinemarker.cpp create mode 100644 app/timeline/timelinemarker.h create mode 100644 app/timeline/timelinepoints.cpp create mode 100644 app/timeline/timelinepoints.h create mode 100644 app/timeline/timelineworkarea.cpp create mode 100644 app/timeline/timelineworkarea.h diff --git a/app/common/rational.h b/app/common/rational.h index 59ed8775e..505ac313f 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -118,6 +118,7 @@ private: QDebug operator<<(QDebug debug, const rational& r); +// We define these limits at 32-bit to try avoiding integer overflow #define RATIONAL_MIN rational(INT32_MIN, 1) #define RATIONAL_MAX rational(INT32_MAX, 1) diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 2e76ae10f..d74b1cd61 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -134,3 +134,28 @@ void TimeBasedPanel::Retranslate() SetSubtitle(tr("(none)")); } } + +void TimeBasedPanel::SetIn() +{ + GetTimeBasedWidget()->SetInAtPlayhead(); +} + +void TimeBasedPanel::SetOut() +{ + GetTimeBasedWidget()->SetOutAtPlayhead(); +} + +void TimeBasedPanel::ResetIn() +{ + GetTimeBasedWidget()->ResetIn(); +} + +void TimeBasedPanel::ResetOut() +{ + GetTimeBasedWidget()->ResetOut(); +} + +void TimeBasedPanel::ClearInOut() +{ + GetTimeBasedWidget()->ClearInOutPoints(); +} diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index 59a4ec84e..555821539 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -44,6 +44,16 @@ public: virtual void ShuttleRight() override; + virtual void SetIn() override; + + virtual void SetOut() override; + + virtual void ResetIn() override; + + virtual void ResetOut() override; + + virtual void ClearInOut() override; + public slots: void SetTimebase(const rational& timebase); diff --git a/app/project/item/footage/footage.h b/app/project/item/footage/footage.h index 42226d64d..0e5f31ee9 100644 --- a/app/project/item/footage/footage.h +++ b/app/project/item/footage/footage.h @@ -30,6 +30,7 @@ #include "project/item/footage/audiostream.h" #include "project/item/footage/imagestream.h" #include "project/item/footage/videostream.h" +#include "timeline/timelinepoints.h" /** * @brief A reference to an external media file with metadata in a project structure @@ -38,7 +39,7 @@ * Footage objects store a list of Stream objects which store the majority of video/audio metadata. These streams * are identical to the stream data in the files. */ -class Footage : public Item +class Footage : public Item, public TimelinePoints { public: enum Status { diff --git a/app/project/item/sequence/sequence.h b/app/project/item/sequence/sequence.h index c9311cd40..dd91dc2f4 100644 --- a/app/project/item/sequence/sequence.h +++ b/app/project/item/sequence/sequence.h @@ -27,6 +27,7 @@ #include "render/videoparams.h" #include "project/item/footage/stream.h" #include "project/item/item.h" +#include "timeline/timelinepoints.h" class Sequence; using SequencePtr = std::shared_ptr; @@ -34,7 +35,7 @@ using SequencePtr = std::shared_ptr; /** * @brief The main timeline object, an graph of edited clips that forms a complete edit */ -class Sequence : public Item, public NodeGraph +class Sequence : public Item, public NodeGraph, public TimelinePoints { public: Sequence(); diff --git a/app/timeline/CMakeLists.txt b/app/timeline/CMakeLists.txt index c0bd69c9f..000775dcc 100644 --- a/app/timeline/CMakeLists.txt +++ b/app/timeline/CMakeLists.txt @@ -18,6 +18,12 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} timeline/timelinecoordinate.h timeline/timelinecoordinate.cpp + timeline/timelinemarker.h + timeline/timelinemarker.cpp + timeline/timelinepoints.h + timeline/timelinepoints.cpp + timeline/timelineworkarea.h + timeline/timelineworkarea.cpp timeline/trackreference.h timeline/trackreference.cpp PARENT_SCOPE diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp new file mode 100644 index 000000000..83a136c7b --- /dev/null +++ b/app/timeline/timelinemarker.cpp @@ -0,0 +1,61 @@ +#include "timelinemarker.h" + +TimelineMarker::TimelineMarker(const TimeRange &time, const QString &name, QObject *parent) : + QObject(parent), + time_(time), + name_(name) +{ +} + +const TimeRange &TimelineMarker::time() const +{ + return time_; +} + +void TimelineMarker::set_time(const TimeRange &time) +{ + time_ = time; + emit TimeChanged(time_); +} + +const QString &TimelineMarker::name() const +{ + return name_; +} + +void TimelineMarker::set_name(const QString &name) +{ + name_ = name; + emit NameChanged(name_); +} + +TimelineMarkerList::~TimelineMarkerList() +{ + qDeleteAll(markers_); +} + +void TimelineMarkerList::AddMarker(const TimeRange &time, const QString &name) +{ + TimelineMarker* m = new TimelineMarker(time, name); + markers_.append(m); + emit MarkerAdded(m); +} + +void TimelineMarkerList::RemoveMarker(TimelineMarker *marker) +{ + for (int i=0;i &TimelineMarkerList::list() const +{ + return markers_; +} diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h new file mode 100644 index 000000000..998f27585 --- /dev/null +++ b/app/timeline/timelinemarker.h @@ -0,0 +1,56 @@ +#ifndef TIMELINEMARKER_H +#define TIMELINEMARKER_H + +#include + +#include "common/timerange.h" + +class TimelineMarker : public QObject +{ + Q_OBJECT +public: + TimelineMarker(const TimeRange& time = TimeRange(), const QString& name = QString(), QObject* parent = nullptr); + + const TimeRange &time() const; + void set_time(const TimeRange& time); + + const QString& name() const; + void set_name(const QString& name); + +signals: + void TimeChanged(const TimeRange& time); + + void NameChanged(const QString& name); + +private: + TimeRange time_; + + QString name_; + +}; + +class TimelineMarkerList : public QObject +{ + Q_OBJECT +public: + TimelineMarkerList() = default; + + virtual ~TimelineMarkerList() override; + + void AddMarker(const TimeRange& time = TimeRange(), const QString& name = QString()); + + void RemoveMarker(TimelineMarker* marker); + + const QList &list() const; + +signals: + void MarkerAdded(TimelineMarker* marker); + + void MarkerRemoved(TimelineMarker* marker); + +private: + QList markers_; + +}; + +#endif // TIMELINEMARKER_H diff --git a/app/timeline/timelinepoints.cpp b/app/timeline/timelinepoints.cpp new file mode 100644 index 000000000..b3aae146c --- /dev/null +++ b/app/timeline/timelinepoints.cpp @@ -0,0 +1,21 @@ +#include "timelinepoints.h" + +TimelineMarkerList *TimelinePoints::markers() +{ + return &markers_; +} + +const TimelineMarkerList *TimelinePoints::markers() const +{ + return &markers_; +} + +const TimelineWorkArea *TimelinePoints::workarea() const +{ + return &workarea_; +} + +TimelineWorkArea *TimelinePoints::workarea() +{ + return &workarea_; +} diff --git a/app/timeline/timelinepoints.h b/app/timeline/timelinepoints.h new file mode 100644 index 000000000..ed2570f1c --- /dev/null +++ b/app/timeline/timelinepoints.h @@ -0,0 +1,25 @@ +#ifndef TIMELINEPOINTS_H +#define TIMELINEPOINTS_H + +#include "timelinemarker.h" +#include "timelineworkarea.h" + +class TimelinePoints +{ +public: + TimelinePoints() = default; + + TimelineMarkerList* markers(); + const TimelineMarkerList* markers() const; + + TimelineWorkArea* workarea(); + const TimelineWorkArea* workarea() const; + +private: + TimelineMarkerList markers_; + + TimelineWorkArea workarea_; + +}; + +#endif // TIMELINEPOINTS_H diff --git a/app/timeline/timelineworkarea.cpp b/app/timeline/timelineworkarea.cpp new file mode 100644 index 000000000..57b29364a --- /dev/null +++ b/app/timeline/timelineworkarea.cpp @@ -0,0 +1,41 @@ +#include "timelineworkarea.h" + +const rational TimelineWorkArea::kResetIn = 0; +const rational TimelineWorkArea::kResetOut = RATIONAL_MAX; + +TimelineWorkArea::TimelineWorkArea(QObject *parent) : + QObject(parent) +{ +} + +bool TimelineWorkArea::enabled() const +{ + return workarea_enabled_; +} + +void TimelineWorkArea::set_enabled(bool e) +{ + workarea_enabled_ = e; + emit EnabledChanged(workarea_enabled_); +} + +const TimeRange &TimelineWorkArea::range() const +{ + return workarea_range_; +} + +void TimelineWorkArea::set_range(const TimeRange &range) +{ + workarea_range_ = range; + emit RangeChanged(workarea_range_); +} + +const rational &TimelineWorkArea::in() const +{ + return workarea_range_.in(); +} + +const rational &TimelineWorkArea::out() const +{ + return workarea_range_.out(); +} diff --git a/app/timeline/timelineworkarea.h b/app/timeline/timelineworkarea.h new file mode 100644 index 000000000..bad23bc82 --- /dev/null +++ b/app/timeline/timelineworkarea.h @@ -0,0 +1,37 @@ +#ifndef TIMELINEWORKAREA_H +#define TIMELINEWORKAREA_H + +#include + +#include "common/timerange.h" + +class TimelineWorkArea : public QObject +{ + Q_OBJECT +public: + TimelineWorkArea(QObject* parent = nullptr); + + bool enabled() const; + void set_enabled(bool e); + + const rational& in() const; + const rational& out() const; + const TimeRange& range() const; + void set_range(const TimeRange& range); + + static const rational kResetIn; + static const rational kResetOut; + +signals: + void EnabledChanged(bool e); + + void RangeChanged(const TimeRange& r); + +private: + bool workarea_enabled_; + + TimeRange workarea_range_; + +}; + +#endif // TIMELINEWORKAREA_H diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 6cd1df5f4..84419b8cc 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -39,16 +39,16 @@ MenuShared::MenuShared() edit_paste_item_ = Menu::CreateItem(this, "paste", nullptr, nullptr, "Ctrl+V"); edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", nullptr, nullptr, "Ctrl+Shift+V"); edit_duplicate_item_ = Menu::CreateItem(this, "duplicate", nullptr, nullptr, "Ctrl+D"); - edit_delete_item_ = Menu::CreateItem(this, "delete", this, SLOT(DeleteSelected()), "Del"); - edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, SLOT(RippleDelete()), "Shift+Del"); - edit_split_item_ = Menu::CreateItem(this, "split", this, SLOT(SplitAtPlayhead()), "Ctrl+K"); + edit_delete_item_ = Menu::CreateItem(this, "delete", this, SLOT(DeleteSelectedTriggered()), "Del"); + edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, SLOT(RippleDeleteTriggered()), "Shift+Del"); + edit_split_item_ = Menu::CreateItem(this, "split", this, SLOT(SplitAtPlayheadTriggered()), "Ctrl+K"); // "In/Out" menu shared items - inout_set_in_item_ = Menu::CreateItem(this, "setinpoint", nullptr, nullptr, "I"); - inout_set_out_item_ = Menu::CreateItem(this, "setoutpoint", nullptr, nullptr, "O"); - inout_reset_in_item_ = Menu::CreateItem(this, "resetin", nullptr, nullptr); - inout_reset_out_item_ = Menu::CreateItem(this, "resetout", nullptr, nullptr); - inout_clear_inout_item_ = Menu::CreateItem(this, "clearinout", nullptr, nullptr, "G"); + inout_set_in_item_ = Menu::CreateItem(this, "setinpoint", this, SLOT(SetInTriggered()), "I"); + inout_set_out_item_ = Menu::CreateItem(this, "setoutpoint", this, SLOT(SetOutTriggered()), "O"); + inout_reset_in_item_ = Menu::CreateItem(this, "resetin", this, SLOT(ResetInTriggered())); + inout_reset_out_item_ = Menu::CreateItem(this, "resetout", this, SLOT(ResetOutTriggered())); + inout_clear_inout_item_ = Menu::CreateItem(this, "clearinout", this, SLOT(ClearInOutTriggered()), "G"); // "Clip Edit" menu shared items clip_add_default_transition_item_ = Menu::CreateItem(this, "deftransition", nullptr, nullptr, "Ctrl+Shift+D"); @@ -112,7 +112,7 @@ MenuShared *MenuShared::instance() return instance_; } -void MenuShared::SplitAtPlayhead() +void MenuShared::SplitAtPlayheadTriggered() { TimelinePanel* timeline = PanelManager::instance()->MostRecentlyFocused(); @@ -121,16 +121,41 @@ void MenuShared::SplitAtPlayhead() } } -void MenuShared::DeleteSelected() +void MenuShared::DeleteSelectedTriggered() { PanelManager::instance()->CurrentlyFocused()->DeleteSelected(); } -void MenuShared::RippleDelete() +void MenuShared::RippleDeleteTriggered() { PanelManager::instance()->CurrentlyFocused()->RippleDelete(); } +void MenuShared::SetInTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetIn(); +} + +void MenuShared::SetOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetOut(); +} + +void MenuShared::ResetInTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ResetIn(); +} + +void MenuShared::ResetOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ResetOut(); +} + +void MenuShared::ClearInOutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ClearInOut(); +} + void MenuShared::Retranslate() { // "New" menu shared items diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 2cee72066..6269aa2bb 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -75,11 +75,21 @@ private: static MenuShared* instance_; private slots: - void SplitAtPlayhead(); + void SplitAtPlayheadTriggered(); - void DeleteSelected(); + void DeleteSelectedTriggered(); - void RippleDelete(); + void RippleDeleteTriggered(); + + void SetInTriggered(); + + void SetOutTriggered(); + + void ResetInTriggered(); + + void ResetOutTriggered(); + + void ClearInOutTriggered(); }; diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index e324c149f..d81df1be8 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -118,6 +118,16 @@ public: virtual void DecreaseTrackHeight(){} + virtual void SetIn(){} + + virtual void SetOut(){} + + virtual void ResetIn(){} + + virtual void ResetOut(){} + + virtual void ClearInOut(){} + protected: /** * @brief paintEvent diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index b7feb9bb6..f75d657ec 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -1,6 +1,11 @@ #include "timebased.h" +#include + #include "common/timecodefunctions.h" +#include "core.h" +#include "project/item/sequence/sequence.h" +#include "widget/timelinewidget/undo/undo.h" TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_status_visible, QWidget *parent) : QWidget(parent), @@ -44,6 +49,8 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) DisconnectNodeInternal(viewer_node_); disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + + ruler()->ConnectTimelinePoints(nullptr); } viewer_node_ = node; @@ -54,6 +61,8 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) ConnectNodeInternal(viewer_node_); connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + + ruler()->ConnectTimelinePoints(static_cast(viewer_node_->parent())); } } @@ -242,3 +251,99 @@ void TimeBasedWidget::CenterScrollOnPlayhead() { scrollbar_->setValue(qRound(TimeToScene(Timecode::timestamp_to_time(ruler_->GetTime(), timebase()))) - scrollbar_->width()/2); } + +void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) +{ + if (!GetConnectedNode()) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + Sequence* s = static_cast(GetConnectedNode()->parent()); + + // Enable workarea if it isn't already enabled + if (!s->workarea()->enabled()) { + new WorkareaSetEnabledCommand(s, true, command); + } + + // Determine our new range + rational in_point, out_point; + + if (m == Timeline::kTrimIn) { + in_point = time; + + if (!s->workarea()->enabled() || s->workarea()->out() < in_point) { + out_point = TimelineWorkArea::kResetOut; + } else { + out_point = s->workarea()->out(); + } + } else { + out_point = time; + + if (!s->workarea()->enabled() || s->workarea()->in() > out_point) { + in_point = TimelineWorkArea::kResetIn; + } else { + in_point = s->workarea()->in(); + } + } + + // Set workarea + new WorkareaSetRangeCommand(s, TimeRange(in_point, out_point), command); + + Core::instance()->undo_stack()->push(command); +} + +void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) +{ + if (!GetConnectedNode()) { + return; + } + + Sequence* s = static_cast(GetConnectedNode()->parent()); + + if (!s->workarea()->enabled()) { + return; + } + + TimeRange r = s->workarea()->range(); + + if (m == Timeline::kTrimIn) { + r.set_in(TimelineWorkArea::kResetIn); + } else { + r.set_out(TimelineWorkArea::kResetOut); + } + + Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(s, r)); +} + +void TimeBasedWidget::SetInAtPlayhead() +{ + SetPoint(Timeline::kTrimIn, GetTime()); +} + +void TimeBasedWidget::SetOutAtPlayhead() +{ + SetPoint(Timeline::kTrimOut, GetTime()); +} + +void TimeBasedWidget::ResetIn() +{ + ResetPoint(Timeline::kTrimIn); +} + +void TimeBasedWidget::ResetOut() +{ + ResetPoint(Timeline::kTrimOut); +} + +void TimeBasedWidget::ClearInOutPoints() +{ + if (!GetConnectedNode()) { + return; + } + + Sequence* s = static_cast(GetConnectedNode()->parent()); + + Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(s, false)); +} diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 50b0f4882..7b341a4b3 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -3,6 +3,7 @@ #include +#include "common/timelinecommon.h" #include "node/output/viewer/viewer.h" #include "widget/resizablescrollbar/resizablescrollbar.h" #include "widget/timelinewidget/timelinescaledobject.h" @@ -48,6 +49,16 @@ public slots: void GoToNextCut(); + void SetInAtPlayhead(); + + void SetOutAtPlayhead(); + + void ResetIn(); + + void ResetOut(); + + void ClearInOutPoints(); + TimeRuler* ruler() const; protected slots: @@ -84,6 +95,26 @@ signals: void TimebaseChanged(const rational&); private: + /** + * @brief Set either in or out point to the current playhead + * + * @param m + * + * Set to kTrimIn or kTrimOut for setting the in point or out point respectively. + */ + void SetPoint(Timeline::MovementMode m, const rational &time); + + /** + * @brief Reset either the in or out point + * + * Sets either the in point to 0 or the out point to `RATIONAL_MAX`. + * + * @param m + * + * Set to kTrimIn or kTrimOut for setting the in point or out point respectively. + */ + void ResetPoint(Timeline::MovementMode m); + ViewerOutput* viewer_node_; TimeRuler* ruler_; diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 6173d9df2..2385b9289 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -335,7 +335,7 @@ void TimelineWidget::SplitAtPlayhead() } } -void TimelineWidget::DeleteSelectedInternal(QList blocks, +void TimelineWidget::DeleteSelectedInternal(const QList &blocks, bool transition_aware, bool remove_from_graph, QUndoCommand *command) @@ -426,7 +426,7 @@ void TimelineWidget::DeleteSelected(bool ripple) range_list.InsertTimeRange(TimeRange(b->in(), b->out())); } - new TimelineRippleDeleteGapsAtRegions(GetConnectedNode(), range_list, command); + new TimelineRippleDeleteGapsAtRegionsCommand(GetConnectedNode(), range_list, command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 2dd6bd758..ca51db637 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -349,7 +349,7 @@ private: bool dual_transition_; }; - void DeleteSelectedInternal(QList blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); + void DeleteSelectedInternal(const QList& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); void SetBlockLinksSelected(Block *block, bool selected); diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 09a9d2db8..2a7dfae1b 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -623,14 +623,14 @@ void BlockSetSpeedCommand::undo_internal() block_->set_speed(old_speed_); } -TimelineRippleDeleteGapsAtRegions::TimelineRippleDeleteGapsAtRegions(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) : +TimelineRippleDeleteGapsAtRegionsCommand::TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput *vo, const TimeRangeList ®ions, QUndoCommand *parent) : UndoCommand(parent), timeline_(vo), regions_(regions) { } -void TimelineRippleDeleteGapsAtRegions::redo_internal() +void TimelineRippleDeleteGapsAtRegionsCommand::redo_internal() { foreach (const TimeRange& range, regions_) { rational max_ripple_length = range.length(); @@ -671,7 +671,7 @@ void TimelineRippleDeleteGapsAtRegions::redo_internal() } } -void TimelineRippleDeleteGapsAtRegions::undo_internal() +void TimelineRippleDeleteGapsAtRegionsCommand::undo_internal() { for (int i=commands_.size()-1;i>=0;i--) { commands_.at(i)->undo(); @@ -679,3 +679,39 @@ void TimelineRippleDeleteGapsAtRegions::undo_internal() } commands_.empty(); } + +WorkareaSetEnabledCommand::WorkareaSetEnabledCommand(TimelinePoints *points, bool enabled, QUndoCommand *parent) : + UndoCommand(parent), + points_(points), + old_enabled_(points_->workarea()->enabled()), + new_enabled_(enabled) +{ +} + +void WorkareaSetEnabledCommand::redo_internal() +{ + points_->workarea()->set_enabled(new_enabled_); +} + +void WorkareaSetEnabledCommand::undo_internal() +{ + points_->workarea()->set_enabled(old_enabled_); +} + +WorkareaSetRangeCommand::WorkareaSetRangeCommand(TimelinePoints *points, const TimeRange &range, QUndoCommand *parent) : + UndoCommand(parent), + points_(points), + old_range_(points_->workarea()->range()), + new_range_(range) +{ +} + +void WorkareaSetRangeCommand::redo_internal() +{ + points_->workarea()->set_range(new_range_); +} + +void WorkareaSetRangeCommand::undo_internal() +{ + points_->workarea()->set_range(old_range_); +} diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index 8e4d81b9c..d8afe6d60 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -27,6 +27,7 @@ #include "node/block/gap/gap.h" #include "node/output/track/track.h" #include "node/output/track/tracklist.h" +#include "timeline/timelinepoints.h" #include "undo/undocommand.h" class BlockResizeCommand : public UndoCommand { @@ -280,9 +281,9 @@ private: }; -class TimelineRippleDeleteGapsAtRegions : public UndoCommand { +class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { public: - TimelineRippleDeleteGapsAtRegions(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr); + TimelineRippleDeleteGapsAtRegionsCommand(ViewerOutput* vo, const TimeRangeList& regions, QUndoCommand* parent = nullptr); protected: virtual void redo_internal() override; @@ -296,4 +297,38 @@ private: }; +class WorkareaSetEnabledCommand : public UndoCommand { +public: + WorkareaSetEnabledCommand(TimelinePoints* points, bool enabled, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TimelinePoints* points_; + + bool old_enabled_; + + bool new_enabled_; + +}; + +class WorkareaSetRangeCommand : public UndoCommand { +public: + WorkareaSetRangeCommand(TimelinePoints* points, const TimeRange& range, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TimelinePoints* points_; + + TimeRange old_range_; + + TimeRange new_range_; + +}; + #endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index a5ef88a9a..15a90b6ab 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -37,7 +37,8 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare centered_text_(true), scale_(1.0), time_(0), - show_cache_status_(cache_status_visible) + show_cache_status_(cache_status_visible), + timeline_points_(nullptr) { QFontMetrics fm = fontMetrics(); @@ -81,6 +82,23 @@ void TimeRuler::SetTimebase(const rational &r) update(); } +void TimeRuler::ConnectTimelinePoints(TimelinePoints *points) +{ + if (timeline_points_) { + disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, &TimeRuler::TimelineWorkareaChanged); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, &TimeRuler::TimelineWorkareaChanged); + } + + timeline_points_ = points; + + if (timeline_points_) { + connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, &TimeRuler::TimelineWorkareaChanged); + connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, &TimeRuler::TimelineWorkareaChanged); + } + + update(); +} + const int64_t &TimeRuler::GetTime() { return time_; @@ -138,6 +156,22 @@ void TimeRuler::paintEvent(QPaintEvent *) QPainter p(this); + // Draw timeline points if connected + if (timeline_points_) { + if (timeline_points_->workarea()->enabled()) { + int workarea_left = qMax(0, TimeToScreen(timeline_points_->workarea()->in())); + int workarea_right; + + if (timeline_points_->workarea()->out() == TimelineWorkArea::kResetOut) { + workarea_right = width(); + } else { + workarea_right = qMin(width(), TimeToScreen(timeline_points_->workarea()->out())); + } + + p.fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); + } + } + double width_of_frame = timebase_dbl_ * scale_; double width_of_second = 0; do { @@ -381,6 +415,11 @@ void TimeRuler::SeekToScreenPoint(int screen) emit TimeChanged(timestamp); } +void TimeRuler::TimelineWorkareaChanged() +{ + update(); +} + void TimeRuler::UpdateHeight() { int height = text_height_; diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index a43b81edd..bb1c5f290 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -26,6 +26,7 @@ #include "common/rational.h" #include "common/timerange.h" +#include "timeline/timelinepoints.h" #include "widget/timelinewidget/view/timelineplayhead.h" class TimeRuler : public QWidget @@ -41,6 +42,8 @@ public: void SetCenteredText(bool c); + void ConnectTimelinePoints(TimelinePoints* points); + const int64_t& GetTime(); public slots: @@ -115,6 +118,11 @@ private: TimeRangeList dirty_cache_ranges_; + TimelinePoints* timeline_points_; + +private slots: + void TimelineWorkareaChanged(); + }; #endif // TIMERULER_H From 8cf9327936c308c7b6a25b595497c23148c384b0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 16:57:51 +1100 Subject: [PATCH 02/43] timelineviewblockitem: fixed issue that caused waveforms to always appear at the start of the timeline --- .../timelinewidget/view/timelineviewblockitem.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index 399f82186..7b012a583 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -124,12 +124,14 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI summary_index = sample_index; } - for (int j=0;jdrawLine(i, + for (int j=0;jdrawLine(line_x, channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), - i, + line_x, channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); } } From 39215a7dd0a950e0a06eefc3f4e383f6e234fb66 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 18:28:57 +1100 Subject: [PATCH 03/43] footageviewer: direct in/out/markers to footage item rather than assuming a sequence item --- app/widget/timebased/timebased.cpp | 48 ++++++++++++++--------------- app/widget/timebased/timebased.h | 4 +++ app/widget/viewer/footageviewer.cpp | 5 +++ app/widget/viewer/footageviewer.h | 3 ++ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index f75d657ec..d5b8ce664 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -10,7 +10,8 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_status_visible, QWidget *parent) : QWidget(parent), viewer_node_(nullptr), - auto_max_scrollbar_(false) + auto_max_scrollbar_(false), + points_(nullptr) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); @@ -50,6 +51,7 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + points_ = nullptr; ruler()->ConnectTimelinePoints(nullptr); } @@ -62,7 +64,9 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); - ruler()->ConnectTimelinePoints(static_cast(viewer_node_->parent())); + if ((points_ = ConnectTimelinePoints())) { + ruler()->ConnectTimelinePoints(points_); + } } } @@ -123,6 +127,11 @@ void TimeBasedWidget::resizeEvent(QResizeEvent *event) UpdateMaximumScroll(); } +TimelinePoints *TimeBasedWidget::ConnectTimelinePoints() +{ + return static_cast(viewer_node_->parent()); +} + void TimeBasedWidget::SetTime(int64_t timestamp) { ruler_->SetTime(timestamp); @@ -254,17 +263,15 @@ void TimeBasedWidget::CenterScrollOnPlayhead() void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) { - if (!GetConnectedNode()) { + if (!points_) { return; } QUndoCommand* command = new QUndoCommand(); - Sequence* s = static_cast(GetConnectedNode()->parent()); - // Enable workarea if it isn't already enabled - if (!s->workarea()->enabled()) { - new WorkareaSetEnabledCommand(s, true, command); + if (!points_->workarea()->enabled()) { + new WorkareaSetEnabledCommand(points_, true, command); } // Determine our new range @@ -273,40 +280,34 @@ void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) if (m == Timeline::kTrimIn) { in_point = time; - if (!s->workarea()->enabled() || s->workarea()->out() < in_point) { + if (!points_->workarea()->enabled() || points_->workarea()->out() < in_point) { out_point = TimelineWorkArea::kResetOut; } else { - out_point = s->workarea()->out(); + out_point = points_->workarea()->out(); } } else { out_point = time; - if (!s->workarea()->enabled() || s->workarea()->in() > out_point) { + if (!points_->workarea()->enabled() || points_->workarea()->in() > out_point) { in_point = TimelineWorkArea::kResetIn; } else { - in_point = s->workarea()->in(); + in_point = points_->workarea()->in(); } } // Set workarea - new WorkareaSetRangeCommand(s, TimeRange(in_point, out_point), command); + new WorkareaSetRangeCommand(points_, TimeRange(in_point, out_point), command); Core::instance()->undo_stack()->push(command); } void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) { - if (!GetConnectedNode()) { + if (!points_ || !points_->workarea()->enabled()) { return; } - Sequence* s = static_cast(GetConnectedNode()->parent()); - - if (!s->workarea()->enabled()) { - return; - } - - TimeRange r = s->workarea()->range(); + TimeRange r = points_->workarea()->range(); if (m == Timeline::kTrimIn) { r.set_in(TimelineWorkArea::kResetIn); @@ -314,7 +315,7 @@ void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) r.set_out(TimelineWorkArea::kResetOut); } - Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(s, r)); + Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points_, r)); } void TimeBasedWidget::SetInAtPlayhead() @@ -339,11 +340,10 @@ void TimeBasedWidget::ResetOut() void TimeBasedWidget::ClearInOutPoints() { - if (!GetConnectedNode()) { + if (!points_) { return; } - Sequence* s = static_cast(GetConnectedNode()->parent()); - Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(s, false)); + Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(points_, false)); } diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 7b341a4b3..4f24556e2 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -83,6 +83,8 @@ protected: virtual void resizeEvent(QResizeEvent *event) override; + virtual TimelinePoints* ConnectTimelinePoints(); + protected slots: /** * @brief Slot to center the horizontal scroll bar on the playhead's current position @@ -123,6 +125,8 @@ private: bool auto_max_scrollbar_; + TimelinePoints* points_; + private slots: void UpdateMaximumScroll(); diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 5a4c4f860..9b0c18db4 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -68,6 +68,11 @@ void FootageViewerWidget::SetFootage(Footage *footage) } } +TimelinePoints *FootageViewerWidget::ConnectTimelinePoints() +{ + return footage_ ? footage_ : nullptr; +} + void FootageViewerWidget::StartFootageDrag() { if (!GetFootage()) { diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index 06bbd46e5..dc1634291 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -15,6 +15,9 @@ public: Footage* GetFootage() const; void SetFootage(Footage* footage); +protected: + virtual TimelinePoints* ConnectTimelinePoints() override; + private: Footage* footage_; From 965242cb290baf29bab4a57e23be7eb1118ae2dd Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 19:55:35 +1100 Subject: [PATCH 04/43] oiiodecoder: set flag on image sequences to identify them as such --- app/codec/oiio/oiiodecoder.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 310e29d03..6f62bc6c9 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -84,6 +84,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) rational default_timebase = Config::Current()["DefaultSequenceFrameRate"].value(); video_stream->set_timebase(default_timebase); video_stream->set_frame_rate(default_timebase.flipped()); + video_stream->set_image_sequence(true); // FIXME: Get actual start number video_stream->set_start_time(1); From 0131d83256c203c83be291b8f27c29b5fff323ba Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 19:56:10 +1100 Subject: [PATCH 05/43] footageproperties: allow for sanity checks before accepting/making changes --- .../footageproperties/footageproperties.cpp | 15 +++++++++++++-- .../streamproperties/streamproperties.h | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index fa5063540..ea543b811 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -91,11 +91,22 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota buttons->setCenterButtons(true); layout->addWidget(buttons, row, 0, 1, 2); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); } void FootagePropertiesDialog::accept() { + // Perform sanity check on all pages + for (int i=0;icount();i++) { + if (!static_cast(stacked_widget_->widget(i))->SanityCheck()) { + // Switch to the failed panel in question + stacked_widget_->setCurrentIndex(i); + + // Do nothing (it's up to the property panel itself to throw the error message) + return; + } + } + QUndoCommand* command = new QUndoCommand(); if (footage_->name() != footage_name_field_->text()) { diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.h b/app/dialog/footageproperties/streamproperties/streamproperties.h index 72840f010..30f8d7aa5 100644 --- a/app/dialog/footageproperties/streamproperties/streamproperties.h +++ b/app/dialog/footageproperties/streamproperties/streamproperties.h @@ -30,6 +30,9 @@ public: StreamProperties(QWidget* parent = nullptr); virtual void Accept(QUndoCommand*){} + + virtual bool SanityCheck(){return true;} + }; #endif // STREAMPROPERTIES_H From 16b072fa985bf6567cfe727374ef700ec9802cb3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 19:56:58 +1100 Subject: [PATCH 06/43] various: allow users to override the start and end index of image sequences Also includes various fixes to footage property setting/signalling. --- .../videostreamproperties.cpp | 110 +++++++++++++++++- .../streamproperties/videostreamproperties.h | 38 ++++++ app/node/input/media/media.cpp | 14 +-- app/node/input/media/media.h | 2 +- app/project/item/footage/imagestream.cpp | 12 +- app/project/item/footage/imagestream.h | 5 +- app/project/item/footage/stream.cpp | 2 + app/project/item/footage/stream.h | 2 + app/project/item/footage/videostream.cpp | 14 ++- app/project/item/footage/videostream.h | 6 + app/render/backend/videorenderworker.cpp | 2 + 11 files changed, 181 insertions(+), 26 deletions(-) diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 333879e67..bb816f296 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -21,7 +21,9 @@ #include "videostreamproperties.h" #include +#include #include +#include #include namespace OCIO = OCIO_NAMESPACE::v1; @@ -35,12 +37,16 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : QGridLayout* video_layout = new QGridLayout(this); video_layout->setMargin(0); - video_layout->addWidget(new QLabel(tr("Color Space:")), 0, 0); + int row = 0; + + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); OCIO::ConstConfigRcPtr config = stream->footage()->project()->color_manager()->GetConfig(); int number_of_colorspaces = config->getNumColorSpaces(); + video_color_space_->addItem(tr("Default (%1)").arg(stream->footage()->project()->default_input_colorspace())); + for (int i=0;igetColorSpaceNameByIndex(i); @@ -49,22 +55,91 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : video_color_space_->setCurrentText(stream_->colorspace()); - video_layout->addWidget(video_color_space_, 0, 1); + video_layout->addWidget(video_color_space_, row, 1); + + row++; video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); video_premultiply_alpha_->setChecked(stream_->premultiplied_alpha()); - video_layout->addWidget(video_premultiply_alpha_, 1, 0, 1, 2); + video_layout->addWidget(video_premultiply_alpha_, row, 0, 1, 2); + + row++; + + if (IsImageSequence(stream.get())) { + QGroupBox* imgseq_group = new QGroupBox(tr("Image Sequence")); + QGridLayout* imgseq_layout = new QGridLayout(imgseq_group); + + int imgseq_row = 0; + + VideoStream* video_stream = static_cast(stream.get()); + + imgseq_layout->addWidget(new QLabel(tr("Start Index:")), imgseq_row, 0); + + imgseq_start_time_ = new IntegerSlider(); + imgseq_start_time_->SetMinimum(0); + imgseq_start_time_->SetValue(video_stream->start_time()); + imgseq_layout->addWidget(imgseq_start_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("End Index:")), imgseq_row, 0); + + imgseq_end_time_ = new IntegerSlider(); + imgseq_end_time_->SetMinimum(0); + imgseq_end_time_->SetValue(video_stream->start_time() + video_stream->duration()); + imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); + + video_layout->addWidget(imgseq_group, row, 0, 1, 2); + } } void VideoStreamProperties::Accept(QUndoCommand *parent) { + QString set_colorspace; + + if (video_color_space_->currentIndex() > 0) { + set_colorspace = video_color_space_->currentText(); + } + if (video_premultiply_alpha_->isChecked() != stream_->premultiplied_alpha() - || video_color_space_->currentText() != stream_->colorspace()) { + || set_colorspace != stream_->colorspace(false)) { + new VideoStreamChangeCommand(stream_, video_premultiply_alpha_->isChecked(), - video_color_space_->currentText(), + set_colorspace, parent); } + + if (IsImageSequence(stream_.get())) { + VideoStreamPtr video_stream = std::static_pointer_cast(stream_); + + if (video_stream->start_time() != imgseq_start_time_->GetValue()) { + new ImageSequenceChangeCommand(video_stream, + imgseq_start_time_->GetValue(), + imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue(), + parent); + } + } +} + +bool VideoStreamProperties::SanityCheck() +{ + if (IsImageSequence(stream_.get())) { + if (imgseq_start_time_->GetValue() >= imgseq_end_time_->GetValue()) { + QMessageBox::critical(this, + tr("Invalid Configuration"), + tr("Image sequence end index must be a value higher than the start index."), + QMessageBox::Ok); + return false; + } + } + + return true; +} + +bool VideoStreamProperties::IsImageSequence(ImageStream *stream) +{ + return (stream->type() == Stream::kVideo && static_cast(stream)->is_image_sequence()); } VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream, @@ -81,7 +156,7 @@ VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageS void VideoStreamProperties::VideoStreamChangeCommand::redo_internal() { old_premultiplied_ = stream_->premultiplied_alpha(); - old_colorspace_ = stream_->colorspace(); + old_colorspace_ = stream_->colorspace(false); stream_->set_premultiplied_alpha(new_premultiplied_); stream_->set_colorspace(new_colorspace_); @@ -92,3 +167,26 @@ void VideoStreamProperties::VideoStreamChangeCommand::undo_internal() stream_->set_premultiplied_alpha(old_premultiplied_); stream_->set_colorspace(old_colorspace_); } + +VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, QUndoCommand *parent) : + UndoCommand(parent), + video_stream_(video_stream), + new_start_index_(start_index), + new_duration_(duration) +{ +} + +void VideoStreamProperties::ImageSequenceChangeCommand::redo_internal() +{ + old_start_index_ = video_stream_->start_time(); + video_stream_->set_start_time(new_start_index_); + + old_duration_ = video_stream_->duration(); + video_stream_->set_duration(new_duration_); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::undo_internal() +{ + video_stream_->set_start_time(old_start_index_); + video_stream_->set_duration(old_duration_); +} diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index e59bfb84f..183ab7e32 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -27,6 +27,7 @@ #include "project/item/footage/videostream.h" #include "streamproperties.h" #include "undo/undocommand.h" +#include "widget/slider/integerslider.h" class VideoStreamProperties : public StreamProperties { @@ -35,7 +36,11 @@ public: virtual void Accept(QUndoCommand* parent) override; + virtual bool SanityCheck() override; + private: + static bool IsImageSequence(ImageStream* stream); + /** * @brief Attached video stream */ @@ -51,6 +56,16 @@ private: */ QComboBox* video_color_space_; + /** + * @brief Sets the start index for image sequences + */ + IntegerSlider* imgseq_start_time_; + + /** + * @brief Sets the end index for image sequences + */ + IntegerSlider* imgseq_end_time_; + class VideoStreamChangeCommand : public UndoCommand { public: VideoStreamChangeCommand(ImageStreamPtr stream, @@ -70,6 +85,29 @@ private: bool old_premultiplied_; QString old_colorspace_; + + }; + + class ImageSequenceChangeCommand : public UndoCommand { + public: + ImageSequenceChangeCommand(VideoStreamPtr video_stream, + int64_t start_index, + int64_t duration, + QUndoCommand* parent = nullptr); + + protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + + private: + VideoStreamPtr video_stream_; + + int64_t new_start_index_; + int64_t old_start_index_; + + int64_t new_duration_; + int64_t old_duration_; + }; }; diff --git a/app/node/input/media/media.cpp b/app/node/input/media/media.cpp index 29189a479..ea27b8c35 100644 --- a/app/node/input/media/media.cpp +++ b/app/node/input/media/media.cpp @@ -69,22 +69,18 @@ void MediaInput::FootageChanged() return; } - if (connected_footage_ != nullptr) { - if (connected_footage_->type() == Stream::kImage || connected_footage_->type() == Stream::kVideo) { - disconnect(connected_footage_.get(), SIGNAL(ColorSpaceChanged()), this, SLOT(FootageColorSpaceChanged())); - } + if (connected_footage_) { + disconnect(connected_footage_.get(), &Stream::ParametersChanged, this, &MediaInput::FootageParametersChanged); } connected_footage_ = new_footage; - if (connected_footage_ != nullptr) { - if (connected_footage_->type() == Stream::kImage || connected_footage_->type() == Stream::kVideo) { - connect(connected_footage_.get(), SIGNAL(ColorSpaceChanged()), this, SLOT(FootageColorSpaceChanged())); - } + if (connected_footage_) { + connect(connected_footage_.get(), &Stream::ParametersChanged, this, &MediaInput::FootageParametersChanged); } } -void MediaInput::FootageColorSpaceChanged() +void MediaInput::FootageParametersChanged() { InvalidateCache(0, RATIONAL_MAX, footage_input_); } diff --git a/app/node/input/media/media.h b/app/node/input/media/media.h index 0253cc231..a19459cae 100644 --- a/app/node/input/media/media.h +++ b/app/node/input/media/media.h @@ -48,7 +48,7 @@ protected: private slots: void FootageChanged(); - void FootageColorSpaceChanged(); + void FootageParametersChanged(); }; diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index 4cc187623..855c0a806 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -91,11 +91,13 @@ bool ImageStream::premultiplied_alpha() const void ImageStream::set_premultiplied_alpha(bool e) { premultiplied_alpha_ = e; + + emit ParametersChanged(); } -const QString &ImageStream::colorspace() const +const QString &ImageStream::colorspace(bool default_if_empty) const { - if (colorspace_.isEmpty()) { + if (colorspace_.isEmpty() && default_if_empty) { return footage()->project()->default_input_colorspace(); } else { return colorspace_; @@ -106,7 +108,7 @@ void ImageStream::set_colorspace(const QString &color) { colorspace_ = color; - emit ColorSpaceChanged(); + emit ParametersChanged(); } void ImageStream::ColorConfigChanged() @@ -123,13 +125,13 @@ void ImageStream::ColorConfigChanged() } // Either way, the color calculation has likely changed so we signal here - emit ColorSpaceChanged(); + emit ParametersChanged(); } void ImageStream::DefaultColorSpaceChanged() { // If no colorspace is set, this stream uses the default color space and it's just changed if (colorspace_.isEmpty()) { - emit ColorSpaceChanged(); + emit ParametersChanged(); } } diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h index 952b0c432..7c6adad2c 100644 --- a/app/project/item/footage/imagestream.h +++ b/app/project/item/footage/imagestream.h @@ -43,12 +43,9 @@ public: bool premultiplied_alpha() const; void set_premultiplied_alpha(bool e); - const QString& colorspace() const; + const QString& colorspace(bool default_if_empty = true) const; void set_colorspace(const QString& color); -signals: - void ColorSpaceChanged(); - protected: virtual void FootageSetEvent(Footage*) override; diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index c7f273271..fc9bb8456 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -107,6 +107,8 @@ const int64_t &Stream::duration() const void Stream::set_duration(const int64_t &duration) { duration_ = duration; + + emit ParametersChanged(); } bool Stream::enabled() const diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index 0a0a7a319..b3c774604 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -115,6 +115,8 @@ protected: signals: void IndexChanged(); + void ParametersChanged(); + private: Footage* footage_; diff --git a/app/project/item/footage/videostream.cpp b/app/project/item/footage/videostream.cpp index 5a0fb5df4..36d0f7fd5 100644 --- a/app/project/item/footage/videostream.cpp +++ b/app/project/item/footage/videostream.cpp @@ -27,7 +27,8 @@ const int64_t VideoStream::kEndTimestamp = AV_NOPTS_VALUE; VideoStream::VideoStream() : - start_time_(0) + start_time_(0), + is_image_sequence_(false) { set_type(kVideo); } @@ -57,6 +58,17 @@ const int64_t &VideoStream::start_time() const void VideoStream::set_start_time(const int64_t &start_time) { start_time_ = start_time; + emit ParametersChanged(); +} + +bool VideoStream::is_image_sequence() const +{ + return is_image_sequence_; +} + +void VideoStream::set_image_sequence(bool e) +{ + is_image_sequence_ = e; } int64_t VideoStream::get_closest_timestamp_in_frame_index(const rational &time) diff --git a/app/project/item/footage/videostream.h b/app/project/item/footage/videostream.h index 3fed4bfc4..05ec4ac95 100644 --- a/app/project/item/footage/videostream.h +++ b/app/project/item/footage/videostream.h @@ -25,6 +25,7 @@ class VideoStream : public ImageStream { + Q_OBJECT public: VideoStream(); @@ -43,6 +44,9 @@ public: const int64_t& start_time() const; void set_start_time(const int64_t& start_time); + bool is_image_sequence() const; + void set_image_sequence(bool e); + int64_t get_closest_timestamp_in_frame_index(const rational& time); int64_t get_closest_timestamp_in_frame_index(int64_t timestamp); void clear_frame_index(); @@ -62,6 +66,8 @@ private: QMutex index_access_lock_; + bool is_image_sequence_; + }; using VideoStreamPtr = std::shared_ptr; diff --git a/app/render/backend/videorenderworker.cpp b/app/render/backend/videorenderworker.cpp index 4c4221426..a2de88f3b 100644 --- a/app/render/backend/videorenderworker.cpp +++ b/app/render/backend/videorenderworker.cpp @@ -179,6 +179,8 @@ void VideoRenderWorker::HashNodeRecursively(QCryptographicHash *hash, const Node if (stream->type() == Stream::kVideo) { hash->addData(QStringLiteral("%1/%2").arg(QString::number(input_time.numerator()), QString::number(input_time.denominator())).toUtf8()); + + hash->addData(QString::number(static_cast(stream.get())->start_time()).toUtf8()); /*Decoder::RetrieveState state = decoder->GetRetrieveState(input_time); if (state == Decoder::kReady) { From 2d57f22f3a9eb3cfc0eaf64743592b82440c361e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 9 Mar 2020 20:08:05 +1100 Subject: [PATCH 07/43] oiiodecoder: implemented heuristic to auto-detect start and end index --- app/codec/oiio/oiiodecoder.cpp | 33 +++++++++++++++++++++++++++++---- app/codec/oiio/oiiodecoder.h | 2 ++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 6f62bc6c9..f4a1e33f3 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -86,11 +86,23 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) video_stream->set_frame_rate(default_timebase.flipped()); video_stream->set_image_sequence(true); - // FIXME: Get actual start number - video_stream->set_start_time(1); + int64_t seq_index = GetImageSequenceIndex(f->filename()); - // FIXME: Get actual duration - video_stream->set_duration(200); + int64_t start_index = seq_index; + int64_t end_index = seq_index; + + // Heuristic to find the first and last images (users can always override this later in FootagePropertiesDialog) + while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), start_index-1))) { + start_index--; + } + + while (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), end_index+1))) { + end_index++; + } + + video_stream->set_start_time(start_index); + + video_stream->set_duration(end_index - start_index); } else { image_stream = std::make_shared(); } @@ -261,6 +273,19 @@ QString OIIODecoder::TransformImageSequenceFileName(const QString &filename, con return file_info.dir().filePath(file_info.fileName().replace(original_basename, new_basename)); } +int64_t OIIODecoder::GetImageSequenceIndex(const QString &filename) +{ + int digit_count = GetImageSequenceDigitCount(filename); + + QFileInfo file_info(filename); + + QString original_basename = file_info.baseName(); + + QString number_only = original_basename.mid(original_basename.size() - digit_count); + + return number_only.toLongLong(); +} + bool OIIODecoder::OpenImageHandler(const QString &fn) { image_ = OIIO::ImageInput::open(fn.toStdString()); diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index f4eff4db6..657758bd8 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -58,6 +58,8 @@ private: static QString TransformImageSequenceFileName(const QString& filename, const int64_t& number); + static int64_t GetImageSequenceIndex(const QString& filename); + bool OpenImageHandler(const QString& fn); void CloseImageHandle(); From c338e6d99df72d169d5b0e554ef0b5b9c1c21bd2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 11 Mar 2020 14:09:39 +1100 Subject: [PATCH 08/43] resizablescrollbar: refined timeline scrollbar resizing to be more accurate --- .../resizablescrollbar/resizablescrollbar.cpp | 37 +++++++++++-------- app/widget/timebased/timebased.cpp | 18 ++++++++- .../timelinewidget/timelinescaledobject.cpp | 4 +- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/app/widget/resizablescrollbar/resizablescrollbar.cpp b/app/widget/resizablescrollbar/resizablescrollbar.cpp index fc6a79d44..f18a86037 100644 --- a/app/widget/resizablescrollbar/resizablescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizablescrollbar.cpp @@ -39,11 +39,8 @@ void ResizableScrollBar::mouseMoveEvent(QMouseEvent *event) QRect sr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); - QRect gr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, - QStyle::SC_ScrollBarGroove, this); if (mouse_dragging_) { - int new_drag_pos = GetActiveMousePos(event); int mouse_movement = new_drag_pos - mouse_drag_start_; mouse_drag_start_ = new_drag_pos; @@ -52,21 +49,29 @@ void ResizableScrollBar::mouseMoveEvent(QMouseEvent *event) mouse_movement = -mouse_movement; } - double scale_multiplier = static_cast(sr.width()) / static_cast(sr.width() + mouse_movement); - emit RequestScale(scale_multiplier); + double width_adjustment = static_cast(sr.width() + mouse_movement); - if (mouse_handle_state_ == kInTopHandle) { - int slider_min = gr.x(); - int slider_max = gr.right() - (sr.width() + mouse_movement); - int val = QStyle::sliderValueFromPosition(minimum(), - maximum(), - event->pos().x() - slider_min, - slider_max - slider_min, - opt.upsideDown); + // Prevent dividing by zero or emitting a negative scale + if (width_adjustment > 0) { + double scale_multiplier = static_cast(sr.width()) / width_adjustment; + emit RequestScale(scale_multiplier); - setValue(val); - } else { - setValue(qRound(static_cast(value()) * scale_multiplier)); + if (mouse_handle_state_ == kInTopHandle) { + QRect gr = style()->subControlRect(QStyle::CC_ScrollBar, &opt, + QStyle::SC_ScrollBarGroove, this); + + int slider_min = gr.x(); + int slider_max = gr.right() - (sr.width() + mouse_movement); + int val = QStyle::sliderValueFromPosition(minimum(), + maximum(), + event->pos().x() - slider_min, + slider_max - slider_min, + opt.upsideDown); + + setValue(val); + } else { + setValue(qRound(static_cast(value()) * scale_multiplier)); + } } } else { diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index d5b8ce664..dfa31c908 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -81,7 +81,22 @@ void TimeBasedWidget::UpdateMaximumScroll() void TimeBasedWidget::ScrollBarResized(const double &multiplier) { - SetScale(GetScale() * multiplier); + QScrollBar* bar = static_cast(sender()); + + int current_max = bar->maximum(); + double proposed_max = static_cast(current_max) * multiplier; + + proposed_max = proposed_max - (bar->width() * 0.5 / multiplier) + (bar->width() * 0.5); + + double corrected_scale; + + if (current_max == 0) { + corrected_scale = multiplier; + } else { + corrected_scale = (proposed_max / static_cast(current_max)); + } + + SetScale(GetScale() * corrected_scale); } TimeRuler *TimeBasedWidget::ruler() const @@ -146,6 +161,7 @@ void TimeBasedWidget::SetTimebase(const rational &timebase) void TimeBasedWidget::SetScale(const double &scale) { + // Simple QObject slot wrapper around TimelineScaledObject::SetScale() TimelineScaledObject::SetScale(scale); } diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index f3a467782..d010c5129 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -71,7 +71,9 @@ const double& TimelineScaledObject::GetScale() const void TimelineScaledObject::SetScale(const double& scale) { - scale_ = scale; + Q_ASSERT(scale > 0); + + scale_ = qMin(scale, max_scale_); ScaleChangedEvent(scale_); } From d427d48ab15b17c7f9e5a5457f67cd5da0d88de1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 12 Mar 2020 12:04:19 +1100 Subject: [PATCH 09/43] stroke effect: implemented 'inner' option --- app/shaders/stroke.frag | 35 +++++++++++++++++++++++++++-------- app/shaders/stroke.xml | 6 ++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/shaders/stroke.frag b/app/shaders/stroke.frag index 20ff1607f..7388f71b1 100644 --- a/app/shaders/stroke.frag +++ b/app/shaders/stroke.frag @@ -10,11 +10,18 @@ uniform sampler2D tex_in; uniform vec3 color_in; uniform float radius_in; uniform float opacity_in; +uniform bool inner_in; void main(void) { - if (radius_in == 0.0 || opacity_in == 0.0) { + vec4 pixel_here = texture2D(tex_in, ove_texcoord); + + // Detect no-op situations + if (radius_in == 0.0 + || opacity_in == 0.0 + || (inner_in && pixel_here.a == 0.0) + || (!inner_in && pixel_here.a == 1.0)) { // No-op, do nothing - gl_FragColor = texture2D(tex_in, ove_texcoord); + gl_FragColor = pixel_here; return; } @@ -31,7 +38,13 @@ void main(void) { if (abs(length(vec2(i, j))) < radius) { // Get pixel here - stroke_weight += texture2D(tex_in, ove_texcoord + vec2(x_coord, y_coord)).a; + float alpha = texture2D(tex_in, ove_texcoord + vec2(x_coord, y_coord)).a; + + if (inner_in) { + alpha = 1.0 - alpha; + } + + stroke_weight += alpha; if (stroke_weight >= 1.0) { break; @@ -47,15 +60,21 @@ void main(void) { stroke_weight *= opacity_in * 0.01; + if (inner_in) { + stroke_weight *= pixel_here.a; + } + // Make RGBA color vec4 stroke_col = vec4(vec3(1.0) * stroke_weight, stroke_weight); //vec4 stroke_col = vec4(color_in * stroke_weight, stroke_weight); - // Alpha over color here - vec4 pixel_here = texture2D(tex_in, ove_texcoord); - - stroke_col *= 1.0 - pixel_here.a; - stroke_col += pixel_here; + if (inner_in) { + // Alpha over the stroke over the texture + stroke_col = pixel_here * (1.0 - stroke_col.a) + stroke_col; + } else { + // Alpha over the texture over the stroke + stroke_col = stroke_col * (1.0 - pixel_here.a) + pixel_here; + } gl_FragColor = stroke_col; } diff --git a/app/shaders/stroke.xml b/app/shaders/stroke.xml index 04de10d07..bb9c7aebc 100644 --- a/app/shaders/stroke.xml +++ b/app/shaders/stroke.xml @@ -38,6 +38,12 @@ 100 + + + Inner + false + + From 41da5dbd53070c614b1e32ffe49d0b8c16c49c30 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 12 Mar 2020 12:06:13 +1100 Subject: [PATCH 10/43] timebasedwidget: added comment explaining scrollbar re-adjustment --- app/widget/timebased/timebased.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index dfa31c908..a23b1701f 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -83,6 +83,9 @@ void TimeBasedWidget::ScrollBarResized(const double &multiplier) { QScrollBar* bar = static_cast(sender()); + // Our extension area (represented by a TimelineViewEndItem) is NOT scaled, but the ResizableScrollBar doesn't know + // this. Here we re-calculate the requested scale knowing that the end item is not affected by scale. + int current_max = bar->maximum(); double proposed_max = static_cast(current_max) * multiplier; From 9e4c79699e22aa4e8004f2000652e7d90524af64 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 15:30:58 +1100 Subject: [PATCH 11/43] node: split off traversing functions from renderer for usage elsewhere Fixes issue where Viewer node wouldn't pick up correct length if a tracks weren't in use. --- app/audio/audiomanager.cpp | 1 + app/audio/audiomanager.h | 2 + app/audio/outputmanager.cpp | 5 +- app/audio/outputmanager.h | 4 +- app/node/CMakeLists.txt | 2 + app/node/input/media/audio/audio.cpp | 5 -- app/node/input/media/audio/audio.h | 3 - app/node/output/viewer/viewer.cpp | 31 ++++---- app/node/output/viewer/viewer.h | 3 - app/node/traverser.cpp | 76 ++++++++++++++++++ app/node/traverser.h | 31 ++++++++ app/node/value.cpp | 8 +- app/render/backend/renderworker.cpp | 112 +++++++-------------------- app/render/backend/renderworker.h | 27 +++---- app/widget/viewer/footageviewer.cpp | 11 ++- app/widget/viewer/footageviewer.h | 2 + app/widget/viewer/viewer.cpp | 33 ++++++-- app/widget/viewer/viewer.h | 4 + 18 files changed, 218 insertions(+), 142 deletions(-) create mode 100644 app/node/traverser.cpp create mode 100644 app/node/traverser.h diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index df4ca479e..956c4a624 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -194,6 +194,7 @@ AudioManager::AudioManager() : RefreshDevices(); connect(&output_manager_, &AudioOutputManager::SentSamples, this, &AudioManager::SentSamples); + connect(&output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); output_manager_.SetEnableSendingSamples(true); } diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 8a806652e..73e990114 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -102,6 +102,8 @@ signals: void SentSamples(QVector averages); + void OutputNotified(); + private: AudioManager(); diff --git a/app/audio/outputmanager.cpp b/app/audio/outputmanager.cpp index de27d92ac..8d601f07c 100644 --- a/app/audio/outputmanager.cpp +++ b/app/audio/outputmanager.cpp @@ -54,7 +54,7 @@ void AudioOutputManager::Push(const QByteArray& samples) ResetToPushMode(); // Start pushing samples to the output - OutputNotified(); + PushMoreSamples(); } void AudioOutputManager::ResetToPushMode() @@ -92,7 +92,7 @@ void AudioOutputManager::PullFromDevice(QIODevice *device, int playback_speed) output_->start(&device_proxy_); } -void AudioOutputManager::OutputNotified() +void AudioOutputManager::PushMoreSamples() { // Check if we're currently in push mode and if we have samples to push if (!push_device_ || pushed_samples_.isEmpty()) { @@ -139,6 +139,7 @@ void AudioOutputManager::SetOutputDevice(QAudioDeviceInfo info, QAudioFormat for output_ = std::unique_ptr(new QAudioOutput(info, format, this)); output_->setNotifyInterval(1); push_device_ = output_->start(); + connect(output_.get(), &QAudioOutput::notify, this, &AudioOutputManager::PushMoreSamples); connect(output_.get(), &QAudioOutput::notify, this, &AudioOutputManager::OutputNotified); } diff --git a/app/audio/outputmanager.h b/app/audio/outputmanager.h index 4b00208de..06024f4d1 100644 --- a/app/audio/outputmanager.h +++ b/app/audio/outputmanager.h @@ -69,6 +69,8 @@ signals: */ void SentSamples(QVector averages); + void OutputNotified(); + private: void ProcessAverages(const char* data, int length); @@ -83,7 +85,7 @@ private: AudioOutputDeviceProxy device_proxy_; private slots: - void OutputNotified(); + void PushMoreSamples(); }; #endif // AUDIOHYBRIDDEVICE_H diff --git a/app/node/CMakeLists.txt b/app/node/CMakeLists.txt index 68bb5194f..bb8f9a42f 100644 --- a/app/node/CMakeLists.txt +++ b/app/node/CMakeLists.txt @@ -46,6 +46,8 @@ set(OLIVE_SOURCES node/output.cpp node/param.h node/param.cpp + node/traverser.h + node/traverser.cpp node/value.h node/value.cpp PARENT_SCOPE diff --git a/app/node/input/media/audio/audio.cpp b/app/node/input/media/audio/audio.cpp index ccfb52f56..6f7992de9 100644 --- a/app/node/input/media/audio/audio.cpp +++ b/app/node/input/media/audio/audio.cpp @@ -28,8 +28,3 @@ QString AudioInput::Description() const { return tr("Import an audio footage stream."); } - -NodeValueTable AudioInput::Value(const NodeValueDatabase &value) const -{ - return value[footage_input_]; -} diff --git a/app/node/input/media/audio/audio.h b/app/node/input/media/audio/audio.h index 641b4f8de..86aa8d330 100644 --- a/app/node/input/media/audio/audio.h +++ b/app/node/input/media/audio/audio.h @@ -15,9 +15,6 @@ public: virtual QString Category() const override; virtual QString Description() const override; -protected: - virtual NodeValueTable Value(const NodeValueDatabase& value) const override; - private: }; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index b86623419..f1327dcdd 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -20,6 +20,8 @@ #include "viewer.h" +#include "node/traverser.h" + ViewerOutput::ViewerOutput() { texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); @@ -28,9 +30,6 @@ ViewerOutput::ViewerOutput() samples_input_ = new NodeInput("samples_in", NodeInput::kSamples); AddInput(samples_input_); - length_input_ = new NodeInput("length_in", NodeInput::kRational); - AddInput(length_input_); - // Create TrackList instances track_inputs_.resize(Timeline::kTrackTypeCount); track_lists_.resize(Timeline::kTrackTypeCount); @@ -91,11 +90,6 @@ NodeInput *ViewerOutput::samples_input() const return samples_input_; } -NodeInput *ViewerOutput::length_input() const -{ - return length_input_; -} - void ViewerOutput::InvalidateCache(const rational &start_range, const rational &end_range, NodeInput *from) { Node::InvalidateCache(start_range, end_range, from); @@ -104,8 +98,6 @@ void ViewerOutput::InvalidateCache(const rational &start_range, const rational & emit VideoChangedBetween(TimeRange(start_range, end_range)); } else if (from == samples_input()) { emit AudioChangedBetween(TimeRange(start_range, end_range)); - } else if (from == length_input()) { - emit LengthChanged(Length()); } SendInvalidateCache(start_range, end_range); @@ -146,18 +138,23 @@ void ViewerOutput::set_audio_params(const AudioParams &audio) rational ViewerOutput::Length() { - if (!length_input_->IsConnected()) { - return timeline_length_; + NodeTraverser traverser; + + rational video_length; + + if (texture_input_->IsConnected()) { + NodeValueTable t = traverser.ProcessNode(NodeDependency(texture_input_->get_connected_node(), 0, 0)); + video_length = t.Get(NodeParam::kNumber, "length").value(); } - Node* connected_node = length_input_->get_connected_node(); + rational audio_length; - if (connected_node) { - // This is kind of messy? - return connected_node->Value(NodeValueDatabase()).Get(NodeParam::kNumber, "length").value(); + if (samples_input_->IsConnected()) { + NodeValueTable t = traverser.ProcessNode(NodeDependency(samples_input_->get_connected_node(), 0, 0)); + audio_length = t.Get(NodeParam::kNumber, "length").value(); } - return 0; + return qMax(video_length, qMax(audio_length, timeline_length_)); } const QUuid &ViewerOutput::uuid() const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index abdcd3f41..2d86dfc0e 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -52,7 +52,6 @@ public: NodeInput* texture_input() const; NodeInput* samples_input() const; - NodeInput* length_input() const; virtual void InvalidateCache(const rational &start_range, const rational &end_range, NodeInput *from = nullptr) override; virtual void InvalidateVisible(NodeInput *from) override; @@ -117,8 +116,6 @@ private: NodeInput* samples_input_; - NodeInput* length_input_; - VideoParams video_params_; AudioParams audio_params_; diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp new file mode 100644 index 000000000..fb3f28239 --- /dev/null +++ b/app/node/traverser.cpp @@ -0,0 +1,76 @@ +#include "traverser.h" + +#include "node.h" + +NodeTraverser::NodeTraverser() +{ + +} + +NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRange &range) +{ + NodeValueDatabase database; + + // We need to insert tables into the database for each input + foreach (NodeParam* param, node->parameters()) { + if (IsCancelled()) { + return NodeValueDatabase(); + } + + if (param->type() == NodeParam::kInput) { + NodeInput* input = static_cast(param); + TimeRange input_time = node->InputTimeAdjustment(input, range); + + NodeValueTable table = ProcessInput(input, input_time); + + InputProcessingEvent(input, input_time, &table); + + database.Insert(input, table); + } + } + + return database; +} + +NodeValueTable NodeTraverser::ProcessNode(const NodeDependency& dep) +{ + const Node* node = dep.node(); + + if (node->IsTrack()) { + // If the range is not wholly contained in this Block, we'll need to do some extra processing + return RenderBlock(static_cast(node), dep.range()); + } + + // FIXME: Cache certain values here if we've already processed them before + + // Generate database of input values of node + NodeValueDatabase database = GenerateDatabase(node, dep.range()); + + // By this point, the node should have all the inputs it needs to render correctly + NodeValueTable table = node->Value(database); + + ProcessNodeEvent(node, dep.range(), database, &table); + + return table; +} + +NodeValueTable NodeTraverser::RenderBlock(const TrackOutput *track, const TimeRange &range) +{ + // By default, don't bother traversing blocks + return NodeValueTable(); +} + +NodeValueTable NodeTraverser::ProcessInput(const NodeInput *input, const TimeRange& range) +{ + if (input->IsConnected()) { + // Value will equal something from the connected node, follow it + return ProcessNode(NodeDependency(input->get_connected_node(), range)); + } else { + // Push onto the table the value at this time from the input + QVariant input_value = input->get_value_at_time(range.in()); + + NodeValueTable table; + table.Push(input->data_type(), input_value); + return table; + } +} diff --git a/app/node/traverser.h b/app/node/traverser.h new file mode 100644 index 000000000..8166a7987 --- /dev/null +++ b/app/node/traverser.h @@ -0,0 +1,31 @@ +#ifndef NODETRAVERSER_H +#define NODETRAVERSER_H + +#include "codec/decoder.h" +#include "common/cancelableobject.h" +#include "dependency.h" +#include "node/output/track/track.h" +#include "project/item/footage/stream.h" +#include "value.h" + +class NodeTraverser : public CancelableObject +{ +public: + NodeTraverser(); + + NodeValueTable ProcessNode(const NodeDependency &dep); + +protected: + NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); + + virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range); + + NodeValueTable ProcessInput(const NodeInput* input, const TimeRange &range); + + virtual void InputProcessingEvent(NodeInput*, const TimeRange&, NodeValueTable*){} + + virtual void ProcessNodeEvent(const Node*, const TimeRange&, const NodeValueDatabase&, NodeValueTable*){} + +}; + +#endif // NODETRAVERSER_H diff --git a/app/node/value.cpp b/app/node/value.cpp index 2f5fc11a5..0ef541256 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -58,13 +58,9 @@ NodeValueTable::NodeValueTable() QVariant NodeValueTable::Get(const NodeParam::DataType &type, const QString &tag) const { - int value_index = GetInternal(type, tag); + NodeValue v = GetWithMeta(type, tag); - if (value_index >= 0) { - return values_.at(value_index).data(); - } - - return QVariant(); + return v.data(); } NodeValue NodeValueTable::GetWithMeta(const NodeParam::DataType &type, const QString &tag) const diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 63aa3d308..07512a4ca 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -87,95 +87,41 @@ bool RenderWorker::IsStarted() return started_; } -NodeValueTable RenderWorker::ProcessNode(const NodeDependency& dep) -{ - const Node* node = dep.node(); - - if (node->IsTrack()) { - // If the range is not wholly contained in this Block, we'll need to do some extra processing - return RenderBlock(static_cast(node), dep.range()); - } - - // FIXME: Cache certain values here if we've already processed them before - - // Generate database of input values of node - NodeValueDatabase database = GenerateDatabase(node, dep.range()); - - // By this point, the node should have all the inputs it needs to render correctly - NodeValueTable table = node->Value(database); - - // Check if we have a shader for this output - RunNodeAccelerated(node, dep.range(), database, &table); - - return table; -} - -NodeValueTable RenderWorker::ProcessInput(const NodeInput *input, const TimeRange& range) -{ - if (input->IsConnected()) { - // Value will equal something from the connected node, follow it - return ProcessNode(NodeDependency(input->get_connected_node(), range)); - } else { - // Push onto the table the value at this time from the input - QVariant input_value = input->get_value_at_time(range.in()); - - NodeValueTable table; - table.Push(input->data_type(), input_value); - return table; - } -} - void RenderWorker::ReportUnavailableFootage(StreamPtr stream, Decoder::RetrieveState state, const rational& stream_time) { emit FootageUnavailable(stream, state, path_.range(), stream_time); } +void RenderWorker::InputProcessingEvent(NodeInput* input, const TimeRange& input_time, NodeValueTable *table) +{ + // Exception for Footage types where we actually retrieve some Footage data from a decoder + if (input->data_type() == NodeParam::kFootage) { + StreamPtr stream = ResolveStreamFromInput(input); + + if (stream) { + DecoderPtr decoder = ResolveDecoderFromInput(stream); + + if (decoder) { + + Decoder::RetrieveState state = decoder->GetRetrieveState(input_time.out()); + + if (state == Decoder::kReady) { + FrameToValue(decoder, stream, input_time, table); + } else { + ReportUnavailableFootage(stream, state, input_time.out()); + } + } + } + } +} + +void RenderWorker::ProcessNodeEvent(const Node *node, const TimeRange &range, const NodeValueDatabase &input_params, NodeValueTable* output_params) +{ + // Check if we have a shader for this output + RunNodeAccelerated(node, range, input_params, output_params); +} + const NodeDependency &RenderWorker::CurrentPath() const { return path_; } - - -#include "common/functiontimer.h" -NodeValueDatabase RenderWorker::GenerateDatabase(const Node* node, const TimeRange &range) -{ - NodeValueDatabase database; - - // We need to insert tables into the database for each input - foreach (NodeParam* param, node->parameters()) { - if (IsCancelled()) { - return NodeValueDatabase(); - } - - if (param->type() == NodeParam::kInput) { - NodeInput* input = static_cast(param); - TimeRange input_time = node->InputTimeAdjustment(input, range); - - NodeValueTable table = ProcessInput(input, input_time); - - // Exception for Footage types where we actually retrieve some Footage data from a decoder - if (input->data_type() == NodeParam::kFootage) { - StreamPtr stream = ResolveStreamFromInput(input); - - if (stream) { - DecoderPtr decoder = ResolveDecoderFromInput(stream); - - if (decoder) { - - Decoder::RetrieveState state = decoder->GetRetrieveState(input_time.out()); - - if (state == Decoder::kReady) { - FrameToValue(decoder, stream, input_time, &table); - } else { - ReportUnavailableFootage(stream, state, input_time.out()); - } - } - } - } - - database.Insert(input, table); - } - } - - return database; -} diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index 7965e297e..885d3c60f 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -3,13 +3,13 @@ #include -#include "common/cancelableobject.h" #include "common/constructors.h" -#include "node/output/track/track.h" -#include "node/node.h" #include "decodercache.h" +#include "node/node.h" +#include "node/output/track/track.h" +#include "node/traverser.h" -class RenderWorker : public QObject, public CancelableObject +class RenderWorker : public QObject, public NodeTraverser { Q_OBJECT public: @@ -38,24 +38,21 @@ protected: virtual void RunNodeAccelerated(const Node *node, const TimeRange& range, const NodeValueDatabase &input_params, NodeValueTable* output_params); - StreamPtr ResolveStreamFromInput(NodeInput* input); - DecoderPtr ResolveDecoderFromInput(StreamPtr stream); - virtual void FrameToValue(DecoderPtr decoder, StreamPtr stream, const TimeRange &range, NodeValueTable* table) = 0; - NodeValueTable ProcessNode(const NodeDependency &dep); - - virtual NodeValueTable RenderBlock(const TrackOutput *track, const TimeRange& range) = 0; - - NodeValueTable ProcessInput(const NodeInput* input, const TimeRange &range); - virtual void ReportUnavailableFootage(StreamPtr stream, Decoder::RetrieveState state, const rational& stream_time); + virtual void InputProcessingEvent(NodeInput *input, const TimeRange &input_time, NodeValueTable* table) override; + + virtual void ProcessNodeEvent(const Node *node, const TimeRange &range, const NodeValueDatabase &input_params, NodeValueTable* output_params) override; + + StreamPtr ResolveStreamFromInput(NodeInput* input); + + DecoderPtr ResolveDecoderFromInput(StreamPtr stream); + const NodeDependency& CurrentPath() const; private: - NodeValueDatabase GenerateDatabase(const Node *node, const TimeRange &range); - bool started_; DecoderCache* decoder_cache_; diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 9b0c18db4..8e3f7ec8d 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -13,9 +13,13 @@ FootageViewerWidget::FootageViewerWidget(QWidget *parent) : audio_node_ = new AudioInput(); viewer_node_ = new ViewerOutput(); + waveform_view_ = new QWidget(); + waveform_view_->setAutoFillBackground(true); + waveform_view_->setStyleSheet("background: black;"); + stack()->addWidget(waveform_view_); + NodeParam::ConnectEdge(video_node_->output(), viewer_node_->texture_input()); NodeParam::ConnectEdge(audio_node_->output(), viewer_node_->samples_input()); - NodeParam::ConnectEdge(video_node_->output(), viewer_node_->length_input()); connect(gl_widget_, &ViewerGLWidget::DragStarted, this, &FootageViewerWidget::StartFootageDrag); } @@ -55,6 +59,10 @@ void FootageViewerWidget::SetFootage(Footage *footage) if (video_stream) { video_node_->SetFootage(video_stream); viewer_node_->set_video_params(VideoParams(video_stream->width(), video_stream->height(), video_stream->frame_rate().flipped())); + + stack()->setCurrentWidget(gl_widget_); + } else { + stack()->setCurrentWidget(waveform_view_); } if (audio_stream) { @@ -79,7 +87,6 @@ void FootageViewerWidget::StartFootageDrag() return; } - qDebug() << "Drag start!"; QDrag* drag = new QDrag(this); QMimeData* mimedata = new QMimeData(); diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index dc1634291..055906bd5 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -27,6 +27,8 @@ private: ViewerOutput* viewer_node_; + QWidget* waveform_view_; + private slots: void StartFootageDrag(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index f14080eed..6f42faaae 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -47,9 +47,13 @@ ViewerWidget::ViewerWidget(QWidget *parent) : QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); + // Set up stacked widget to allow switching away from the viewer widget + stack_ = new QStackedWidget(); + layout->addWidget(stack_); + // Create main OpenGL-based view sizer_ = new ViewerSizer(); - layout->addWidget(sizer_); + stack_->addWidget(sizer_); gl_widget_ = new ViewerGLWidget(); connect(gl_widget_, &ViewerGLWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); @@ -111,7 +115,13 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) { - SetTimebase(n->video_params().time_base()); + if (!n->video_params().time_base().isNull()) { + SetTimebase(n->video_params().time_base()); + } else if (n->audio_params().sample_rate() > 0) { + SetTimebase(rational(1, n->audio_params().sample_rate())); + } else { + SetTimebase(rational()); + } connect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); @@ -139,7 +149,7 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) { Pause(); - SetTimebase(0); + SetTimebase(rational()); disconnect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); @@ -171,6 +181,11 @@ void ViewerWidget::resizeEvent(QResizeEvent *event) } } +QStackedWidget *ViewerWidget::stack() const +{ + return stack_; +} + void ViewerWidget::TogglePlayPause() { if (IsPlaying()) { @@ -251,7 +266,11 @@ void ViewerWidget::PlayInternal(int speed) controls_->ShowPauseButton(); - connect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + if (stack_->currentWidget() == gl_widget_) { + connect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + } else { + connect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); + } } void ViewerWidget::PushScrubbedAudio() @@ -387,7 +406,11 @@ void ViewerWidget::Pause() playback_speed_ = 0; controls_->ShowPlayButton(); - disconnect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + if (stack_->currentWidget() == gl_widget_) { + disconnect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); + } else { + disconnect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); + } } } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index a84aebc59..a5e38febe 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -115,6 +115,8 @@ protected: virtual void resizeEvent(QResizeEvent *event) override; + QStackedWidget* stack() const; + OpenGLBackend* video_renderer_; AudioBackend* audio_renderer_; @@ -131,6 +133,8 @@ private: int CalculateDivider(); + QStackedWidget* stack_; + ViewerSizer* sizer_; PlaybackControls* controls_; From e2fe9ad548cd56989bc0ac5574f1ef863af9426c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 15:48:09 +1100 Subject: [PATCH 12/43] viewer: folded waveform view into base viewer widget class --- app/widget/viewer/footageviewer.cpp | 17 +++++------------ app/widget/viewer/footageviewer.h | 2 -- app/widget/viewer/viewer.cpp | 28 ++++++++++++++++++++++------ app/widget/viewer/viewer.h | 6 ++++-- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 8e3f7ec8d..926806f3d 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -13,14 +13,6 @@ FootageViewerWidget::FootageViewerWidget(QWidget *parent) : audio_node_ = new AudioInput(); viewer_node_ = new ViewerOutput(); - waveform_view_ = new QWidget(); - waveform_view_->setAutoFillBackground(true); - waveform_view_->setStyleSheet("background: black;"); - stack()->addWidget(waveform_view_); - - NodeParam::ConnectEdge(video_node_->output(), viewer_node_->texture_input()); - NodeParam::ConnectEdge(audio_node_->output(), viewer_node_->samples_input()); - connect(gl_widget_, &ViewerGLWidget::DragStarted, this, &FootageViewerWidget::StartFootageDrag); } @@ -33,6 +25,9 @@ void FootageViewerWidget::SetFootage(Footage *footage) { if (footage_) { ConnectViewerNode(nullptr); + + NodeParam::DisconnectEdge(video_node_->output(), viewer_node_->texture_input()); + NodeParam::DisconnectEdge(audio_node_->output(), viewer_node_->samples_input()); } footage_ = footage; @@ -59,15 +54,13 @@ void FootageViewerWidget::SetFootage(Footage *footage) if (video_stream) { video_node_->SetFootage(video_stream); viewer_node_->set_video_params(VideoParams(video_stream->width(), video_stream->height(), video_stream->frame_rate().flipped())); - - stack()->setCurrentWidget(gl_widget_); - } else { - stack()->setCurrentWidget(waveform_view_); + NodeParam::ConnectEdge(video_node_->output(), viewer_node_->texture_input()); } if (audio_stream) { audio_node_->SetFootage(audio_stream); viewer_node_->set_audio_params(AudioParams(audio_stream->sample_rate(), audio_stream->channel_layout())); + NodeParam::ConnectEdge(audio_node_->output(), viewer_node_->samples_input()); } ConnectViewerNode(viewer_node_, footage->project()->color_manager()); diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index 055906bd5..dc1634291 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -27,8 +27,6 @@ private: ViewerOutput* viewer_node_; - QWidget* waveform_view_; - private slots: void StartFootageDrag(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 6f42faaae..0853868f7 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -51,7 +51,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : stack_ = new QStackedWidget(); layout->addWidget(stack_); - // Create main OpenGL-based view + // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); stack_->addWidget(sizer_); @@ -59,6 +59,12 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(gl_widget_, &ViewerGLWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); sizer_->SetWidget(gl_widget_); + // Create waveform view when audio is connected and video isn't + waveform_view_ = new QWidget(); + waveform_view_->setAutoFillBackground(true); + waveform_view_->setStyleSheet("background: black;"); + stack_->addWidget(waveform_view_); + // Create time ruler layout->addWidget(ruler()); @@ -128,6 +134,8 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters); connect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible); + connect(n, &ViewerOutput::VideoGraphChanged, this, &ViewerWidget::UpdateStack); + connect(n, &ViewerOutput::AudioGraphChanged, this, &ViewerWidget::UpdateStack); SizeChangedSlot(n->video_params().width(), n->video_params().height()); LengthChangedSlot(n->Length()); @@ -143,6 +151,8 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) divider_ = CalculateDivider(); UpdateRendererParameters(); + + UpdateStack(); } void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) @@ -156,6 +166,8 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererParameters); disconnect(n, &ViewerOutput::VisibleInvalidated, this, &ViewerWidget::InvalidateVisible); + disconnect(n, &ViewerOutput::VideoGraphChanged, this, &ViewerWidget::UpdateStack); + disconnect(n, &ViewerOutput::AudioGraphChanged, this, &ViewerWidget::UpdateStack); // Effectively disables the viewer and clears the state SizeChangedSlot(0, 0); @@ -181,11 +193,6 @@ void ViewerWidget::resizeEvent(QResizeEvent *event) } } -QStackedWidget *ViewerWidget::stack() const -{ - return stack_; -} - void ViewerWidget::TogglePlayPause() { if (IsPlaying()) { @@ -306,6 +313,15 @@ int ViewerWidget::CalculateDivider() return divider_; } +void ViewerWidget::UpdateStack() +{ + if (!GetConnectedNode() || GetConnectedNode()->texture_input()->IsConnected()) { + stack_->setCurrentWidget(gl_widget_); + } else { + stack_->setCurrentWidget(waveform_view_); + } +} + void ViewerWidget::UpdateRendererParameters() { if (!GetConnectedNode()) { diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index a5e38febe..697ef7cbc 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -115,8 +115,6 @@ protected: virtual void resizeEvent(QResizeEvent *event) override; - QStackedWidget* stack() const; - OpenGLBackend* video_renderer_; AudioBackend* audio_renderer_; @@ -156,6 +154,8 @@ private: bool time_changed_from_timer_; + QWidget* waveform_view_; + private slots: void PlaybackTimerUpdate(); @@ -188,6 +188,8 @@ private slots: void InvalidateVisible(); + void UpdateStack(); + }; #endif // VIEWER_WIDGET_H From 0418c8ab85a161446e3ca5d06121ec75aafaca6b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 16:02:18 +1100 Subject: [PATCH 13/43] viewer: began waveformview widget --- app/widget/viewer/CMakeLists.txt | 2 ++ app/widget/viewer/viewer.cpp | 5 ++--- app/widget/viewer/viewer.h | 3 ++- app/widget/viewer/waveformview.cpp | 24 ++++++++++++++++++++++++ app/widget/viewer/waveformview.h | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 app/widget/viewer/waveformview.cpp create mode 100644 app/widget/viewer/waveformview.h diff --git a/app/widget/viewer/CMakeLists.txt b/app/widget/viewer/CMakeLists.txt index c5cf17d3b..6d99220dc 100644 --- a/app/widget/viewer/CMakeLists.txt +++ b/app/widget/viewer/CMakeLists.txt @@ -24,5 +24,7 @@ set(OLIVE_SOURCES widget/viewer/viewerglwidget.cpp widget/viewer/viewersizer.h widget/viewer/viewersizer.cpp + widget/viewer/waveformview.h + widget/viewer/waveformview.cpp PARENT_SCOPE ) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 0853868f7..c3b2de6d4 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -60,9 +60,8 @@ ViewerWidget::ViewerWidget(QWidget *parent) : sizer_->SetWidget(gl_widget_); // Create waveform view when audio is connected and video isn't - waveform_view_ = new QWidget(); - waveform_view_->setAutoFillBackground(true); - waveform_view_->setStyleSheet("background: black;"); + waveform_view_ = new WaveformView(); + stack_->addWidget(waveform_view_); // Create time ruler diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 697ef7cbc..6e52b9fa1 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -35,6 +35,7 @@ #include "render/backend/audio/audiobackend.h" #include "viewerglwidget.h" #include "viewersizer.h" +#include "waveformview.h" #include "widget/playbackcontrols/playbackcontrols.h" #include "widget/timebased/timebased.h" @@ -154,7 +155,7 @@ private: bool time_changed_from_timer_; - QWidget* waveform_view_; + WaveformView* waveform_view_; private slots: void PlaybackTimerUpdate(); diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp new file mode 100644 index 000000000..130993efb --- /dev/null +++ b/app/widget/viewer/waveformview.cpp @@ -0,0 +1,24 @@ +#include "waveformview.h" + +#include +#include + +WaveformView::WaveformView(QWidget *parent) : + TimeBasedWidget(parent) +{ + setAutoFillBackground(true); + setStyleSheet("background: black;"); +} + +void WaveformView::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + QPainter p(this); + + p.setPen(Qt::green); + + for (int i=0;i(i) * 0.025) * (height() / 2)); + } +} diff --git a/app/widget/viewer/waveformview.h b/app/widget/viewer/waveformview.h new file mode 100644 index 000000000..cc1a1c17a --- /dev/null +++ b/app/widget/viewer/waveformview.h @@ -0,0 +1,16 @@ +#ifndef WAVEFORMVIEW_H +#define WAVEFORMVIEW_H + +#include "widget/timebased/timebased.h" + +class WaveformView : public TimeBasedWidget +{ +public: + WaveformView(QWidget* parent = nullptr); + +protected: + virtual void paintEvent(QPaintEvent* event) override; + +}; + +#endif // WAVEFORMVIEW_H From c1c8067ee757010edbf2c9ebd57b4e48a2b282a2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 16:41:09 +1100 Subject: [PATCH 14/43] viewer: implemented basic zoom No scrolling functionality yet, but the zoom is functional and efficient. --- app/widget/viewer/viewer.cpp | 28 +++++++++--- app/widget/viewer/viewer.h | 2 + app/widget/viewer/viewerglwidget.h | 24 +++++----- app/widget/viewer/viewersizer.cpp | 71 ++++++++++++++++++++++++------ app/widget/viewer/viewersizer.h | 21 +++++++++ 5 files changed, 113 insertions(+), 33 deletions(-) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index c3b2de6d4..a2a8230fd 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -57,11 +57,11 @@ ViewerWidget::ViewerWidget(QWidget *parent) : gl_widget_ = new ViewerGLWidget(); connect(gl_widget_, &ViewerGLWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); + connect(sizer_, &ViewerSizer::RequestMatrix, gl_widget_, &ViewerGLWidget::SetMatrix); sizer_->SetWidget(gl_widget_); // Create waveform view when audio is connected and video isn't waveform_view_ = new WaveformView(); - stack_->addWidget(waveform_view_); // Create time ruler @@ -272,7 +272,7 @@ void ViewerWidget::PlayInternal(int speed) controls_->ShowPauseButton(); - if (stack_->currentWidget() == gl_widget_) { + if (stack_->currentWidget() == sizer_) { connect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); } else { connect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); @@ -315,7 +315,7 @@ int ViewerWidget::CalculateDivider() void ViewerWidget::UpdateStack() { if (!GetConnectedNode() || GetConnectedNode()->texture_input()->IsConnected()) { - stack_->setCurrentWidget(gl_widget_); + stack_->setCurrentWidget(sizer_); } else { stack_->setCurrentWidget(waveform_view_); } @@ -393,12 +393,21 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) // Playback resolution QMenu* playback_resolution_menu = menu.addMenu(tr("Resolution")); playback_resolution_menu->addAction(tr("Full"))->setData(1); - playback_resolution_menu->addAction(tr("1/2"))->setData(2); - playback_resolution_menu->addAction(tr("1/4"))->setData(4); - playback_resolution_menu->addAction(tr("1/8"))->setData(8); - playback_resolution_menu->addAction(tr("1/16"))->setData(16); + int dividers[] = {2, 4, 8, 16}; + for (int i=0;i<4;i++) { + playback_resolution_menu->addAction(tr("1/%1").arg(dividers[i]))->setData(dividers[i]); + } connect(playback_resolution_menu, &QMenu::triggered, this, &ViewerWidget::SetDividerFromMenu); + // Viewer Zoom Level + QMenu* zoom_menu = menu.addMenu(tr("Zoom")); + int zoom_levels[] = {10, 25, 50, 75, 100, 150, 200, 400}; + zoom_menu->addAction(tr("Fit"))->setData(0); + for (int i=0;i<8;i++) { + zoom_menu->addAction(tr("%1%").arg(zoom_levels[i]))->setData(zoom_levels[i]); + } + connect(zoom_menu, &QMenu::triggered, this, &ViewerWidget::SetZoomFromMenu); + foreach (QAction* a, playback_resolution_menu->actions()) { a->setCheckable(true); if (a->data() == divider_) { @@ -564,6 +573,11 @@ void ViewerWidget::SetDividerFromMenu(QAction *action) UpdateRendererParameters(); } +void ViewerWidget::SetZoomFromMenu(QAction *action) +{ + sizer_->SetZoom(action->data().toInt()); +} + void ViewerWidget::InvalidateVisible() { video_renderer_->InvalidateCache(TimeRange(GetTime(), GetTime())); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 6e52b9fa1..a45a34691 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -187,6 +187,8 @@ private slots: void SetDividerFromMenu(QAction* action); + void SetZoomFromMenu(QAction* action); + void InvalidateVisible(); void UpdateStack(); diff --git a/app/widget/viewer/viewerglwidget.h b/app/widget/viewer/viewerglwidget.h index f2e2bd1ca..4c14fb701 100644 --- a/app/widget/viewer/viewerglwidget.h +++ b/app/widget/viewer/viewerglwidget.h @@ -68,18 +68,17 @@ public: */ void DisconnectColorManager(); - /** - * @brief Set the transformation matrix to draw with - * - * Set this if you want the drawing to pass through some sort of transform (most of the time you won't want this). - */ - void SetMatrix(const QMatrix4x4& mat); - /** * @brief Set an image to load and display on screen */ void SetImage(const QString& fn); + ColorManager* color_manager() const; + + const QString& ocio_display() const; + const QString& ocio_view() const; + const QString& ocio_look() const; + public slots: /** * @brief Set the texture to draw and draw it @@ -113,11 +112,12 @@ public slots: */ void SetOCIOLook(const QString& look); - ColorManager* color_manager() const; - - const QString& ocio_display() const; - const QString& ocio_view() const; - const QString& ocio_look() const; + /** + * @brief Set the transformation matrix to draw with + * + * Set this if you want the drawing to pass through some sort of transform (most of the time you won't want this). + */ + void SetMatrix(const QMatrix4x4& mat); signals: void DragStarted(); diff --git a/app/widget/viewer/viewersizer.cpp b/app/widget/viewer/viewersizer.cpp index a9c3c2fc1..d9d906bc1 100644 --- a/app/widget/viewer/viewersizer.cpp +++ b/app/widget/viewer/viewersizer.cpp @@ -1,9 +1,12 @@ #include "viewersizer.h" +#include + ViewerSizer::ViewerSizer(QWidget *parent) : QWidget(parent), widget_(nullptr), - aspect_ratio_(0) + aspect_ratio_(0), + zoom_(0) { } @@ -23,15 +26,25 @@ void ViewerSizer::SetWidget(QWidget *widget) void ViewerSizer::SetChildSize(int width, int height) { - if (height == 0) { + width_ = width; + height_ = height; + + if (!width_ || !height_) { aspect_ratio_ = 0; } else { - aspect_ratio_ = static_cast(width) / static_cast(height); + aspect_ratio_ = static_cast(width_) / static_cast(height_); } UpdateSize(); } +void ViewerSizer::SetZoom(int percent) +{ + zoom_ = percent; + + UpdateSize(); +} + void ViewerSizer::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); @@ -53,21 +66,51 @@ void ViewerSizer::UpdateSize() widget_->setVisible(true); - double our_aspect_ratio = static_cast(width()) / static_cast(height()); + QSize child_size; + QMatrix4x4 child_matrix; - QPoint child_pos; - QSize child_size = size(); + if (zoom_ <= 0) { + + // If zoom is 0, we auto-fit + double our_aspect_ratio = static_cast(width()) / static_cast(height()); + + child_size = size(); + + if (our_aspect_ratio > aspect_ratio_) { + // This container is wider than the image, scale by height + child_size = QSize(qRound(child_size.height() * aspect_ratio_), height()); + } else { + // This container is taller than the image, scale by width + child_size = QSize(width(), qRound(child_size.width() / aspect_ratio_)); + } - if (our_aspect_ratio > aspect_ratio_) { - // This container is wider than the image, scale by height - child_size.setWidth(qRound(child_size.height() * aspect_ratio_)); - child_pos.setX(width() / 2 - child_size.width() / 2); } else { - // This container is taller than the image, scale by width - child_size.setHeight(qRound(child_size.width() / aspect_ratio_)); - child_pos.setY(height() / 2 - child_size.height() / 2); + + float x_scale = 1.0f; + float y_scale = 1.0f; + + int zoomed_width = qRound(width_ * static_cast(zoom_) * 0.01); + int zoomed_height = qRound(height_ * static_cast(zoom_) * 0.01); + + if (zoomed_width > width()) { + x_scale = static_cast(zoomed_width) / static_cast(width()); + zoomed_width = width(); + } + + if (zoomed_height > height()) { + y_scale = static_cast(zoomed_height) / static_cast(height()); + zoomed_height = height(); + } + + // Rather than make a huge surface, we still crop at our width/height and then signal a matrix + child_matrix.scale(x_scale, y_scale, 1.0F); + + child_size = QSize(zoomed_width, zoomed_height); + } widget_->resize(child_size); - widget_->move(child_pos); + widget_->move(width() / 2 - child_size.width() / 2, height() / 2 - child_size.height() / 2); + + emit RequestMatrix(child_matrix); } diff --git a/app/widget/viewer/viewersizer.h b/app/widget/viewer/viewersizer.h index 182b61164..9b383b01f 100644 --- a/app/widget/viewer/viewersizer.h +++ b/app/widget/viewer/viewersizer.h @@ -52,6 +52,16 @@ public: */ void SetChildSize(int width, int height); + /** + * @brief Set the zoom value of the child widget + * + * The number is an integer percentage (100 = 100%). Set to 0 to auto-fit. + */ + void SetZoom(int percent); + +signals: + void RequestMatrix(const QMatrix4x4& matrix); + protected: /** * @brief Listen for resize events to ensure the child widget remains correctly sized @@ -71,11 +81,22 @@ private: */ QWidget* widget_; + /** + * @brief Internal resolution values + */ + int width_; + int height_; + /** * @brief Aspect ratio calculated from the size provided by SetChildSize() */ double aspect_ratio_; + /** + * @brief Internal zoom value + */ + int zoom_; + }; #endif // VIEWERSIZER_H From fdfe53c70036f61b2c8bf3393dea872d244885ce Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 16:49:59 +1100 Subject: [PATCH 15/43] oiiodecoder: check for sequential images in sequence heuristic --- app/codec/oiio/oiiodecoder.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index f4a1e33f3..ea7daff5c 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -62,17 +62,23 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) return false; } + is_sequence_ = false; + // Heuristically determine whether this file is part of an image sequence or not if (GetImageSequenceDigitCount(f->filename()) > 0) { - // We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer - // to the Core which will definitely be in the UI thread and block here until we get an answer from the user - QMetaObject::invokeMethod(Core::instance(), - "ConfirmImageSequence", - Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, is_sequence_), - Q_ARG(QString, f->filename())); - } else { - is_sequence_ = false; + int64_t ind = GetImageSequenceIndex(f->filename()); + + // Check if files around exist around it with that follow a sequence + if (QFileInfo::exists(TransformImageSequenceFileName(f->filename(), ind - 1)) + || QFileInfo::exists(TransformImageSequenceFileName(f->filename(), ind + 1))) { + // We need user feedback here and since UI must occur in the UI thread (and we could be in any thread), we defer + // to the Core which will definitely be in the UI thread and block here until we get an answer from the user + QMetaObject::invokeMethod(Core::instance(), + "ConfirmImageSequence", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, is_sequence_), + Q_ARG(QString, f->filename())); + } } ImageStreamPtr image_stream; From 2ac6b601bf30a0e664326347d745bd05413679bb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 13 Mar 2020 17:03:27 +1100 Subject: [PATCH 16/43] renderer: fixed segfault when no texture is generated --- app/render/backend/opengl/openglproxy.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index 2d2170da3..d486fbcd8 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -397,6 +397,10 @@ void OpenGLProxy::TextureToBuffer(const QVariant &tex_in, void *buffer) { OpenGLTextureCache::ReferencePtr texture = tex_in.value(); + if (!texture) { + return; + } + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); buffer_.Attach(texture->texture()); buffer_.Bind(); From 75417f85d1f709f153dae1c309cfd612cff4c26c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 00:40:39 +1100 Subject: [PATCH 17/43] timeruler: ported common functions to timelinescaledobject Removed code that was largely duplicated. --- app/widget/timeruler/timeruler.cpp | 54 ++++++++++++------------------ app/widget/timeruler/timeruler.h | 18 ++++------ 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 15a90b6ab..56670d420 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -35,7 +35,6 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare scroll_(0), text_visible_(text_visible), centered_text_(true), - scale_(1.0), time_(0), show_cache_status_(cache_status_visible), timeline_points_(nullptr) @@ -59,29 +58,6 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare UpdateHeight(); } -const double &TimeRuler::GetScale() -{ - return scale_; -} - -void TimeRuler::SetScale(const double &d) -{ - scale_ = d; - - update(); -} - -void TimeRuler::SetTimebase(const rational &r) -{ - timebase_ = r; - - timebase_dbl_ = timebase_.toDouble(); - - timebase_flipped_dbl_ = timebase_.flipped().toDouble(); - - update(); -} - void TimeRuler::ConnectTimelinePoints(TimelinePoints *points) { if (timeline_points_) { @@ -141,7 +117,7 @@ void TimeRuler::CacheInvalidatedRange(const TimeRange& range) void TimeRuler::CacheTimeReady(const rational &time) { if (show_cache_status_) { - dirty_cache_ranges_.RemoveTimeRange(TimeRange(time, time + timebase_)); + dirty_cache_ranges_.RemoveTimeRange(TimeRange(time, time + timebase())); update(); } @@ -150,7 +126,7 @@ void TimeRuler::CacheTimeReady(const rational &time) void TimeRuler::paintEvent(QPaintEvent *) { // Nothing to paint if the timebase is invalid - if (timebase_.isNull()) { + if (timebase().isNull()) { return; } @@ -172,12 +148,12 @@ void TimeRuler::paintEvent(QPaintEvent *) } } - double width_of_frame = timebase_dbl_ * scale_; + double width_of_frame = timebase_dbl() * GetScale(); double width_of_second = 0; do { - width_of_second += timebase_dbl_; + width_of_second += timebase_dbl(); } while (width_of_second < 1.0); - width_of_second *= scale_; + width_of_second *= GetScale(); double width_of_minute = width_of_second * 60; double width_of_hour = width_of_minute * 60; double width_of_day = width_of_hour * 24; @@ -273,7 +249,7 @@ void TimeRuler::paintEvent(QPaintEvent *) if (text_visible_) { QRect text_rect; Qt::Alignment text_align; - QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase_, Timecode::CurrentDisplay()); + QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase(), Timecode::CurrentDisplay()); int timecode_width = QFontMetricsWidth(fm, timecode_str); int timecode_left; @@ -362,6 +338,18 @@ void TimeRuler::mouseMoveEvent(QMouseEvent *event) } } +void TimeRuler::TimebaseChangedEvent(const rational &tb) +{ + timebase_flipped_dbl_ = tb.flipped().toDouble(); + + update(); +} + +void TimeRuler::ScaleChangedEvent(const double &) +{ + update(); +} + void TimeRuler::DrawPlayhead(QPainter *p, int x, int y) { p->setRenderHint(QPainter::Antialiasing); @@ -388,7 +376,7 @@ int TimeRuler::CacheStatusHeight() const double TimeRuler::ScreenToUnitFloat(int screen) { - return (screen + scroll_) / scale_ / timebase_dbl_; + return (screen + scroll_) / GetScale() / timebase_dbl(); } int64_t TimeRuler::ScreenToUnit(int screen) @@ -398,12 +386,12 @@ int64_t TimeRuler::ScreenToUnit(int screen) int TimeRuler::UnitToScreen(int64_t unit) { - return qFloor(static_cast(unit) * scale_ * timebase_dbl_) - scroll_; + return qFloor(static_cast(unit) * GetScale() * timebase_dbl()) - scroll_; } int TimeRuler::TimeToScreen(const rational &time) { - return qFloor(time.toDouble() * scale_) - scroll_; + return qFloor(time.toDouble() * GetScale()) - scroll_; } void TimeRuler::SeekToScreenPoint(int screen) diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index bb1c5f290..b0752d656 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -27,19 +27,15 @@ #include "common/rational.h" #include "common/timerange.h" #include "timeline/timelinepoints.h" +#include "widget/timelinewidget/timelinescaledobject.h" #include "widget/timelinewidget/view/timelineplayhead.h" -class TimeRuler : public QWidget +class TimeRuler : public QWidget, public TimelineScaledObject { Q_OBJECT public: TimeRuler(bool text_visible = true, bool cache_status_visible = false, QWidget* parent = nullptr); - const double& GetScale(); - void SetScale(const double& d); - - void SetTimebase(const rational& r); - void SetCenteredText(bool c); void ConnectTimelinePoints(TimelinePoints* points); @@ -63,6 +59,10 @@ protected: virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void TimebaseChangedEvent(const rational& tb) override; + + virtual void ScaleChangedEvent(const double&); + signals: /** * @brief Signal emitted whenever the time changes on this ruler, either by user or programatically @@ -100,12 +100,6 @@ private: bool centered_text_; - double scale_; - - rational timebase_; - - double timebase_dbl_; - double timebase_flipped_dbl_; int64_t time_; From f0e6f9a5f9594dfef5961631370978019351b60e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 00:45:50 +1100 Subject: [PATCH 18/43] various: created derived class for widget derivations of TimelineScaledObject Minor code cleanup, faster than deriving both QWidget and TimelineScaledObject each time. --- app/widget/timebased/timebased.cpp | 2 +- app/widget/timebased/timebased.h | 2 +- app/widget/timelinewidget/timelinescaledobject.cpp | 5 +++++ app/widget/timelinewidget/timelinescaledobject.h | 8 ++++++++ app/widget/timeruler/timeruler.cpp | 2 +- app/widget/timeruler/timeruler.h | 2 +- 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index a23b1701f..ded2687ec 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -8,7 +8,7 @@ #include "widget/timelinewidget/undo/undo.h" TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_status_visible, QWidget *parent) : - QWidget(parent), + TimelineScaledWidget(parent), viewer_node_(nullptr), auto_max_scrollbar_(false), points_(nullptr) diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 4f24556e2..24ea72299 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -9,7 +9,7 @@ #include "widget/timelinewidget/timelinescaledobject.h" #include "widget/timeruler/timeruler.h" -class TimeBasedWidget : public QWidget, public TimelineScaledObject +class TimeBasedWidget : public TimelineScaledWidget { Q_OBJECT public: diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index d010c5129..278efb07e 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -77,3 +77,8 @@ void TimelineScaledObject::SetScale(const double& scale) ScaleChangedEvent(scale_); } + +TimelineScaledWidget::TimelineScaledWidget(QWidget *parent) : + QWidget(parent) +{ +} diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index ed6a6bcb7..26a151a6f 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -1,6 +1,8 @@ #ifndef TIMELINESCALEDOBJECT_H #define TIMELINESCALEDOBJECT_H +#include + #include "common/rational.h" class TimelineScaledObject @@ -41,4 +43,10 @@ private: }; +class TimelineScaledWidget : public QWidget, public TimelineScaledObject +{ +public: + TimelineScaledWidget(QWidget* parent = nullptr); +}; + #endif // TIMELINESCALEDOBJECT_H diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 56670d420..beb0daf5c 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -31,7 +31,7 @@ #include "core.h" TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* parent) : - QWidget(parent), + TimelineScaledWidget(parent), scroll_(0), text_visible_(text_visible), centered_text_(true), diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index b0752d656..471d1b752 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -30,7 +30,7 @@ #include "widget/timelinewidget/timelinescaledobject.h" #include "widget/timelinewidget/view/timelineplayhead.h" -class TimeRuler : public QWidget, public TimelineScaledObject +class TimeRuler : public TimelineScaledWidget { Q_OBJECT public: From 73d693f2d599d594eb856fa2d3bdf24712ba064d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 01:22:03 +1100 Subject: [PATCH 19/43] waveformview: implemented basic waveformview display --- .../timelinewidget/timelinescaledobject.h | 1 + .../view/timelineviewblockitem.cpp | 50 +------ app/widget/viewer/viewer.cpp | 10 ++ app/widget/viewer/viewer.h | 2 + app/widget/viewer/waveformview.cpp | 136 +++++++++++++++++- app/widget/viewer/waveformview.h | 28 +++- 6 files changed, 176 insertions(+), 51 deletions(-) diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index 26a151a6f..b8afc1bd1 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -45,6 +45,7 @@ private: class TimelineScaledWidget : public QWidget, public TimelineScaledObject { + Q_OBJECT public: TimelineScaledWidget(QWidget* parent = nullptr); }; diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index 7b012a583..a0792c6a9 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -29,11 +29,10 @@ #include #include -#include "audio/sumsamples.h" -#include "common/clamp.h" #include "common/qtutils.h" #include "config/config.h" #include "node/block/transition/transition.h" +#include "widget/viewer/waveformview.h" TimelineViewBlockItem::TimelineViewBlockItem(Block *block, QGraphicsItem* parent) : TimelineViewRect(parent), @@ -94,47 +93,12 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI QByteArray w = wave_file.readAll(); // FIXME: Hardcoded channel count - int channels = 2; - - const SampleSummer::Sum* samples = reinterpret_cast(w.constData()); - int nb_samples = w.size() / sizeof(SampleSummer::Sum); - - int sample_index, next_sample_index = 0; - - QVector summary; - int summary_index = -1; - - int channel_height = rect().height() / channels; - int channel_half_height = channel_height / 2; - - for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / this->GetScale()) * channels); - - if (summary_index != sample_index) { - summary = SampleSummer::ReSumSamples(&samples[sample_index], - qMax(channels, next_sample_index - sample_index), - channels); - summary_index = sample_index; - } - - int line_x = i + rect().x(); - - for (int j=0;jdrawLine(line_x, - channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), - line_x, - channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); - } - } + WaveformView::DrawWaveform(painter, + rect().toRect(), + this->GetScale(), + reinterpret_cast(w.constData()), + w.size() / sizeof(SampleSummer::Sum), + 2); wave_file.close(); } diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index a2a8230fd..747296c0b 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -70,6 +70,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : // Create scrollbar layout->addWidget(scrollbar()); connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); + connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &WaveformView::SetScroll); // Create lower controls controls_ = new PlaybackControls(); @@ -94,6 +95,8 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(video_renderer_, &VideoRenderBackend::RangeInvalidated, ruler(), &TimeRuler::CacheInvalidatedRange); audio_renderer_ = new AudioBackend(this); + waveform_view_->SetBackend(audio_renderer_); + connect(PixelFormat::instance(), &PixelFormat::FormatChanged, this, &ViewerWidget::UpdateRendererParameters); SetAutoMaxScrollBar(true); @@ -180,6 +183,13 @@ void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n) audio_renderer_->SetViewerNode(n); } +void ViewerWidget::ScaleChangedEvent(const double &s) +{ + TimeBasedWidget::ScaleChangedEvent(s); + + waveform_view_->SetScale(s); +} + void ViewerWidget::resizeEvent(QResizeEvent *event) { TimeBasedWidget::resizeEvent(event); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index a45a34691..5a702b9bc 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -114,6 +114,8 @@ protected: virtual void DisconnectNodeInternal(ViewerOutput *) override; virtual void ConnectedNodeChanged(ViewerOutput*n) override; + virtual void ScaleChangedEvent(const double& s) override; + virtual void resizeEvent(QResizeEvent *event) override; OpenGLBackend* video_renderer_; diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp index 130993efb..20508eb87 100644 --- a/app/widget/viewer/waveformview.cpp +++ b/app/widget/viewer/waveformview.cpp @@ -1,24 +1,148 @@ #include "waveformview.h" +#include #include #include +#include "common/clamp.h" + WaveformView::WaveformView(QWidget *parent) : - TimeBasedWidget(parent) + TimelineScaledWidget(parent), + backend_(nullptr) { setAutoFillBackground(true); - setStyleSheet("background: black;"); + setBackgroundRole(QPalette::Base); +} + +void WaveformView::SetBackend(AudioRenderBackend *backend) +{ + if (backend_) { + disconnect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); + } + + backend_ = backend; + + if (backend_) { + connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); + } + + update(); +} + +void WaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const SampleSummer::Sum* samples, int nb_samples, int channels) +{ + int sample_index, next_sample_index = 0; + + QVector summary; + int summary_index = -1; + + int channel_height = rect.height() / channels; + int channel_half_height = channel_height / 2; + + for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / scale) * channels); + + if (summary_index != sample_index) { + summary = SampleSummer::ReSumSamples(&samples[sample_index], + qMax(channels, next_sample_index - sample_index), + channels); + summary_index = sample_index; + } + + int line_x = i + rect.x(); + + for (int j=0;jdrawLine(line_x, + channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), + line_x, + channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); + } + } +} + +void WaveformView::SetScroll(int scroll) +{ + scroll_ = scroll; + + qDebug() << "Got scroll" << scroll_; + + update(); } void WaveformView::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); - QPainter p(this); + if (!backend_ || backend_->CachePathName().isEmpty() || !backend_->params().is_valid()) { + return; + } - p.setPen(Qt::green); + const AudioRenderingParams& params = backend_->params(); + + QFile fs(backend_->CachePathName()); + + if (fs.open(QFile::ReadOnly)) { + + QPainter p(this); + + p.setPen(Qt::green); + + int channel_height = height() / params.channel_count(); + int channel_half_height = channel_height / 2; + + int drew = 0; + + fs.seek(params.samples_to_bytes(GetSampleIndexFromPixel(0))); + + for (int x=0; x samples = SampleSummer::SumSamples(reinterpret_cast(read_buffer.constData()), + samples_len, + params.channel_count()); + + for (int i=0;i(channel_half_height), + x, + channel_mid + samples.at(i).max * static_cast(channel_half_height)); + + drew++; + } + } + + fs.close(); - for (int i=0;i(i) * 0.025) * (height() / 2)); } } + +void WaveformView::ScaleChangedEvent(const double &) +{ + update(); +} + +int WaveformView::GetSampleIndexFromPixel(int x) const +{ + qDebug() << "X was" << x << "scroll was" << scroll_ << "=" << (x + scroll_); + return qRound(static_cast(x + scroll_) * static_cast(backend_->params().sample_rate()) / GetScale()); +} diff --git a/app/widget/viewer/waveformview.h b/app/widget/viewer/waveformview.h index cc1a1c17a..1bc3b542c 100644 --- a/app/widget/viewer/waveformview.h +++ b/app/widget/viewer/waveformview.h @@ -1,16 +1,40 @@ #ifndef WAVEFORMVIEW_H #define WAVEFORMVIEW_H -#include "widget/timebased/timebased.h" +#include -class WaveformView : public TimeBasedWidget +#include "audio/sumsamples.h" +#include "render/audioparams.h" +#include "render/backend/audiorenderbackend.h" +#include "widget/timelinewidget/timelinescaledobject.h" + +class WaveformView : public TimelineScaledWidget { + Q_OBJECT public: WaveformView(QWidget* parent = nullptr); + //void SetData(const QString& file, const AudioRenderingParams& params); + + void SetBackend(AudioRenderBackend* backend); + + static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const SampleSummer::Sum *samples, int nb_samples, int channels); + +public slots: + void SetScroll(int scroll); + protected: virtual void paintEvent(QPaintEvent* event) override; + virtual void ScaleChangedEvent(const double& s) override; + +private: + int GetSampleIndexFromPixel(int x) const; + + AudioRenderBackend* backend_; + + int scroll_; + }; #endif // WAVEFORMVIEW_H From 7f2dcc8200a50e06ac652ed7387a37b370b330d1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 01:57:40 +1100 Subject: [PATCH 20/43] waveformview: made UI seekable --- app/render/backend/audiorenderbackend.cpp | 2 + app/render/backend/audiorenderbackend.h | 3 + .../timelinewidget/view/timelineplayhead.cpp | 8 +- .../timelinewidget/view/timelineplayhead.h | 8 +- app/widget/timeruler/CMakeLists.txt | 2 + app/widget/timeruler/seekablewidget.cpp | 110 +++++++++++++++++ app/widget/timeruler/seekablewidget.h | 72 +++++++++++ app/widget/timeruler/timeruler.cpp | 112 ++---------------- app/widget/timeruler/timeruler.h | 47 +------- app/widget/viewer/viewer.cpp | 2 + app/widget/viewer/waveformview.cpp | 38 +++--- app/widget/viewer/waveformview.h | 14 +-- 12 files changed, 233 insertions(+), 185 deletions(-) create mode 100644 app/widget/timeruler/seekablewidget.cpp create mode 100644 app/widget/timeruler/seekablewidget.h diff --git a/app/render/backend/audiorenderbackend.cpp b/app/render/backend/audiorenderbackend.cpp index 5baf68c17..2200328c7 100644 --- a/app/render/backend/audiorenderbackend.cpp +++ b/app/render/backend/audiorenderbackend.cpp @@ -27,6 +27,8 @@ void AudioRenderBackend::SetParameters(const AudioRenderingParams ¶ms) // Regenerate the cache ID RegenerateCacheID(); + + emit ParamsChanged(); } void AudioRenderBackend::ConnectViewer(ViewerOutput *node) diff --git a/app/render/backend/audiorenderbackend.h b/app/render/backend/audiorenderbackend.h index 775d7991a..897efa8d7 100644 --- a/app/render/backend/audiorenderbackend.h +++ b/app/render/backend/audiorenderbackend.h @@ -24,6 +24,9 @@ public: QString CachePathName(); +signals: + void ParamsChanged(); + protected: virtual void ConnectViewer(ViewerOutput* node) override; diff --git a/app/widget/timelinewidget/view/timelineplayhead.cpp b/app/widget/timelinewidget/view/timelineplayhead.cpp index 0b9ea95a9..f15e9cf2d 100644 --- a/app/widget/timelinewidget/view/timelineplayhead.cpp +++ b/app/widget/timelinewidget/view/timelineplayhead.cpp @@ -22,12 +22,12 @@ #include -const QColor &TimelinePlayhead::PlayheadColor() const +const QColor &TimelinePlayhead::GetPlayheadColor() const { return playhead_color_; } -const QColor &TimelinePlayhead::PlayheadHighlightColor() const +const QColor &TimelinePlayhead::GetPlayheadHighlightColor() const { return playhead_highlight_color_; } @@ -45,10 +45,10 @@ void TimelinePlayhead::SetPlayheadHighlightColor(QColor c) void TimelinePlayhead::Draw(QPainter* painter, const QRectF& playhead_rect) const { painter->setPen(Qt::NoPen); - painter->setBrush(PlayheadHighlightColor()); + painter->setBrush(GetPlayheadHighlightColor()); painter->drawRect(playhead_rect); - painter->setPen(PlayheadColor()); + painter->setPen(GetPlayheadColor()); painter->setBrush(Qt::NoBrush); painter->drawLine(QLineF(playhead_rect.topLeft(), playhead_rect.bottomLeft())); } diff --git a/app/widget/timelinewidget/view/timelineplayhead.h b/app/widget/timelinewidget/view/timelineplayhead.h index bbdf1f934..15b2f0031 100644 --- a/app/widget/timelinewidget/view/timelineplayhead.h +++ b/app/widget/timelinewidget/view/timelineplayhead.h @@ -31,13 +31,13 @@ class TimelinePlayhead : public QWidget { Q_OBJECT - Q_PROPERTY(QColor playheadColor READ PlayheadColor WRITE SetPlayheadColor DESIGNABLE true) - Q_PROPERTY(QColor playheadHighlightColor READ PlayheadHighlightColor WRITE SetPlayheadHighlightColor DESIGNABLE true) + Q_PROPERTY(QColor playheadColor READ GetPlayheadColor WRITE SetPlayheadColor DESIGNABLE true) + Q_PROPERTY(QColor playheadHighlightColor READ GetPlayheadHighlightColor WRITE SetPlayheadHighlightColor DESIGNABLE true) public: TimelinePlayhead() = default; - const QColor& PlayheadColor() const; - const QColor& PlayheadHighlightColor() const; + const QColor& GetPlayheadColor() const; + const QColor& GetPlayheadHighlightColor() const; void SetPlayheadColor(QColor c); void SetPlayheadHighlightColor(QColor c); diff --git a/app/widget/timeruler/CMakeLists.txt b/app/widget/timeruler/CMakeLists.txt index 9fe06158f..9c7882713 100644 --- a/app/widget/timeruler/CMakeLists.txt +++ b/app/widget/timeruler/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/timeruler/seekablewidget.h + widget/timeruler/seekablewidget.cpp widget/timeruler/timeruler.h widget/timeruler/timeruler.cpp PARENT_SCOPE diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp new file mode 100644 index 000000000..b24d90ae2 --- /dev/null +++ b/app/widget/timeruler/seekablewidget.cpp @@ -0,0 +1,110 @@ +#include "seekablewidget.h" + +#include +#include + +SeekableWidget::SeekableWidget(QWidget* parent) : + TimelineScaledWidget(parent), + time_(0), + timeline_points_(nullptr), + scroll_(0) +{ + +} + +void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) +{ + if (timeline_points_) { + disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); + } + + timeline_points_ = points; + + if (timeline_points_) { + connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); + } + + update(); +} + +const int64_t &SeekableWidget::GetTime() const +{ + return time_; +} + +const int &SeekableWidget::GetScroll() const +{ + return scroll_; +} + +void SeekableWidget::mousePressEvent(QMouseEvent *event) +{ + SeekToScreenPoint(event->pos().x()); +} + +void SeekableWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) { + SeekToScreenPoint(event->pos().x()); + } +} + +void SeekableWidget::ScaleChangedEvent(const double &) +{ + update(); +} + +TimelinePoints *SeekableWidget::timeline_points() const +{ + return timeline_points_; +} + +void SeekableWidget::SetTime(const int64_t &r) +{ + time_ = r; + + update(); +} + +void SeekableWidget::SetScroll(int s) +{ + scroll_ = s; + + update(); +} + +double SeekableWidget::ScreenToUnitFloat(int screen) +{ + return (screen + scroll_) / GetScale() / timebase_dbl(); +} + +int64_t SeekableWidget::ScreenToUnit(int screen) +{ + return qFloor(ScreenToUnitFloat(screen)); +} + +int64_t SeekableWidget::ScreenToUnitRounded(int screen) +{ + return qRound64(ScreenToUnitFloat(screen)); +} + +int SeekableWidget::UnitToScreen(int64_t unit) +{ + return qFloor(static_cast(unit) * GetScale() * timebase_dbl()) - scroll_; +} + +int SeekableWidget::TimeToScreen(const rational &time) +{ + return qFloor(time.toDouble() * GetScale()) - scroll_; +} + +void SeekableWidget::SeekToScreenPoint(int screen) +{ + int64_t timestamp = qMax(static_cast(0), ScreenToUnitRounded(screen)); + + SetTime(timestamp); + + emit TimeChanged(timestamp); +} diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h new file mode 100644 index 000000000..9c79d20e6 --- /dev/null +++ b/app/widget/timeruler/seekablewidget.h @@ -0,0 +1,72 @@ +#ifndef SEEKABLEWIDGET_H +#define SEEKABLEWIDGET_H + +#include "common/rational.h" +#include "timeline/timelinepoints.h" +#include "widget/timelinewidget/view/timelineplayhead.h" +#include "widget/timelinewidget/timelinescaledobject.h" + +class SeekableWidget : public TimelineScaledWidget +{ + Q_OBJECT +public: + SeekableWidget(QWidget *parent = nullptr); + + const int64_t& GetTime() const; + + const int& GetScroll() const; + + void ConnectTimelinePoints(TimelinePoints* points); + +public slots: + void SetTime(const int64_t &r); + + void SetScroll(int s); + +protected: + void SeekToScreenPoint(int screen); + + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + + virtual void ScaleChangedEvent(const double&) override; + + TimelinePoints* timeline_points() const; + + double ScreenToUnitFloat(int screen); + + int64_t ScreenToUnit(int screen); + int64_t ScreenToUnitRounded(int screen); + + int UnitToScreen(int64_t unit); + + int TimeToScreen(const rational& time); + + inline const QColor& GetPlayheadColor() const + { + return style_.GetPlayheadColor(); + } + + inline const QColor& GetPlayheadHighlightColor() const + { + return style_.GetPlayheadHighlightColor(); + } + +signals: + /** + * @brief Signal emitted whenever the time changes on this ruler, either by user or programatically + */ + void TimeChanged(int64_t); + +private: + int64_t time_; + + TimelinePlayhead style_; + + TimelinePoints* timeline_points_; + + int scroll_; + +}; + +#endif // SEEKABLEWIDGET_H diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index beb0daf5c..25df8bdfe 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -21,9 +21,7 @@ #include "timeruler.h" #include -#include #include -#include #include "common/timecodefunctions.h" #include "common/qtutils.h" @@ -31,13 +29,10 @@ #include "core.h" TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* parent) : - TimelineScaledWidget(parent), - scroll_(0), + SeekableWidget(parent), text_visible_(text_visible), centered_text_(true), - time_(0), - show_cache_status_(cache_status_visible), - timeline_points_(nullptr) + show_cache_status_(cache_status_visible) { QFontMetrics fm = fontMetrics(); @@ -58,28 +53,6 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare UpdateHeight(); } -void TimeRuler::ConnectTimelinePoints(TimelinePoints *points) -{ - if (timeline_points_) { - disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, &TimeRuler::TimelineWorkareaChanged); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, &TimeRuler::TimelineWorkareaChanged); - } - - timeline_points_ = points; - - if (timeline_points_) { - connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, &TimeRuler::TimelineWorkareaChanged); - connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, &TimeRuler::TimelineWorkareaChanged); - } - - update(); -} - -const int64_t &TimeRuler::GetTime() -{ - return time_; -} - void TimeRuler::SetCacheStatusLength(const rational &length) { if (show_cache_status_) { @@ -91,20 +64,6 @@ void TimeRuler::SetCacheStatusLength(const rational &length) } } -void TimeRuler::SetTime(const int64_t &r) -{ - time_ = r; - - update(); -} - -void TimeRuler::SetScroll(int s) -{ - scroll_ = s; - - update(); -} - void TimeRuler::CacheInvalidatedRange(const TimeRange& range) { if (show_cache_status_) { @@ -133,15 +92,15 @@ void TimeRuler::paintEvent(QPaintEvent *) QPainter p(this); // Draw timeline points if connected - if (timeline_points_) { - if (timeline_points_->workarea()->enabled()) { - int workarea_left = qMax(0, TimeToScreen(timeline_points_->workarea()->in())); + if (timeline_points()) { + if (timeline_points()->workarea()->enabled()) { + int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); int workarea_right; - if (timeline_points_->workarea()->out() == TimelineWorkArea::kResetOut) { + if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { workarea_right = width(); } else { - workarea_right = qMin(width(), TimeToScreen(timeline_points_->workarea()->out())); + workarea_right = qMin(width(), TimeToScreen(timeline_points()->workarea()->out())); } p.fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); @@ -239,7 +198,7 @@ void TimeRuler::paintEvent(QPaintEvent *) const int kAverageTextWidth = 200; for (int i=-kAverageTextWidth;i(i + scroll_); + double screen_pt = static_cast(i + GetScroll()); if (long_interval > -1) { int this_long_unit = qFloor(screen_pt/long_interval); @@ -318,26 +277,14 @@ void TimeRuler::paintEvent(QPaintEvent *) } // Draw the playhead if it's on screen at the moment - int playhead_pos = UnitToScreen(time_); + int playhead_pos = UnitToScreen(GetTime()); if (playhead_pos + playhead_width_ >= 0 && playhead_pos - playhead_width_ < width()) { p.setPen(Qt::NoPen); - p.setBrush(style_.PlayheadColor()); + p.setBrush(GetPlayheadColor()); DrawPlayhead(&p, playhead_pos, line_bottom); } } -void TimeRuler::mousePressEvent(QMouseEvent *event) -{ - SeekToScreenPoint(event->pos().x()); -} - -void TimeRuler::mouseMoveEvent(QMouseEvent *event) -{ - if (event->buttons() & Qt::LeftButton) { - SeekToScreenPoint(event->pos().x()); - } -} - void TimeRuler::TimebaseChangedEvent(const rational &tb) { timebase_flipped_dbl_ = tb.flipped().toDouble(); @@ -345,11 +292,6 @@ void TimeRuler::TimebaseChangedEvent(const rational &tb) update(); } -void TimeRuler::ScaleChangedEvent(const double &) -{ - update(); -} - void TimeRuler::DrawPlayhead(QPainter *p, int x, int y) { p->setRenderHint(QPainter::Antialiasing); @@ -374,40 +316,6 @@ int TimeRuler::CacheStatusHeight() const return fontMetrics().height() / 4; } -double TimeRuler::ScreenToUnitFloat(int screen) -{ - return (screen + scroll_) / GetScale() / timebase_dbl(); -} - -int64_t TimeRuler::ScreenToUnit(int screen) -{ - return qFloor(ScreenToUnitFloat(screen)); -} - -int TimeRuler::UnitToScreen(int64_t unit) -{ - return qFloor(static_cast(unit) * GetScale() * timebase_dbl()) - scroll_; -} - -int TimeRuler::TimeToScreen(const rational &time) -{ - return qFloor(time.toDouble() * GetScale()) - scroll_; -} - -void TimeRuler::SeekToScreenPoint(int screen) -{ - int64_t timestamp = qMax(0, qRound(ScreenToUnitFloat(screen))); - - SetTime(timestamp); - - emit TimeChanged(timestamp); -} - -void TimeRuler::TimelineWorkareaChanged() -{ - update(); -} - void TimeRuler::UpdateHeight() { int height = text_height_; diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index 471d1b752..f926d59d3 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -24,13 +24,10 @@ #include #include -#include "common/rational.h" #include "common/timerange.h" -#include "timeline/timelinepoints.h" -#include "widget/timelinewidget/timelinescaledobject.h" -#include "widget/timelinewidget/view/timelineplayhead.h" +#include "seekablewidget.h" -class TimeRuler : public TimelineScaledWidget +class TimeRuler : public SeekableWidget { Q_OBJECT public: @@ -38,15 +35,7 @@ public: void SetCenteredText(bool c); - void ConnectTimelinePoints(TimelinePoints* points); - - const int64_t& GetTime(); - public slots: - void SetTime(const int64_t &r); - - void SetScroll(int s); - void CacheInvalidatedRange(const TimeRange &range); void CacheTimeReady(const rational& time); @@ -56,19 +45,8 @@ public slots: protected: virtual void paintEvent(QPaintEvent* e) override; - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseMoveEvent(QMouseEvent *event) override; - virtual void TimebaseChangedEvent(const rational& tb) override; - virtual void ScaleChangedEvent(const double&); - -signals: - /** - * @brief Signal emitted whenever the time changes on this ruler, either by user or programatically - */ - void TimeChanged(int64_t); - private: void UpdateHeight(); @@ -76,16 +54,6 @@ private: int CacheStatusHeight() const; - double ScreenToUnitFloat(int screen); - - int64_t ScreenToUnit(int screen); - - int UnitToScreen(int64_t unit); - - int TimeToScreen(const rational& time); - - void SeekToScreenPoint(int screen); - int text_height_; int cache_status_height_; @@ -94,29 +62,18 @@ private: int playhead_width_; - int scroll_; - bool text_visible_; bool centered_text_; double timebase_flipped_dbl_; - int64_t time_; - - TimelinePlayhead style_; - bool show_cache_status_; rational cache_length_; TimeRangeList dirty_cache_ranges_; - TimelinePoints* timeline_points_; - -private slots: - void TimelineWorkareaChanged(); - }; #endif // TIMERULER_H diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 747296c0b..f8c329237 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -96,6 +96,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : audio_renderer_ = new AudioBackend(this); waveform_view_->SetBackend(audio_renderer_); + connect(waveform_view_, &WaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); connect(PixelFormat::instance(), &PixelFormat::FormatChanged, this, &ViewerWidget::UpdateRendererParameters); @@ -109,6 +110,7 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) } controls_->SetTime(i); + waveform_view_->SetTime(i); if (GetConnectedNode() && last_time_ != i) { rational time_set = Timecode::timestamp_to_time(i, timebase()); diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp index 20508eb87..a0f26c03b 100644 --- a/app/widget/viewer/waveformview.cpp +++ b/app/widget/viewer/waveformview.cpp @@ -7,7 +7,7 @@ #include "common/clamp.h" WaveformView::WaveformView(QWidget *parent) : - TimelineScaledWidget(parent), + SeekableWidget(parent), backend_(nullptr) { setAutoFillBackground(true); @@ -18,12 +18,18 @@ void WaveformView::SetBackend(AudioRenderBackend *backend) { if (backend_) { disconnect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); + disconnect(backend_, &AudioRenderBackend::ParamsChanged, this, &WaveformView::BackendParamsChanged); + + SetTimebase(0); } backend_ = backend; if (backend_) { connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); + connect(backend_, &AudioRenderBackend::ParamsChanged, this, &WaveformView::BackendParamsChanged); + + SetTimebase(rational(1, backend_->params().sample_rate())); } update(); @@ -69,15 +75,6 @@ void WaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const doub } } -void WaveformView::SetScroll(int scroll) -{ - scroll_ = scroll; - - qDebug() << "Got scroll" << scroll_; - - update(); -} - void WaveformView::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); @@ -94,6 +91,7 @@ void WaveformView::paintEvent(QPaintEvent *event) QPainter p(this); + // FIXME: Hardcoded color p.setPen(Qt::green); int channel_height = height() / params.channel_count(); @@ -101,11 +99,11 @@ void WaveformView::paintEvent(QPaintEvent *event) int drew = 0; - fs.seek(params.samples_to_bytes(GetSampleIndexFromPixel(0))); + fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); for (int x=0; x Date: Sat, 14 Mar 2020 22:34:23 +1100 Subject: [PATCH 21/43] timelinescaledobject: implemented minimum scale Used by Viewer to keep TimeRuler and WaveformView within restrictions. --- app/widget/timelinewidget/timelinescaledobject.cpp | 14 +++++++++++++- app/widget/timelinewidget/timelinescaledobject.h | 4 ++++ app/widget/viewer/viewer.cpp | 12 ++++++++++++ app/widget/viewer/viewer.h | 2 ++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index 278efb07e..cd0fb9bcc 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -3,8 +3,11 @@ #include #include +#include "common/clamp.h" + TimelineScaledObject::TimelineScaledObject() : scale_(1.0), + min_scale_(0), max_scale_(DBL_MAX) { @@ -64,6 +67,15 @@ void TimelineScaledObject::SetMaximumScale(const double &max) } } +void TimelineScaledObject::SetMinimumScale(const double &min) +{ + min_scale_ = min; + + if (GetScale() < min_scale_) { + SetScale(min_scale_); + } +} + const double& TimelineScaledObject::GetScale() const { return scale_; @@ -73,7 +85,7 @@ void TimelineScaledObject::SetScale(const double& scale) { Q_ASSERT(scale > 0); - scale_ = qMin(scale, max_scale_); + scale_ = clamp(scale, min_scale_, max_scale_); ScaleChangedEvent(scale_); } diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index b8afc1bd1..0889064c8 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -32,6 +32,8 @@ protected: void SetMaximumScale(const double& max); + void SetMinimumScale(const double& min); + private: rational timebase_; @@ -39,6 +41,8 @@ private: double scale_; + double min_scale_; + double max_scale_; }; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index f8c329237..742690c2e 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -202,6 +202,8 @@ void ViewerWidget::resizeEvent(QResizeEvent *event) UpdateRendererParameters(); } + + UpdateMinimumScale(); } void ViewerWidget::TogglePlayPause() @@ -324,6 +326,15 @@ int ViewerWidget::CalculateDivider() return divider_; } +void ViewerWidget::UpdateMinimumScale() +{ + if (!GetConnectedNode()) { + return; + } + + SetMinimumScale(static_cast(ruler()->width()) / GetConnectedNode()->Length().toDouble()); +} + void ViewerWidget::UpdateStack() { if (!GetConnectedNode() || GetConnectedNode()->texture_input()->IsConnected()) { @@ -554,6 +565,7 @@ void ViewerWidget::LengthChangedSlot(const rational &length) { controls_->SetEndTime(Timecode::time_to_timestamp(length, timebase())); ruler()->SetCacheStatusLength(length); + UpdateMinimumScale(); } void ViewerWidget::ColorDisplayChanged(QAction* action) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 5a702b9bc..88aab38b7 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -134,6 +134,8 @@ private: int CalculateDivider(); + void UpdateMinimumScale(); + QStackedWidget* stack_; ViewerSizer* sizer_; From 1c4411a4ad80a99fce17649475c59beb397f00d7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 23:03:09 +1100 Subject: [PATCH 22/43] timebasedwidget/timeruler: reimplemented creating and displaying markers --- app/config/config.cpp | 1 + app/panel/timebased/timebased.cpp | 5 +++ app/panel/timebased/timebased.h | 2 ++ app/widget/panel/panel.h | 2 ++ app/widget/timebased/timebased.cpp | 22 +++++++++++++ app/widget/timebased/timebased.h | 2 ++ app/widget/timeruler/timeruler.cpp | 52 ++++++++++++++++++++++++++++++ app/window/mainwindow/mainmenu.cpp | 7 +++- app/window/mainwindow/mainmenu.h | 2 ++ 9 files changed, 94 insertions(+), 1 deletion(-) diff --git a/app/config/config.cpp b/app/config/config.cpp index 0d22fc54e..b294f5909 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -75,6 +75,7 @@ void Config::SetDefaults() config_map_["Autoscroll"] = AutoScroll::kPage; config_map_["DefaultViewerDivider"] = 2; config_map_["AutoSelectDivider"] = false; + config_map_["SetNameWithMarker"] = false; config_map_["DropWithoutSequenceBehavior"] = TimelineWidget::kDWSAsk; config_map_["DiskCachePath"] = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index d74b1cd61..e0e93e7a7 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -159,3 +159,8 @@ void TimeBasedPanel::ClearInOut() { GetTimeBasedWidget()->ClearInOutPoints(); } + +void TimeBasedPanel::SetMarker() +{ + GetTimeBasedWidget()->SetMarker(); +} diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index 555821539..db4c7f9c5 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -54,6 +54,8 @@ public: virtual void ClearInOut() override; + virtual void SetMarker() override; + public slots: void SetTimebase(const rational& timebase); diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index d81df1be8..0ea111e45 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -128,6 +128,8 @@ public: virtual void ClearInOut(){} + virtual void SetMarker(){} + protected: /** * @brief paintEvent diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index ded2687ec..816ba0f20 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -1,8 +1,10 @@ #include "timebased.h" +#include #include #include "common/timecodefunctions.h" +#include "config/config.h" #include "core.h" #include "project/item/sequence/sequence.h" #include "widget/timelinewidget/undo/undo.h" @@ -366,3 +368,23 @@ void TimeBasedWidget::ClearInOutPoints() Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(points_, false)); } + +void TimeBasedWidget::SetMarker() +{ + if (!points_) { + return; + } + + bool ok; + QString marker_name; + + if (Config::Current()["SetNameWithMarker"].toBool()) { + marker_name = QInputDialog::getText(this, tr("Set Marker"), tr("Marker name:"), QLineEdit::Normal, QString(), &ok); + } else { + ok = true; + } + + if (ok) { + points_->markers()->AddMarker(TimeRange(GetTime(), GetTime()), marker_name); + } +} diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 24ea72299..6a58ba12b 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -59,6 +59,8 @@ public slots: void ClearInOutPoints(); + void SetMarker(); + TimeRuler* ruler() const; protected slots: diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 25df8bdfe..6cc411854 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -93,6 +93,8 @@ void TimeRuler::paintEvent(QPaintEvent *) // Draw timeline points if connected if (timeline_points()) { + + // Draw in/out workarea if (timeline_points()->workarea()->enabled()) { int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); int workarea_right; @@ -105,6 +107,51 @@ void TimeRuler::paintEvent(QPaintEvent *) p.fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); } + + // Draw markers + if (!timeline_points()->markers()->list().isEmpty()) { + int marker_bottom = height() - text_height_; + + if (show_cache_status_) { + marker_bottom -= cache_status_height_; + } + + if (text_visible_) { + marker_bottom -= cache_status_height_; + } + + int marker_top = marker_bottom - text_height_; + + // FIXME: Hardcoded marker colors + p.setPen(Qt::black); + p.setBrush(Qt::green); + + foreach (TimelineMarker* marker, timeline_points()->markers()->list()) { + int marker_left = TimeToScreen(marker->time().in()); + int marker_right = TimeToScreen(marker->time().out()); + + if (marker_left >= width() || marker_right < 0) { + continue; + } + + if (marker->time().length() == 0) { + // Single point in time marker + DrawPlayhead(&p, marker_left, marker_bottom); + } else { + // Marker range + int rect_left = qMax(0, marker_left); + int rect_right = qMin(width(), marker_right); + + QRect marker_rect(rect_left, marker_top, rect_right - rect_left, marker_bottom - marker_top); + + p.drawRect(marker_rect); + + if (!marker->name().isEmpty()) { + p.drawText(marker_rect, marker->name()); + } + } + } + } } double width_of_frame = timebase_dbl() * GetScale(); @@ -320,13 +367,18 @@ void TimeRuler::UpdateHeight() { int height = text_height_; + // Add text height if (text_visible_) { height += text_height_; } + // Add cache status height if (show_cache_status_) { height += cache_status_height_; } + // Add marker height + height += text_height_; + setFixedHeight(height); } diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 429a63282..95c4eba68 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -87,7 +87,7 @@ MainMenu::MainMenu(QMainWindow *parent) : edit_delete_inout_item_ = edit_menu_->AddItem("deleteinout", nullptr, nullptr, ";"); edit_ripple_delete_inout_item_ = edit_menu_->AddItem("rippledeleteinout", nullptr, nullptr, "'"); edit_menu_->addSeparator(); - edit_set_marker_item_ = edit_menu_->AddItem("marker", nullptr, nullptr, "M"); + edit_set_marker_item_ = edit_menu_->AddItem("marker", this, SLOT(SetMarkerTriggered()), "M"); // // VIEW MENU @@ -507,6 +507,11 @@ void MainMenu::GoToNextCutTriggered() PanelManager::instance()->CurrentlyFocused()->GoToNextCut(); } +void MainMenu::SetMarkerTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->SetMarker(); +} + void MainMenu::Retranslate() { // MenuShared is not a QWidget and therefore does not receive a LanguageEvent, we use MainMenu's to update it diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index 81476ab2d..2f4eea741 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -140,6 +140,8 @@ private slots: void GoToPrevCutTriggered(); void GoToNextCutTriggered(); + void SetMarkerTriggered(); + private: /** * @brief Set strings based on the current application language. From 78baf7661ff6ef451f918d0e78745b9c350863d2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 14 Mar 2020 23:27:41 +1100 Subject: [PATCH 23/43] audiorenderer: split audio rendering up into chunks for better parallelism --- app/render/backend/audiorenderbackend.cpp | 12 ++++++++++++ app/render/backend/audiorenderbackend.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/app/render/backend/audiorenderbackend.cpp b/app/render/backend/audiorenderbackend.cpp index 2200328c7..70dec1bb2 100644 --- a/app/render/backend/audiorenderbackend.cpp +++ b/app/render/backend/audiorenderbackend.cpp @@ -100,6 +100,18 @@ void AudioRenderBackend::ConnectWorkerToThis(RenderWorker *worker) connect(arw, &AudioRenderWorker::ConformUnavailable, this, &AudioRenderBackend::ConformUnavailable, Qt::QueuedConnection); } +TimeRange AudioRenderBackend::PopNextFrameFromQueue() +{ + TimeRange range = cache_queue_.first(); + + // Limit range per worker to 2 seconds (FIXME: arbitrary, should be tweaked, maybe even in config?) + range.set_out(qMin(range.out(), range.in() + rational(2))); + + cache_queue_.RemoveTimeRange(range); + + return range; +} + void AudioRenderBackend::ConformUnavailable(StreamPtr stream, const TimeRange &range, const rational &stream_time, const AudioRenderingParams& params) { ConformWaitInfo info = {stream, params, range, stream_time}; diff --git a/app/render/backend/audiorenderbackend.h b/app/render/backend/audiorenderbackend.h index 897efa8d7..5724e4e16 100644 --- a/app/render/backend/audiorenderbackend.h +++ b/app/render/backend/audiorenderbackend.h @@ -47,6 +47,8 @@ protected: virtual void ConnectWorkerToThis(RenderWorker* worker) override; + virtual TimeRange PopNextFrameFromQueue() override; + QHash copy_map_; private: From b57b091ca4ad9e0cefb430d06a9cd71b25dc8b01 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 13:15:36 +1100 Subject: [PATCH 24/43] timeline: auto-unlink blocks when they're deleted --- app/node/block/block.cpp | 20 ++++++-- app/node/block/block.h | 3 +- app/widget/timelinewidget/timelinewidget.cpp | 2 + app/widget/timelinewidget/undo/undo.cpp | 49 ++++++++++++++++++++ app/widget/timelinewidget/undo/undo.h | 30 ++++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 312f10103..6228dcec2 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -204,11 +204,12 @@ void Block::LengthInputChanged() void Block::Link(Block *a, Block *b) { - if (a == b || a == nullptr || b == nullptr) { + if (a == b || !a || !b) { return; } - // Assume both clips are already linked since Link() and Unlink() should be the only entry points to this array + // Prevent duplicate link entries (assume that we only need to check one clip since this should be the only function + // that adds to the linked array) if (a->linked_clips_.contains(b)) { return; } @@ -217,7 +218,7 @@ void Block::Link(Block *a, Block *b) b->linked_clips_.append(a); } -void Block::Link(QList blocks) +void Block::Link(const QList& blocks) { foreach (Block* a, blocks) { foreach (Block* b, blocks) { @@ -228,10 +229,23 @@ void Block::Link(QList blocks) void Block::Unlink(Block *a, Block *b) { + if (a == b || !a || !b) { + return; + } + a->linked_clips_.removeOne(b); b->linked_clips_.removeOne(a); } +void Block::Unlink(const QList &blocks) +{ + foreach (Block* a, blocks) { + foreach (Block* b, blocks) { + Unlink(a, b); + } + } +} + bool Block::AreLinked(Block *a, Block *b) { return a->linked_clips_.contains(b); diff --git a/app/node/block/block.h b/app/node/block/block.h index e0e43f1fe..3cf16d46e 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -77,8 +77,9 @@ public: void set_block_name(const QString& name); static void Link(Block* a, Block* b); - static void Link(QList blocks); + static void Link(const QList& blocks); static void Unlink(Block* a, Block* b); + static void Unlink(const QList& blocks); static bool AreLinked(Block* a, Block* b); const QVector& linked_clips(); bool HasLinks(); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 2385b9289..28359e0fc 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -380,6 +380,8 @@ void TimelineWidget::DeleteSelectedInternal(const QList &blocks, } if (remove_from_graph) { + new BlockUnlinkAllCommand(b, command); + new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); } } diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 2a7dfae1b..4eb3e66af 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -715,3 +715,52 @@ void WorkareaSetRangeCommand::undo_internal() { points_->workarea()->set_range(old_range_); } + +BlockLinkCommand::BlockLinkCommand(const QList &blocks, bool link, QUndoCommand *parent) : + UndoCommand(parent), + blocks_(blocks), + link_(link) +{ +} + +void BlockLinkCommand::redo_internal() +{ + if (link_) { + Block::Link(blocks_); + } else { + Block::Unlink(blocks_); + } +} + +void BlockLinkCommand::undo_internal() +{ + if (link_) { + Block::Unlink(blocks_); + } else { + Block::Link(blocks_); + } +} + +BlockUnlinkAllCommand::BlockUnlinkAllCommand(Block *block, QUndoCommand *parent) : + UndoCommand(parent), + block_(block) +{ +} + +void BlockUnlinkAllCommand::redo_internal() +{ + unlinked_ = block_->linked_clips(); + + foreach (Block* link, unlinked_) { + Block::Unlink(block_, link); + } +} + +void BlockUnlinkAllCommand::undo_internal() +{ + foreach (Block* link, unlinked_) { + Block::Link(block_, link); + } + + unlinked_.clear(); +} diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index d8afe6d60..ce2d82285 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -331,4 +331,34 @@ private: }; +class BlockLinkCommand : public UndoCommand { +public: + BlockLinkCommand(const QList& blocks, bool link, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + QList blocks_; + + bool link_; + +}; + +class BlockUnlinkAllCommand : public UndoCommand { +public: + BlockUnlinkAllCommand(Block* block, QUndoCommand* parent = nullptr); + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + Block* block_; + + QVector unlinked_; + +}; + #endif // TIMELINEUNDOABLE_H From 02e7c68104c7bceb45aa8cc5a47d8dfea52d4222 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 13:41:46 +1100 Subject: [PATCH 25/43] timeline: reimplemented linking/unlinking functionality --- app/node/block/block.cpp | 26 ++++++++++++---- app/node/block/block.h | 6 ++-- app/panel/timeline/timeline.cpp | 5 ++++ app/panel/timeline/timeline.h | 2 ++ app/widget/menu/menushared.cpp | 7 ++++- app/widget/menu/menushared.h | 2 ++ app/widget/panel/panel.h | 2 ++ app/widget/timelinewidget/timelinewidget.cpp | 25 ++++++++++++++++ app/widget/timelinewidget/timelinewidget.h | 2 ++ app/widget/timelinewidget/undo/undo.cpp | 31 +++++++++++++++----- app/widget/timelinewidget/undo/undo.h | 13 ++++++-- 11 files changed, 102 insertions(+), 19 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 6228dcec2..69cda6126 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -202,20 +202,25 @@ void Block::LengthInputChanged() emit LengthChanged(length()); } -void Block::Link(Block *a, Block *b) +bool Block::Link(Block *a, Block *b) { if (a == b || !a || !b) { - return; + return false; } // Prevent duplicate link entries (assume that we only need to check one clip since this should be the only function // that adds to the linked array) - if (a->linked_clips_.contains(b)) { - return; + if (Block::AreLinked(a, b)) { + return false; } a->linked_clips_.append(b); b->linked_clips_.append(a); + + emit a->LinksChanged(); + emit b->LinksChanged(); + + return true; } void Block::Link(const QList& blocks) @@ -227,14 +232,23 @@ void Block::Link(const QList& blocks) } } -void Block::Unlink(Block *a, Block *b) +bool Block::Unlink(Block *a, Block *b) { if (a == b || !a || !b) { - return; + return false; + } + + if (!Block::AreLinked(a, b)) { + return false; } a->linked_clips_.removeOne(b); b->linked_clips_.removeOne(a); + + emit a->LinksChanged(); + emit b->LinksChanged(); + + return true; } void Block::Unlink(const QList &blocks) diff --git a/app/node/block/block.h b/app/node/block/block.h index 3cf16d46e..cb9ea4e93 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -76,9 +76,9 @@ public: QString block_name() const; void set_block_name(const QString& name); - static void Link(Block* a, Block* b); + static bool Link(Block* a, Block* b); static void Link(const QList& blocks); - static void Unlink(Block* a, Block* b); + static bool Unlink(Block* a, Block* b); static void Unlink(const QList& blocks); static bool AreLinked(Block* a, Block* b); const QVector& linked_clips(); @@ -104,6 +104,8 @@ signals: void LengthChanged(const rational& length); + void LinksChanged(); + protected: rational SequenceToMediaTime(const rational& sequence_time) const; diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 95dfdbb7d..53fd6116c 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -115,6 +115,11 @@ void TimelinePanel::Overwrite() } } +void TimelinePanel::ToggleLinks() +{ + static_cast(GetTimeBasedWidget())->ToggleLinksOnSelected(); +} + void TimelinePanel::InsertFootageAtPlayhead(const QList &footage) { static_cast(GetTimeBasedWidget())->InsertFootageAtPlayhead(footage); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 02c09d1e5..3d5c1dd91 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -61,6 +61,8 @@ public: virtual void Overwrite() override; + virtual void ToggleLinks() override; + void InsertFootageAtPlayhead(const QList &footage); void OverwriteFootageAtPlayhead(const QList &footage); diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 84419b8cc..d128017ac 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -52,7 +52,7 @@ MenuShared::MenuShared() // "Clip Edit" menu shared items clip_add_default_transition_item_ = Menu::CreateItem(this, "deftransition", nullptr, nullptr, "Ctrl+Shift+D"); - clip_link_unlink_item_ = Menu::CreateItem(this, "linkunlink", nullptr, nullptr, "Ctrl+L"); + clip_link_unlink_item_ = Menu::CreateItem(this, "linkunlink", this, SLOT(ToggleLinksTriggered()), "Ctrl+L"); clip_enable_disable_item_ = Menu::CreateItem(this, "enabledisable", nullptr, nullptr, "Shift+E"); clip_nest_item_ = Menu::CreateItem(this, "nest", nullptr, nullptr); @@ -156,6 +156,11 @@ void MenuShared::ClearInOutTriggered() PanelManager::instance()->CurrentlyFocused()->ClearInOut(); } +void MenuShared::ToggleLinksTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->ToggleLinks(); +} + void MenuShared::Retranslate() { // "New" menu shared items diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 6269aa2bb..d15408f57 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -91,6 +91,8 @@ private slots: void ClearInOutTriggered(); + void ToggleLinksTriggered(); + }; #endif // MENUSHARED_H diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index 0ea111e45..8e447a948 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -130,6 +130,8 @@ public: virtual void SetMarker(){} + virtual void ToggleLinks(){} + protected: /** * @brief paintEvent diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 28359e0fc..2a19176a7 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -472,6 +472,30 @@ void TimelineWidget::OverwriteFootageAtPlayhead(const QList &footage) import_tool_->PlaceAt(footage, GetTime(), false); } +void TimelineWidget::ToggleLinksOnSelected() +{ + QList sel = GetSelectedBlocks(); + + // Prioritize unlinking + + QList blocks; + bool link = true; + + foreach (TimelineViewBlockItem* item, sel) { + if (link && item->block()->HasLinks()) { + link = false; + } + + blocks.append(item->block()); + } + + if (link) { + Core::instance()->undo_stack()->push(new BlockLinkManyCommand(blocks, true)); + } else { + Core::instance()->undo_stack()->push(new BlockLinkManyCommand(blocks, false)); + } +} + QList TimelineWidget::GetSelectedBlocks() { QList list; @@ -687,6 +711,7 @@ void TimelineWidget::AddBlock(Block *block, TrackReference track) views_.at(track.type())->view()->scene()->addItem(item); connect(block, &Block::Refreshed, this, &TimelineWidget::BlockChanged); + connect(block, &Block::LinksChanged, this, &TimelineWidget::PreviewUpdated); if (block->type() == Block::kClip) { connect(static_cast(block), &ClipBlock::PreviewUpdated, this, &TimelineWidget::PreviewUpdated); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index ca51db637..86534a45a 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -57,6 +57,8 @@ public: void OverwriteFootageAtPlayhead(const QList &footage); + void ToggleLinksOnSelected(); + QList GetSelectedBlocks(); signals: diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 4eb3e66af..d9b875c0e 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -716,9 +716,10 @@ void WorkareaSetRangeCommand::undo_internal() points_->workarea()->set_range(old_range_); } -BlockLinkCommand::BlockLinkCommand(const QList &blocks, bool link, QUndoCommand *parent) : +BlockLinkCommand::BlockLinkCommand(Block *a, Block *b, bool link, QUndoCommand *parent) : UndoCommand(parent), - blocks_(blocks), + a_(a), + b_(b), link_(link) { } @@ -726,18 +727,20 @@ BlockLinkCommand::BlockLinkCommand(const QList &blocks, bool link, QUnd void BlockLinkCommand::redo_internal() { if (link_) { - Block::Link(blocks_); + done_ = Block::Link(a_, b_); } else { - Block::Unlink(blocks_); + done_ = Block::Unlink(a_, b_); } } void BlockLinkCommand::undo_internal() { - if (link_) { - Block::Unlink(blocks_); - } else { - Block::Link(blocks_); + if (done_) { + if (link_) { + Block::Unlink(a_, b_); + } else { + Block::Link(a_, b_); + } } } @@ -764,3 +767,15 @@ void BlockUnlinkAllCommand::undo_internal() unlinked_.clear(); } + +BlockLinkManyCommand::BlockLinkManyCommand(const QList blocks, bool link, QUndoCommand *parent) : + UndoCommand(parent) +{ + foreach (Block* a, blocks) { + foreach (Block* b, blocks) { + if (a != b) { + new BlockLinkCommand(a, b, link, this); + } + } + } +} diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index ce2d82285..3756fe217 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -331,19 +331,28 @@ private: }; +class BlockLinkManyCommand : public UndoCommand { +public: + BlockLinkManyCommand(const QList blocks, bool link, QUndoCommand* parent = nullptr); +}; + class BlockLinkCommand : public UndoCommand { public: - BlockLinkCommand(const QList& blocks, bool link, QUndoCommand* parent = nullptr); + BlockLinkCommand(Block* a, Block* b, bool link, QUndoCommand* parent = nullptr); protected: virtual void redo_internal() override; virtual void undo_internal() override; private: - QList blocks_; + Block* a_; + + Block* b_; bool link_; + bool done_; + }; class BlockUnlinkAllCommand : public UndoCommand { From a6fb0c7e2b75b464f750667090f9196d957cee3b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 14:28:26 +1100 Subject: [PATCH 26/43] footageproperties: fixed bug that failed to update image sequence end time --- app/dialog/footageproperties/footageproperties.cpp | 2 +- .../streamproperties/videostreamproperties.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index ea543b811..37f2dc467 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -87,7 +87,7 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota connect(track_list, SIGNAL(currentRowChanged(int)), stacked_widget_, SLOT(setCurrentIndex(int))); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->setCenterButtons(true); layout->addWidget(buttons, row, 0, 1, 2); diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index bb816f296..b4362c7d8 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -113,10 +113,13 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) if (IsImageSequence(stream_.get())) { VideoStreamPtr video_stream = std::static_pointer_cast(stream_); - if (video_stream->start_time() != imgseq_start_time_->GetValue()) { + int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue(); + + if (video_stream->start_time() != imgseq_start_time_->GetValue() + || video_stream->duration() != new_dur) { new ImageSequenceChangeCommand(video_stream, imgseq_start_time_->GetValue(), - imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue(), + new_dur, parent); } } From 50061e4e559614d17653c647dc68164f6918d095 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 18:21:14 +1100 Subject: [PATCH 27/43] waveformview: show in/out points in waveformview Moved drawing routines to the base class that is used by both TimeRuler and WaveformView --- app/render/audioparams.cpp | 5 ++ app/render/audioparams.h | 1 + app/widget/timebased/timebased.cpp | 9 ++- app/widget/timebased/timebased.h | 2 + app/widget/timeruler/seekablewidget.cpp | 91 +++++++++++++++++++++ app/widget/timeruler/seekablewidget.h | 16 ++++ app/widget/timeruler/timeruler.cpp | 101 ++++-------------------- app/widget/timeruler/timeruler.h | 6 -- app/widget/viewer/viewer.cpp | 8 +- app/widget/viewer/waveformview.cpp | 4 +- 10 files changed, 146 insertions(+), 97 deletions(-) diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index f91848e55..099c9ceac 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -26,6 +26,11 @@ const uint64_t &AudioParams::channel_layout() const return channel_layout_; } +rational AudioParams::time_base() const +{ + return rational(1, sample_rate()); +} + AudioRenderingParams::AudioRenderingParams() : format_(SampleFormat::SAMPLE_FMT_INVALID) { diff --git a/app/render/audioparams.h b/app/render/audioparams.h index 566309c87..308f68fae 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -14,6 +14,7 @@ public: const int& sample_rate() const; const uint64_t& channel_layout() const; + rational time_base() const; private: int sample_rate_; diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index 816ba0f20..96cd6e025 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -62,13 +62,13 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) ConnectedNodeChanged(viewer_node_); if (viewer_node_) { - ConnectNodeInternal(viewer_node_); - connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); if ((points_ = ConnectTimelinePoints())) { ruler()->ConnectTimelinePoints(points_); } + + ConnectNodeInternal(viewer_node_); } } @@ -152,6 +152,11 @@ TimelinePoints *TimeBasedWidget::ConnectTimelinePoints() return static_cast(viewer_node_->parent()); } +TimelinePoints *TimeBasedWidget::GetConnectedTimelinePoints() const +{ + return points_; +} + void TimeBasedWidget::SetTime(int64_t timestamp) { ruler_->SetTime(timestamp); diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 6a58ba12b..40e8bf513 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -87,6 +87,8 @@ protected: virtual TimelinePoints* ConnectTimelinePoints(); + TimelinePoints* GetConnectedTimelinePoints() const; + protected slots: /** * @brief Slot to center the horizontal scroll bar on the playhead's current position diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index b24d90ae2..624261979 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -1,15 +1,23 @@ #include "seekablewidget.h" #include +#include #include +#include "common/qtutils.h" + SeekableWidget::SeekableWidget(QWidget* parent) : TimelineScaledWidget(parent), time_(0), timeline_points_(nullptr), scroll_(0) { + QFontMetrics fm = fontMetrics(); + text_height_ = fm.height(); + + // Set width of playhead marker + playhead_width_ = QFontMetricsWidth(fm, "H"); } void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) @@ -108,3 +116,86 @@ void SeekableWidget::SeekToScreenPoint(int screen) emit TimeChanged(timestamp); } + +void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) +{ + if (!timeline_points()) { + return; + } + + // Draw in/out workarea + if (timeline_points()->workarea()->enabled()) { + int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); + int workarea_right; + + if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { + workarea_right = width(); + } else { + workarea_right = qMin(width(), TimeToScreen(timeline_points()->workarea()->out())); + } + + p->fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); + } + + // Draw markers + if (marker_bottom > 0 && !timeline_points()->markers()->list().isEmpty()) { + + int marker_top = marker_bottom - text_height_; + + // FIXME: Hardcoded marker colors + p->setPen(Qt::black); + p->setBrush(Qt::green); + + foreach (TimelineMarker* marker, timeline_points()->markers()->list()) { + int marker_left = TimeToScreen(marker->time().in()); + int marker_right = TimeToScreen(marker->time().out()); + + if (marker_left >= width() || marker_right < 0) { + continue; + } + + if (marker->time().length() == 0) { + // Single point in time marker + DrawPlayhead(p, marker_left, marker_bottom); + } else { + // Marker range + int rect_left = qMax(0, marker_left); + int rect_right = qMin(width(), marker_right); + + QRect marker_rect(rect_left, marker_top, rect_right - rect_left, marker_bottom - marker_top); + + p->drawRect(marker_rect); + + if (!marker->name().isEmpty()) { + p->drawText(marker_rect, marker->name()); + } + } + } + } +} + +void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) +{ + int half_width = playhead_width_ / 2; + + if (x + half_width < 0 || x - half_width > width()) { + return; + } + + p->setRenderHint(QPainter::Antialiasing); + + int half_text_height = text_height() / 3; + + QPoint points[] = { + QPoint(x, y), + QPoint(x - half_width, y - half_text_height), + QPoint(x - half_width, y - text_height()), + QPoint(x + 1 + half_width, y - text_height()), + QPoint(x + 1 + half_width, y - half_text_height), + QPoint(x + 1, y), + }; + + p->drawPolygon(points, 6); + + p->setRenderHint(QPainter::Antialiasing, false); +} diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index 9c79d20e6..e96c23b53 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -31,6 +31,8 @@ protected: virtual void ScaleChangedEvent(const double&) override; + void DrawTimelinePoints(QPainter *p, int marker_bottom = 0); + TimelinePoints* timeline_points() const; double ScreenToUnitFloat(int screen); @@ -42,6 +44,16 @@ protected: int TimeToScreen(const rational& time); + void DrawPlayhead(QPainter* p, int x, int y); + + inline const int& text_height() const { + return text_height_; + } + + inline const int& playhead_width() const { + return playhead_width_; + } + inline const QColor& GetPlayheadColor() const { return style_.GetPlayheadColor(); @@ -67,6 +79,10 @@ private: int scroll_; + int text_height_; + + int playhead_width_; + }; #endif // SEEKABLEWIDGET_H diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 6cc411854..c9ff31939 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -39,16 +39,12 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); // Text height is used to calculate widget height - text_height_ = fm.height(); - cache_status_height_ = text_height_ / 4; + cache_status_height_ = text_height() / 4; // Get the "minimum" space allowed between two line markers on the ruler (in screen pixels) // Mediocre but reliable way of scaling UI objects by font/DPI size minimum_gap_between_lines_ = QFontMetricsWidth(fm, "H"); - // Set width of playhead marker - playhead_width_ = minimum_gap_between_lines_; - // Text visibility affects height, so we set that here UpdateHeight(); } @@ -93,65 +89,17 @@ void TimeRuler::paintEvent(QPaintEvent *) // Draw timeline points if connected if (timeline_points()) { + int marker_bottom = height() - text_height(); - // Draw in/out workarea - if (timeline_points()->workarea()->enabled()) { - int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); - int workarea_right; - - if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { - workarea_right = width(); - } else { - workarea_right = qMin(width(), TimeToScreen(timeline_points()->workarea()->out())); - } - - p.fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); + if (show_cache_status_) { + marker_bottom -= cache_status_height_; } - // Draw markers - if (!timeline_points()->markers()->list().isEmpty()) { - int marker_bottom = height() - text_height_; - - if (show_cache_status_) { - marker_bottom -= cache_status_height_; - } - - if (text_visible_) { - marker_bottom -= cache_status_height_; - } - - int marker_top = marker_bottom - text_height_; - - // FIXME: Hardcoded marker colors - p.setPen(Qt::black); - p.setBrush(Qt::green); - - foreach (TimelineMarker* marker, timeline_points()->markers()->list()) { - int marker_left = TimeToScreen(marker->time().in()); - int marker_right = TimeToScreen(marker->time().out()); - - if (marker_left >= width() || marker_right < 0) { - continue; - } - - if (marker->time().length() == 0) { - // Single point in time marker - DrawPlayhead(&p, marker_left, marker_bottom); - } else { - // Marker range - int rect_left = qMax(0, marker_left); - int rect_right = qMin(width(), marker_right); - - QRect marker_rect(rect_left, marker_top, rect_right - rect_left, marker_bottom - marker_top); - - p.drawRect(marker_rect); - - if (!marker->name().isEmpty()) { - p.drawText(marker_rect, marker->name()); - } - } - } + if (text_visible_) { + marker_bottom -= cache_status_height_; } + + DrawTimelinePoints(&p, marker_bottom); } double width_of_frame = timebase_dbl() * GetScale(); @@ -325,11 +273,9 @@ void TimeRuler::paintEvent(QPaintEvent *) // Draw the playhead if it's on screen at the moment int playhead_pos = UnitToScreen(GetTime()); - if (playhead_pos + playhead_width_ >= 0 && playhead_pos - playhead_width_ < width()) { - p.setPen(Qt::NoPen); - p.setBrush(GetPlayheadColor()); - DrawPlayhead(&p, playhead_pos, line_bottom); - } + p.setPen(Qt::NoPen); + p.setBrush(GetPlayheadColor()); + DrawPlayhead(&p, playhead_pos, line_bottom); } void TimeRuler::TimebaseChangedEvent(const rational &tb) @@ -339,25 +285,6 @@ void TimeRuler::TimebaseChangedEvent(const rational &tb) update(); } -void TimeRuler::DrawPlayhead(QPainter *p, int x, int y) -{ - p->setRenderHint(QPainter::Antialiasing); - - int half_text_height = text_height_ / 3; - int half_width = playhead_width_ / 2; - - QPoint points[] = { - QPoint(x, y), - QPoint(x - half_width, y - half_text_height), - QPoint(x - half_width, y - text_height_), - QPoint(x + 1 + half_width, y - text_height_), - QPoint(x + 1 + half_width, y - half_text_height), - QPoint(x + 1, y), - }; - - p->drawPolygon(points, 6); -} - int TimeRuler::CacheStatusHeight() const { return fontMetrics().height() / 4; @@ -365,11 +292,11 @@ int TimeRuler::CacheStatusHeight() const void TimeRuler::UpdateHeight() { - int height = text_height_; + int height = text_height(); // Add text height if (text_visible_) { - height += text_height_; + height += text_height(); } // Add cache status height @@ -378,7 +305,7 @@ void TimeRuler::UpdateHeight() } // Add marker height - height += text_height_; + height += text_height(); setFixedHeight(height); } diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index f926d59d3..83ada935e 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -50,18 +50,12 @@ protected: private: void UpdateHeight(); - void DrawPlayhead(QPainter* p, int x, int y); - int CacheStatusHeight() const; - int text_height_; - int cache_status_height_; int minimum_gap_between_lines_; - int playhead_width_; - bool text_visible_; bool centered_text_; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 742690c2e..ce1efbbf2 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -128,7 +128,7 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) if (!n->video_params().time_base().isNull()) { SetTimebase(n->video_params().time_base()); } else if (n->audio_params().sample_rate() > 0) { - SetTimebase(rational(1, n->audio_params().sample_rate())); + SetTimebase(n->audio_params().time_base()); } else { SetTimebase(rational()); } @@ -157,6 +157,10 @@ void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) UpdateRendererParameters(); UpdateStack(); + + if (GetConnectedTimelinePoints()) { + waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints()); + } } void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) @@ -177,6 +181,8 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) SizeChangedSlot(0, 0); gl_widget_->DisconnectColorManager(); + + waveform_view_->ConnectTimelinePoints(nullptr); } void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n) diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp index a0f26c03b..e6305bc16 100644 --- a/app/widget/viewer/waveformview.cpp +++ b/app/widget/viewer/waveformview.cpp @@ -29,7 +29,7 @@ void WaveformView::SetBackend(AudioRenderBackend *backend) connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); connect(backend_, &AudioRenderBackend::ParamsChanged, this, &WaveformView::BackendParamsChanged); - SetTimebase(rational(1, backend_->params().sample_rate())); + SetTimebase(backend_->params().time_base()); } update(); @@ -91,6 +91,8 @@ void WaveformView::paintEvent(QPaintEvent *event) QPainter p(this); + DrawTimelinePoints(&p); + // FIXME: Hardcoded color p.setPen(Qt::green); From 34f60e639a5cf3f89c4b7e82dbdbd177c1a65fce Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 20:01:58 +1100 Subject: [PATCH 28/43] nodeview: implemented copying/pasting nodes Re-uses XML-based project saving/loading functions to store nodes, their parameters, and their connections in a text-based format in the clipboard. --- app/common/CMakeLists.txt | 2 + app/common/xmlutils.cpp | 37 ++++++ app/common/{xmlreadloop.h => xmlutils.h} | 8 ++ app/node/input.cpp | 2 +- app/node/inputarray.cpp | 2 +- app/node/node.cpp | 2 +- app/node/output.cpp | 2 +- app/panel/node/node.cpp | 15 +++ app/panel/node/node.h | 5 + app/project/item/folder/folder.cpp | 2 +- app/project/item/footage/footage.cpp | 2 +- app/project/item/footage/imagestream.cpp | 2 +- app/project/item/item.cpp | 31 ++++- app/project/item/item.h | 4 +- app/project/item/sequence/sequence.cpp | 30 +---- app/project/project.cpp | 7 +- app/project/project.h | 2 + app/widget/menu/menushared.cpp | 28 ++++- app/widget/menu/menushared.h | 8 ++ app/widget/nodeview/nodeview.cpp | 144 ++++++++++++++++++----- app/widget/nodeview/nodeview.h | 3 + app/widget/nodeview/nodeviewscene.cpp | 28 +++++ app/widget/nodeview/nodeviewscene.h | 3 + app/widget/panel/panel.h | 8 ++ 24 files changed, 306 insertions(+), 71 deletions(-) create mode 100644 app/common/xmlutils.cpp rename app/common/{xmlreadloop.h => xmlutils.h} (61%) diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index c02c8acf2..e08078af0 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -45,5 +45,7 @@ set(OLIVE_SOURCES common/timelinecommon.h common/timerange.h common/timerange.cpp + common/xmlutils.h + common/xmlutils.cpp PARENT_SCOPE ) diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp new file mode 100644 index 000000000..c16c7fc69 --- /dev/null +++ b/app/common/xmlutils.cpp @@ -0,0 +1,37 @@ +#include "xmlutils.h" + +#include "node/factory.h" + +Node* XMLLoadNode(QXmlStreamReader* reader) { + QString node_id; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == "id") { + node_id = attr.value().toString(); + + // Currently the only thing we need + break; + } + } + + if (node_id.isEmpty()) { + qWarning() << "Found node with no ID"; + return nullptr; + } + + Node* node = NodeFactory::CreateFromID(node_id); + + if (!node) { + qWarning() << "Failed to load" << node_id << "- no node with that ID is installed"; + } + + return node; +} + +void XMLConnectNodes(const QHash& output_ptrs, const QList& desired_connections) +{ + foreach (const NodeParam::SerializedConnection& con, desired_connections) { + NodeParam::ConnectEdge(output_ptrs.value(con.output), + con.input); + } +} diff --git a/app/common/xmlreadloop.h b/app/common/xmlutils.h similarity index 61% rename from app/common/xmlreadloop.h rename to app/common/xmlutils.h index 80e101c5f..a25f040bd 100644 --- a/app/common/xmlreadloop.h +++ b/app/common/xmlutils.h @@ -1,6 +1,10 @@ #ifndef XMLREADLOOP_H #define XMLREADLOOP_H +#include + +#include "node/node.h" + #define XMLReadLoop(reader, section) \ while (!reader->atEnd() && !(reader->name() == section && reader->isEndElement()) && reader->readNext()) @@ -8,4 +12,8 @@ QXmlStreamAttributes __attributes = reader->attributes(); \ foreach (const QXmlStreamAttribute& item, __attributes) +Node *XMLLoadNode(QXmlStreamReader* reader); + +void XMLConnectNodes(const QHash &output_ptrs, const QList &desired_connections); + #endif // XMLREADLOOP_H diff --git a/app/node/input.cpp b/app/node/input.cpp index c4a5281f6..8d9f62bac 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -26,7 +26,7 @@ #include "common/bezier.h" #include "common/lerp.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node.h" #include "output.h" #include "inputarray.h" diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index e7d0e0e01..ac8c9406f 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -2,7 +2,7 @@ #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node.h" NodeInputArray::NodeInputArray(const QString &id, const DataType &type, const QVariant &default_value) : diff --git a/app/node/node.cpp b/app/node/node.cpp index 38a3b6500..7c2deb60f 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -24,7 +24,7 @@ #include #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" Node::Node() : can_be_deleted_(true) diff --git a/app/node/output.cpp b/app/node/output.cpp index dd87ac9c1..efcdeda7e 100644 --- a/app/node/output.cpp +++ b/app/node/output.cpp @@ -20,7 +20,7 @@ #include "output.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node/node.h" NodeOutput::NodeOutput(const QString &id) : diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index d9be8e307..f21fe2098 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -59,6 +59,21 @@ void NodePanel::DeleteSelected() node_view_->DeleteSelected(); } +void NodePanel::CutSelected() +{ + node_view_->CopySelected(true); +} + +void NodePanel::CopySelected() +{ + node_view_->CopySelected(false); +} + +void NodePanel::Paste() +{ + node_view_->Paste(); +} + void NodePanel::Select(const QList &nodes) { node_view_->Select(nodes); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index e84d9c7c8..5ce5b75ca 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -40,6 +40,11 @@ public: virtual void DeleteSelected() override; + virtual void CutSelected() override; + virtual void CopySelected() override; + + virtual void Paste() override; + public slots: void Select(const QList& nodes); void SelectWithDependencies(const QList& nodes); diff --git a/app/project/item/folder/folder.cpp b/app/project/item/folder/folder.cpp index 35a516332..e8cb04456 100644 --- a/app/project/item/folder/folder.cpp +++ b/app/project/item/folder/folder.cpp @@ -20,7 +20,7 @@ #include "folder.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "project/item/footage/footage.h" #include "project/item/sequence/sequence.h" #include "ui/icons/icons.h" diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 299597bf2..cccf67b75 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -24,7 +24,7 @@ #include "codec/decoder.h" #include "common/timecodefunctions.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "config/config.h" #include "ui/icons/icons.h" diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index 855c0a806..312bca748 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -20,7 +20,7 @@ #include "imagestream.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "footage.h" #include "project/project.h" #include "render/colormanager.h" diff --git a/app/project/item/item.cpp b/app/project/item/item.cpp index 661708260..d694bc1e4 100644 --- a/app/project/item/item.cpp +++ b/app/project/item/item.cpp @@ -76,11 +76,19 @@ const QList &Item::children() const return children_; } -ItemPtr Item::shared_ptr_from_raw(Item *item) +ItemPtr Item::shared_ptr_from_raw(Item *item, bool traverse) { for (int i=0;iCanHaveChildren()) { + ItemPtr grandchild = shared_ptr_from_raw(item); + + if (grandchild) { + return grandchild; + } } } @@ -147,6 +155,23 @@ void Item::set_project(Project *project) project_ = project; } +QList Item::get_children_of_type(Type type, bool recursive) const +{ + QList list; + + foreach (ItemPtr item, children_) { + if (item->type() == type) { + list.append(item); + } + + if (recursive && item->CanHaveChildren()) { + list.append(item->get_children_of_type(type, recursive)); + } + } + + return list; +} + bool Item::CanHaveChildren() const { return false; diff --git a/app/project/item/item.h b/app/project/item/item.h index e8385f09f..42cb83bf7 100644 --- a/app/project/item/item.h +++ b/app/project/item/item.h @@ -77,7 +77,7 @@ public: Item* child(int i) const; const QList& children() const; - ItemPtr shared_ptr_from_raw(Item* item); + ItemPtr shared_ptr_from_raw(Item* item, bool traverse = false); const QString& name() const; void set_name(const QString& n); @@ -97,6 +97,8 @@ public: Project* project() const; void set_project(Project* project); + QList get_children_of_type(Type type, bool recursive) const; + virtual bool CanHaveChildren() const; bool ChildExistsWithName(const QString& name); diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index e8edb5601..e91fea51e 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -25,7 +25,7 @@ #include "config/config.h" #include "common/channellayout.h" #include "common/timecodefunctions.h" -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "node/factory.h" #include "panel/panelmanager.h" #include "panel/node/node.h" @@ -110,28 +110,7 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis Node* node; if (reader->name() == "node") { - QString node_id; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == "id") { - node_id = attr.value().toString(); - - // Currently the only thing we need - break; - } - } - - if (node_id.isEmpty()) { - qDebug() << "Found node with no ID"; - continue; - } - - node = NodeFactory::CreateFromID(node_id); - - if (!node) { - qDebug() << "Failed to load" << node_id << "- no node with that ID is installed"; - continue; - } + node = XMLLoadNode(reader); } else { node = viewer_output_; } @@ -146,10 +125,7 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis } // Make connections - foreach (const NodeParam::SerializedConnection& con, desired_connections) { - NodeParam::ConnectEdge(output_ptrs.value(con.output), - con.input); - } + XMLConnectNodes(output_ptrs, desired_connections); // Ensure this and all children are in the main thread // (FIXME: Weird place for this? This should probably be in ProjectLoadManager somehow) diff --git a/app/project/project.cpp b/app/project/project.cpp index 4cb69bd59..5b7231d95 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -23,7 +23,7 @@ #include #include -#include "common/xmlreadloop.h" +#include "common/xmlutils.h" #include "core.h" #include "dialog/progress/progress.h" #include "window/mainwindow/mainwindow.h" @@ -137,3 +137,8 @@ ColorManager *Project::color_manager() { return &color_manager_; } + +QList Project::get_items_of_type(Item::Type type) const +{ + return root_.get_children_of_type(type, true); +} diff --git a/app/project/project.h b/app/project/project.h index b7ee369e1..3a90bb6f3 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -63,6 +63,8 @@ public: ColorManager* color_manager(); + QList get_items_of_type(Item::Type type) const; + signals: void NameChanged(); diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index d128017ac..fb36b2238 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -34,10 +34,10 @@ MenuShared::MenuShared() new_folder_item_ = Menu::CreateItem(this, "newfolder", Core::instance(), SLOT(CreateNewFolder())); // "Edit" menu shared items - edit_cut_item_ = Menu::CreateItem(this, "cut", nullptr, nullptr, "Ctrl+X"); - edit_copy_item_ = Menu::CreateItem(this, "copy", nullptr, nullptr, "Ctrl+C"); - edit_paste_item_ = Menu::CreateItem(this, "paste", nullptr, nullptr, "Ctrl+V"); - edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", nullptr, nullptr, "Ctrl+Shift+V"); + edit_cut_item_ = Menu::CreateItem(this, "cut", this, SLOT(CutTriggered()), "Ctrl+X"); + edit_copy_item_ = Menu::CreateItem(this, "copy", this, SLOT(CopyTriggered()), "Ctrl+C"); + edit_paste_item_ = Menu::CreateItem(this, "paste", this, SLOT(PasteTriggered()), "Ctrl+V"); + edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", this, SLOT(PasteInsertTriggered()), "Ctrl+Shift+V"); edit_duplicate_item_ = Menu::CreateItem(this, "duplicate", nullptr, nullptr, "Ctrl+D"); edit_delete_item_ = Menu::CreateItem(this, "delete", this, SLOT(DeleteSelectedTriggered()), "Del"); edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, SLOT(RippleDeleteTriggered()), "Shift+Del"); @@ -161,6 +161,26 @@ void MenuShared::ToggleLinksTriggered() PanelManager::instance()->CurrentlyFocused()->ToggleLinks(); } +void MenuShared::CutTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->CutSelected(); +} + +void MenuShared::CopyTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->CopySelected(); +} + +void MenuShared::PasteTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->Paste(); +} + +void MenuShared::PasteInsertTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->PasteInsert(); +} + void MenuShared::Retranslate() { // "New" menu shared items diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index d15408f57..c1e6e54ff 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -93,6 +93,14 @@ private slots: void ToggleLinksTriggered(); + void CutTriggered(); + + void CopyTriggered(); + + void PasteTriggered(); + + void PasteInsertTriggered(); + }; #endif // MENUSHARED_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index f1d895a05..2c728acf3 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,11 +20,14 @@ #include "nodeview.h" +#include #include +#include #include "core.h" #include "nodeviewundo.h" #include "node/factory.h" +#include "common/xmlutils.h" NodeView::NodeView(QWidget *parent) : QGraphicsView(parent), @@ -90,16 +93,7 @@ void NodeView::DeleteSelected() return; } - QList selected = scene_.selectedItems(); - QList selected_nodes; - - foreach (QGraphicsItem* item, selected) { - NodeViewItem* node_item = dynamic_cast(item); - - if (node_item) { - selected_nodes.append(node_item->node()); - } - } + QList selected_nodes = scene_.GetSelectedNodes(); if (selected_nodes.isEmpty()) { return; @@ -133,9 +127,6 @@ void NodeView::Select(const QList &nodes) foreach (Node* n, nodes) { NodeViewItem* item = scene_.NodeToUIObject(n); - Q_ASSERT(n); - Q_ASSERT(item); - item->setSelected(true); } } @@ -150,6 +141,117 @@ void NodeView::SelectWithDependencies(QList nodes) Select(nodes); } +void NodeView::CopySelected(bool cut) +{ + if (!graph_) { + return; + } + + QList selected = scene_.GetSelectedNodes(); + + if (selected.isEmpty()) { + return; + } + + QString copy_str; + + QXmlStreamWriter writer(©_str); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + writer.writeStartElement(QStringLiteral("olive")); + + foreach (Node* n, selected) { + n->Save(&writer); + } + + writer.writeEndElement(); // clipboard + writer.writeEndDocument(); + + if (cut) { + DeleteSelected(); + } + + QGuiApplication::clipboard()->setText(copy_str); +} + +void NodeView::Paste() +{ + if (!graph_) { + return; + } + + QString clipboard = QGuiApplication::clipboard()->text(); + + if (clipboard.isEmpty()) { + return; + } + + QXmlStreamReader reader(clipboard); + + QList pasted_nodes; + QHash output_ptrs; + QList desired_connections; + QList footage_connections; + + XMLReadLoop((&reader), QStringLiteral("olive")) { + if (reader.name() == QStringLiteral("node")) { + Node* node = XMLLoadNode(&reader); + + if (node) { + node->Load(&reader, output_ptrs, desired_connections, footage_connections, nullptr, reader.name().toString()); + + graph_->AddNode(node); + + pasted_nodes.append(node); + } + } + } + + // Make connections + if (!desired_connections.isEmpty()) { + XMLConnectNodes(output_ptrs, desired_connections); + } + + // Connect footage to existing footage if it exists + if (!footage_connections.isEmpty()) { + // Get list of all footage from project + // FIXME: Assumes sequence + QList footage = static_cast(graph_)->project()->get_items_of_type(Item::kFootage); + + if (!footage.isEmpty()) { + foreach (const NodeInput::FootageConnection& con, footage_connections) { + if (con.footage) { + // Assume this is a pointer to a Stream* + Stream* loaded_stream = reinterpret_cast(con.footage); + + bool found = false; + + foreach (ItemPtr item, footage) { + const QList& streams = std::static_pointer_cast(item)->streams(); + + foreach (StreamPtr s, streams) { + if (s.get() == loaded_stream) { + con.input->set_standard_value(QVariant::fromValue(s)); + found = true; + break; + } + } + + if (found) { + break; + } + } + } + } + } + } + + if (!pasted_nodes.isEmpty()) { + // FIXME: Attach to cursor so user can drop in place + } +} + void NodeView::ItemsChanged() { QHash::const_iterator i; @@ -263,21 +365,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) void NodeView::SceneSelectionChangedSlot() { - // Get the scene's selected items and convert it into a list of selected nodes - QList selected_items = scene_.selectedItems(); - - QList selected_nodes; - - for (int i=0;i(selected_items.at(i)); - - if (item != nullptr) { - selected_nodes.append(item->node()); - } - } - - emit SelectionChanged(selected_nodes); + emit SelectionChanged(scene_.GetSelectedNodes()); } void NodeView::ShowContextMenu(const QPoint &pos) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 4ea98bea3..7811ae4bb 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -57,6 +57,9 @@ public: void Select(const QList& nodes); void SelectWithDependencies(QList nodes); + void CopySelected(bool cut); + void Paste(); + signals: /** * @brief Signal emitted when the selected nodes have changed diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 750e391a2..a514eb898 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -42,6 +42,34 @@ void NodeViewScene::SetGraph(NodeGraph *graph) graph_ = graph; } +QList NodeViewScene::GetSelectedNodes() const +{ + QHash::const_iterator iterator; + QList selected; + + for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { + if (iterator.value()->isSelected()) { + selected.append(iterator.key()); + } + } + + return selected; +} + +QList NodeViewScene::GetSelectedItems() const +{ + QHash::const_iterator iterator; + QList selected; + + for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { + if (iterator.value()->isSelected()) { + selected.append(iterator.value()); + } + } + + return selected; +} + const QHash &NodeViewScene::item_map() const { return item_map_; diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 2c768fd11..014791759 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -38,6 +38,9 @@ public: void SetGraph(NodeGraph* graph); + QList GetSelectedNodes() const; + QList GetSelectedItems() const; + const QHash& item_map() const; const QHash& edge_map() const; diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index 8e447a948..ff8ce54b1 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -132,6 +132,14 @@ public: virtual void ToggleLinks(){} + virtual void CutSelected(){} + + virtual void CopySelected(){} + + virtual void Paste(){} + + virtual void PasteInsert(){} + protected: /** * @brief paintEvent From 4bc5378116771266dad9120ccd22ec446e3dc359 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 15 Mar 2020 20:08:56 +1100 Subject: [PATCH 29/43] ffmpegencoder: explicit cast from int64_t to int --- app/codec/ffmpeg/ffmpegencoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index e92189472..60f61efe5 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -355,7 +355,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV } if (params().video_buffer_size() > 0) { - video_codec_ctx_->rc_buffer_size = params().video_buffer_size(); + video_codec_ctx_->rc_buffer_size = static_cast(params().video_buffer_size()); } } From 1c192100c2462637a438813ad4f32bbee7a7854b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 17 Mar 2020 00:01:18 +1100 Subject: [PATCH 30/43] viewer: fixed bug where seeking would be stuck to one frame --- app/widget/viewer/viewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index ce1efbbf2..4ff69b2d2 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -459,7 +459,7 @@ void ViewerWidget::Pause() playback_speed_ = 0; controls_->ShowPlayButton(); - if (stack_->currentWidget() == gl_widget_) { + if (stack_->currentWidget() == sizer_) { disconnect(gl_widget_, &ViewerGLWidget::frameSwapped, this, &ViewerWidget::PlaybackTimerUpdate); } else { disconnect(AudioManager::instance(), &AudioManager::OutputNotified, this, &ViewerWidget::PlaybackTimerUpdate); From 72eb3a5d829cd6354215257b298172a1c039e031 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 17 Mar 2020 00:34:32 +1100 Subject: [PATCH 31/43] nodeview: worked around qt issue where the selection signal would still be sent after an item was deleted --- app/widget/nodeview/nodeview.cpp | 12 ++---------- app/widget/nodeview/nodeviewscene.cpp | 27 +++++++++++++++++++++++++++ app/widget/nodeview/nodeviewscene.h | 3 +++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 2c728acf3..571559926 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -104,20 +104,12 @@ void NodeView::DeleteSelected() void NodeView::SelectAll() { - QList all_items = this->items(); - - foreach (QGraphicsItem* i, all_items) { - i->setSelected(true); - } + scene_.SelectAll(); } void NodeView::DeselectAll() { - QList selected_items = scene_.selectedItems(); - - foreach (QGraphicsItem* i, selected_items) { - i->setSelected(false); - } + scene_.DeselectAll(); } void NodeView::Select(const QList &nodes) diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index a514eb898..dffd8702f 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -10,6 +10,15 @@ NodeViewScene::NodeViewScene(QObject *parent) : void NodeViewScene::clear() { + // Deselect everything (prevents signals that a selection has changed after deleting an object) + DeselectAll(); + + // HACK: QGraphicsScene contains some sort of internal hashing of the selected items which doesn't update unless + // we call a function like this. That means even though we deselect all items above, QGraphicsScene will + // continue to incorrectly signal selectionChanged() when items that were selected (but are now not) get + // deleted. Calling this function appears to update the internal cache and prevent this. + selectedItems(); + { QHash::const_iterator i; for (i=item_map_.begin();i!=item_map_.end();i++) { @@ -27,6 +36,24 @@ void NodeViewScene::clear() } } +void NodeViewScene::SelectAll() +{ + QList all_items = this->items(); + + foreach (QGraphicsItem* i, all_items) { + i->setSelected(true); + } +} + +void NodeViewScene::DeselectAll() +{ + QList selected_items = this->selectedItems(); + + foreach (QGraphicsItem* i, selected_items) { + i->setSelected(false); + } +} + NodeViewItem *NodeViewScene::NodeToUIObject(Node *n) { return item_map_.value(n); diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 014791759..8a2c22933 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -16,6 +16,9 @@ public: void clear(); + void SelectAll(); + void DeselectAll(); + /** * @brief Retrieve the graphical widget corresponding to a specific Node * From a1f5c85f49ffcbcc25acea8814328028293dbeeb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 17 Mar 2020 02:38:17 +1100 Subject: [PATCH 32/43] various: use simpler/smarter XML reading functions Should address any XML parsing issues and allows for simpler, more maintainable code. Should also address a crash that could occur when pasting nodes that had inputs that weren't copied. --- app/common/xmlutils.cpp | 23 ++- app/common/xmlutils.h | 5 +- app/config/config.cpp | 67 +++++---- app/node/input.cpp | 169 ++++++++++++----------- app/node/inputarray.cpp | 10 +- app/node/node.cpp | 48 +++---- app/node/node.h | 2 +- app/node/output.cpp | 2 + app/project/item/folder/folder.cpp | 33 ++--- app/project/item/footage/footage.cpp | 48 ++++--- app/project/item/footage/imagestream.cpp | 9 +- app/project/item/footage/stream.cpp | 3 +- app/project/item/sequence/sequence.cpp | 99 +++++++------ app/project/project.cpp | 35 ++--- app/project/projectloadmanager.cpp | 36 +++-- app/widget/nodeview/nodeview.cpp | 22 ++- 16 files changed, 326 insertions(+), 285 deletions(-) diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp index c16c7fc69..790abe546 100644 --- a/app/common/xmlutils.cpp +++ b/app/common/xmlutils.cpp @@ -31,7 +31,26 @@ Node* XMLLoadNode(QXmlStreamReader* reader) { void XMLConnectNodes(const QHash& output_ptrs, const QList& desired_connections) { foreach (const NodeParam::SerializedConnection& con, desired_connections) { - NodeParam::ConnectEdge(output_ptrs.value(con.output), - con.input); + NodeOutput* out = output_ptrs.value(con.output); + + if (out) { + NodeParam::ConnectEdge(out, con.input); + } } } + +bool XMLReadNextStartElement(QXmlStreamReader *reader) +{ + QXmlStreamReader::TokenType token; + + while ((token = reader->readNext()) != QXmlStreamReader::Invalid + && token != QXmlStreamReader::EndDocument) { + if (reader->isEndElement()) { + return false; + } else if (reader->isStartElement()) { + return true; + } + } + + return false; +} diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index a25f040bd..d3530abb3 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -5,9 +5,6 @@ #include "node/node.h" -#define XMLReadLoop(reader, section) \ - while (!reader->atEnd() && !(reader->name() == section && reader->isEndElement()) && reader->readNext()) - #define XMLAttributeLoop(reader, item) \ QXmlStreamAttributes __attributes = reader->attributes(); \ foreach (const QXmlStreamAttribute& item, __attributes) @@ -16,4 +13,6 @@ Node *XMLLoadNode(QXmlStreamReader* reader); void XMLConnectNodes(const QHash &output_ptrs, const QList &desired_connections); +bool XMLReadNextStartElement(QXmlStreamReader* reader); + #endif // XMLREADLOOP_H diff --git a/app/config/config.cpp b/app/config/config.cpp index b294f5909..f9c1bcb46 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -29,6 +29,7 @@ #include "common/autoscroll.h" #include "common/filefunctions.h" +#include "common/xmlutils.h" #include "core.h" #include "window/mainwindow/mainwindow.h" @@ -120,52 +121,50 @@ void Config::Load() QString config_version; - while (!reader.atEnd()) { - reader.readNext(); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("Configuration")) { + while (XMLReadNextStartElement(&reader)) { + QString key = reader.name().toString(); + QString value = reader.readElementText(); - if (!reader.isStartElement()) { - continue; - } + if (key == QStringLiteral("Version")) { + config_version = value; - QString key = reader.name().toString(); + if (!value.contains(".")) { + qDebug() << "CONFIG: This is a 0.1.x config file, upconvert"; + } + } else if (key == QStringLiteral("DefaultSequenceFrameRate") && !config_version.contains('.')) { + // 0.1.x stored this value as a float while we now use rationals, we'll use a heuristic to find the closest + // supported rational + qDebug() << " CONFIG: Finding closest match to" << value; - reader.readNext(); - QString value = reader.text().toString(); + double config_fr = value.toDouble(); - if (key == "Configuration") { - // First element, ignore - } else if (key == "Version") { - config_version = value; + QList supported_frame_rates = Core::SupportedFrameRates(); - if (!value.contains(".")) { - qDebug() << "CONFIG: This is a 0.1.x config file, upconvert"; - } - } else if (key == "DefaultSequenceFrameRate" && !config_version.contains(".")) { - // 0.1.x stored this value as a float while we now use rationals, we'll use a heuristic to find the closest - // supported rational - qDebug() << " CONFIG: Finding closest match to" << value; + rational match = supported_frame_rates.first(); + double match_diff = qAbs(match.toDouble() - config_fr); - double config_fr = value.toDouble(); + for (int i=1;i supported_frame_rates = Core::SupportedFrameRates(); + if (diff < match_diff) { + match = supported_frame_rates.at(i); + match_diff = diff; + } + } - rational match = supported_frame_rates.first(); - double match_diff = qAbs(match.toDouble() - config_fr); + qDebug() << " CONFIG: Closest match was" << match.toDouble(); - for (int i=1;imain_window(), QCoreApplication::translate("Config", "Error loading settings"), QCoreApplication::translate("Config", "Failed to load application settings. This session will " - "use defaults."), + "use defaults.\n\n%1").arg(reader.errorString()), QMessageBox::Ok); current_config_.SetDefaults(); } diff --git a/app/node/input.cpp b/app/node/input.cpp index 8d9f62bac..8602ab06e 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -91,111 +91,111 @@ void NodeInput::Load(QXmlStreamReader *reader, QHash& par return; } - if (attr.name() == "keyframing") { - set_is_keyframing(attr.value() == "1"); + if (attr.name() == QStringLiteral("keyframing")) { + set_is_keyframing(attr.value() == QStringLiteral("1")); } } - XMLReadLoop(reader, "input") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "standard") { - // Load standard value - int val_index = 0; + if (reader->name() == QStringLiteral("standard")) { + // Load standard value + int val_index = 0; - XMLReadLoop(reader, "standard") { - if (cancelled && *cancelled) { - return; + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } + + if (reader->name() == QStringLiteral("value")) { + QString value_text = reader->readElementText(); + + if (value_text.isEmpty()) { + standard_value_.replace(val_index, QVariant()); + } else { + standard_value_.replace(val_index, StringToValue(value_text, footage_connections)); } - if (reader->isStartElement() && reader->name() == "value") { - reader->readNext(); + val_index++; + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("keyframes")) { + int track = 0; - QString value_text = reader->text().toString(); + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } - if (value_text.isEmpty()) { - standard_value_.replace(val_index, QVariant()); - } else { - standard_value_.replace(val_index, StringToValue(value_text, footage_connections)); + if (reader->name() == QStringLiteral("track")) { + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; } - val_index++; - } - } - } else if (reader->name() == "keyframes") { - int track = 0; + if (reader->name() == QStringLiteral("key")) { + rational key_time; + NodeKeyframe::Type key_type; + QVariant key_value; + QPointF key_in_handle; + QPointF key_out_handle; - XMLReadLoop(reader, "keyframes") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement() && reader->name() == "track") { - XMLReadLoop(reader, "track") { - if (cancelled && *cancelled) { - return; - } - - if (reader->name() == "key") { - rational key_time; - NodeKeyframe::Type key_type; - QVariant key_value; - QPointF key_in_handle; - QPointF key_out_handle; - - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } - - if (attr.name() == "time") { - key_time = rational::fromString(attr.value().toString()); - } else if (attr.name() == "type") { - key_type = static_cast(attr.value().toInt()); - } else if (attr.name() == "inhandlex") { - key_in_handle.setX(attr.value().toDouble()); - } else if (attr.name() == "inhandley") { - key_in_handle.setY(attr.value().toDouble()); - } else if (attr.name() == "outhandlex") { - key_out_handle.setX(attr.value().toDouble()); - } else if (attr.name() == "outhandley") { - key_out_handle.setY(attr.value().toDouble()); - } + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; } - reader->readNext(); - - key_value = StringToValue(reader->text().toString(), footage_connections); - - NodeKeyframePtr key = NodeKeyframe::Create(key_time, key_value, key_type, track); - key->set_bezier_control_in(key_in_handle); - key->set_bezier_control_out(key_out_handle); - key->set_parent(this); - keyframe_tracks_[track].append(key); + if (attr.name() == QStringLiteral("time")) { + key_time = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("type")) { + key_type = static_cast(attr.value().toInt()); + } else if (attr.name() == QStringLiteral("inhandlex")) { + key_in_handle.setX(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("inhandley")) { + key_in_handle.setY(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("outhandlex")) { + key_out_handle.setX(attr.value().toDouble()); + } else if (attr.name() == QStringLiteral("outhandley")) { + key_out_handle.setY(attr.value().toDouble()); + } } + + key_value = StringToValue(reader->readElementText(), footage_connections); + + NodeKeyframePtr key = NodeKeyframe::Create(key_time, key_value, key_type, track); + key->set_bezier_control_in(key_in_handle); + key->set_bezier_control_out(key_out_handle); + key->set_parent(this); + keyframe_tracks_[track].append(key); + } else { + reader->skipCurrentElement(); } - - track++; } + + track++; + } else { + reader->skipCurrentElement(); } - } else if (reader->name() == "connections") { - XMLReadLoop(reader, "connections") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement() && reader->name() == "connection") { - reader->readNext(); - - input_connections.append({this, reader->text().toULongLong()}); - } - } - } else { - LoadInternal(reader, param_ptrs, input_connections, footage_connections, cancelled); } + } else if (reader->name() == QStringLiteral("connections")) { + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; + } + + if (reader->name() == QStringLiteral("connection")) { + input_connections.append({this, reader->readElementText().toULongLong()}); + } else { + reader->skipCurrentElement(); + } + } + } else { + LoadInternal(reader, param_ptrs, input_connections, footage_connections, cancelled); } } } @@ -268,8 +268,9 @@ const NodeParam::DataType &NodeInput::data_type() const return data_type_; } -void NodeInput::LoadInternal(QXmlStreamReader*, QHash&, QList&, QList&, const QAtomicInt*) +void NodeInput::LoadInternal(QXmlStreamReader* reader, QHash&, QList&, QList&, const QAtomicInt*) { + reader->skipCurrentElement(); } void NodeInput::SaveInternal(QXmlStreamWriter*) const diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index ac8c9406f..7fe1fc668 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -166,13 +166,17 @@ void NodeInputArray::RemoveAt(int index) void NodeInputArray::LoadInternal(QXmlStreamReader *reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt* cancelled) { - if (reader->name() == "subparameters") { - XMLReadLoop(reader, "subparameters") { - if (reader->name() == "input") { + if (reader->name() == QStringLiteral("subparameters")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("input")) { Append(); At(GetSize() - 1)->Load(reader, param_ptrs, input_connections, footage_connections, cancelled); + } else { + reader->skipCurrentElement(); } } + } else { + NodeInput::Load(reader, param_ptrs, input_connections, footage_connections, cancelled); } } diff --git a/app/node/node.cpp b/app/node/node.cpp index 7c2deb60f..fb12a0093 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -50,39 +50,39 @@ Node::~Node() } } -void Node::Load(QXmlStreamReader *reader, QHash &output_ptrs, QList& input_connections, QList& footage_connections, const QAtomicInt* cancelled, const QString& element) +void Node::Load(QXmlStreamReader *reader, QHash &output_ptrs, QList& input_connections, QList& footage_connections, const QAtomicInt* cancelled) { - XMLReadLoop(reader, (element.isEmpty() ? "node" : element)) { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "input" || reader->name() == "output") { - QString param_id; + if (reader->name() == QStringLiteral("input") || reader->name() == QStringLiteral("output")) { + QString param_id; - XMLAttributeLoop(reader, attr) { - if (attr.name() == "id") { - param_id = attr.value().toString(); + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + param_id = attr.value().toString(); - break; - } + break; } - - if (param_id.isEmpty()) { - qDebug() << "Found parameter with no ID"; - continue; - } - - NodeParam* param = GetParameterWithID(param_id); - - if (!param) { - qDebug() << "No parameter in" << id() << "with parameter" << param_id; - continue; - } - - param->Load(reader, output_ptrs, input_connections, footage_connections, cancelled); } + + if (param_id.isEmpty()) { + qDebug() << "Found parameter with no ID"; + continue; + } + + NodeParam* param = GetParameterWithID(param_id); + + if (!param) { + qDebug() << "No parameter in" << id() << "with parameter" << param_id; + continue; + } + + param->Load(reader, output_ptrs, input_connections, footage_connections, cancelled); + } else { + reader->skipCurrentElement(); } } } diff --git a/app/node/node.h b/app/node/node.h index d14fef691..b049a8618 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -67,7 +67,7 @@ public: /** * @brief Clear current node variables and replace them with */ - void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled, const QString &element = QString()); + void Load(QXmlStreamReader* reader, QHash& param_ptrs, QList &input_connections, QList& footage_connections, const QAtomicInt *cancelled); /** * @brief Save this node into a text/XML format diff --git a/app/node/output.cpp b/app/node/output.cpp index efcdeda7e..79646932f 100644 --- a/app/node/output.cpp +++ b/app/node/output.cpp @@ -55,6 +55,8 @@ void NodeOutput::Load(QXmlStreamReader* reader, QHash& pa param_ptrs.insert(saved_ptr, this); } } + + reader->skipCurrentElement(); } void NodeOutput::Save(QXmlStreamWriter *writer) const diff --git a/app/project/item/folder/folder.cpp b/app/project/item/folder/folder.cpp index e8cb04456..12a221bd3 100644 --- a/app/project/item/folder/folder.cpp +++ b/app/project/item/folder/folder.cpp @@ -46,37 +46,38 @@ QIcon Folder::icon() void Folder::Load(QXmlStreamReader *reader, QHash &footage_ptrs, QList& footage_connections, const QAtomicInt *cancelled) { + qDebug() << "Hello?"; + XMLAttributeLoop(reader, attr) { if (cancelled && *cancelled) { return; } - if (attr.name() == "name") { + if (attr.name() == QStringLiteral("name")) { set_name(attr.value().toString()); } } - XMLReadLoop(reader, "folder") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - ItemPtr child; + ItemPtr child; - if (reader->name() == "folder") { - child = std::make_shared(); - } else if (reader->name() == "footage") { - child = std::make_shared(); - } else if (reader->name() == "sequence") { - child = std::make_shared(); - } else { - continue; - } - - add_child(child); - child->Load(reader, footage_ptrs, footage_connections, cancelled); + if (reader->name() == QStringLiteral("folder")) { + child = std::make_shared(); + } else if (reader->name() == QStringLiteral("footage")) { + child = std::make_shared(); + } else if (reader->name() == QStringLiteral("sequence")) { + child = std::make_shared(); + } else { + reader->skipCurrentElement(); + continue; } + + add_child(child); + child->Load(reader, footage_ptrs, footage_connections, cancelled); } } diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index cccf67b75..e0b98a9ac 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -40,48 +40,50 @@ Footage::~Footage() void Footage::Load(QXmlStreamReader *reader, QHash& footage_ptrs, QList&, const QAtomicInt* cancelled) { + qDebug() << "Hello?"; + QXmlStreamAttributes attributes = reader->attributes(); foreach (const QXmlStreamAttribute& attr, attributes) { - if (attr.name() == "name") { + if (attr.name() == QStringLiteral("name")) { set_name(attr.value().toString()); - } else if (attr.name() == "filename") { + } else if (attr.name() == QStringLiteral("filename")) { set_filename(attr.value().toString()); } } Decoder::ProbeMedia(this, cancelled); - XMLReadLoop(reader, "footage") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "stream") { - int stream_index = -1; - quintptr stream_ptr = 0; + if (reader->name() == QStringLiteral("stream")) { + int stream_index = -1; + quintptr stream_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } - - if (attr.name() == "index") { - stream_index = attr.value().toInt(); - } else if (attr.name() == "ptr") { - stream_ptr = attr.value().toULongLong(); - } + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; } - if (stream_index > -1 && stream_ptr > 0) { - footage_ptrs.insert(stream_ptr, stream(stream_index)); - - stream(stream_index)->Load(reader); - } else { - qWarning() << "Invalid stream found in project file"; + if (attr.name() == QStringLiteral("index")) { + stream_index = attr.value().toInt(); + } else if (attr.name() == QStringLiteral("ptr")) { + stream_ptr = attr.value().toULongLong(); } } + + if (stream_index > -1 && stream_ptr > 0) { + footage_ptrs.insert(stream_ptr, stream(stream_index)); + + stream(stream_index)->Load(reader); + } else { + qWarning() << "Invalid stream found in project file"; + } + } else { + reader->skipCurrentElement(); } } } diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index 312bca748..2e17c0563 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -43,10 +43,11 @@ void ImageStream::FootageSetEvent(Footage *f) void ImageStream::LoadCustomParameters(QXmlStreamReader *reader) { - XMLReadLoop(reader, "stream") { - if (reader->isStartElement() && reader->name() == "colorspace") { - reader->readNext(); - set_colorspace(reader->text().toString()); + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("colorspace")) { + set_colorspace(reader->readElementText()); + } else { + reader->skipCurrentElement(); } } } diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index fc9bb8456..52a402851 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -151,8 +151,9 @@ void Stream::FootageSetEvent(Footage*) { } -void Stream::LoadCustomParameters(QXmlStreamReader*) +void Stream::LoadCustomParameters(QXmlStreamReader* reader) { + reader->skipCurrentElement(); } void Stream::SaveCustomParameters(QXmlStreamWriter*) const diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index e91fea51e..f15344310 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -59,68 +59,63 @@ void Sequence::Load(QXmlStreamReader *reader, QHash &, QLis QHash output_ptrs; QList desired_connections; - XMLReadLoop(reader, "sequence") { + while (XMLReadNextStartElement(reader)) { if (cancelled && *cancelled) { return; } - if (reader->isStartElement()) { - if (reader->name() == "video") { - int video_width, video_height; - rational video_timebase; + if (reader->name() == QStringLiteral("video")) { + int video_width, video_height; + rational video_timebase; - XMLReadLoop(reader, "video") { - if (cancelled && *cancelled) { - return; - } - - if (reader->isStartElement()) { - if (reader->name() == "width") { - reader->readNext(); - video_width = reader->text().toInt(); - } else if (reader->name() == "height") { - reader->readNext(); - video_height = reader->text().toInt(); - } else if (reader->name() == "timebase") { - reader->readNext(); - video_timebase = rational::fromString(reader->text().toString()); - } - } + while (XMLReadNextStartElement(reader)) { + if (cancelled && *cancelled) { + return; } - set_video_params(VideoParams(video_width, video_height, video_timebase)); - } else if (reader->name() == "audio") { - int rate; - uint64_t layout; - - XMLReadLoop(reader, "audio") { - if (reader->isStartElement()) { - if (reader->name() == "rate") { - reader->readNext(); - rate = reader->text().toInt(); - } else if (reader->name() == "layout") { - reader->readNext(); - layout = reader->text().toULongLong(); - } - } - } - - set_audio_params(AudioParams(rate, layout)); - } else if (reader->name() == "node" || reader->name() == "viewer") { - Node* node; - - if (reader->name() == "node") { - node = XMLLoadNode(reader); + if (reader->name() == QStringLiteral("width")) { + video_width = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("height")) { + video_height = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("timebase")) { + video_timebase = rational::fromString(reader->readElementText()); } else { - node = viewer_output_; - } - - if (node) { - node->Load(reader, output_ptrs, desired_connections, footage_connections, cancelled, reader->name().toString()); - - AddNode(node); + reader->skipCurrentElement(); } } + + set_video_params(VideoParams(video_width, video_height, video_timebase)); + } else if (reader->name() == QStringLiteral("audio")) { + int rate; + uint64_t layout; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("rate")) { + rate = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("layout")) { + layout = reader->readElementText().toULongLong(); + } else { + reader->skipCurrentElement(); + } + } + + set_audio_params(AudioParams(rate, layout)); + } else if (reader->name() == QStringLiteral("node") || reader->name() == QStringLiteral("viewer")) { + Node* node; + + if (reader->name() == QStringLiteral("node")) { + node = XMLLoadNode(reader); + } else { + node = viewer_output_; + } + + if (node) { + node->Load(reader, output_ptrs, desired_connections, footage_connections, cancelled); + + AddNode(node); + } + } else { + reader->skipCurrentElement(); } } diff --git a/app/project/project.cpp b/app/project/project.cpp index 5b7231d95..7f98197b9 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -35,29 +35,32 @@ Project::Project() void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) { + qDebug() << "Hello?"; + QHash footage_ptrs; QList footage_connections; - XMLReadLoop(reader, "project") { - if (reader->isStartElement()) { - if (reader->name() == "folder") { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("folder")) { - // Assume this folder is our root - root_.Load(reader, footage_ptrs, footage_connections, cancelled); + // Assume this folder is our root + root_.Load(reader, footage_ptrs, footage_connections, cancelled); - } else if (reader->name() == "colormanagement") { + } else if (reader->name() == QStringLiteral("colormanagement")) { - // Read color management info - XMLReadLoop(reader, "colormanagement") { - if (reader->name() == "config") { - reader->readNext(); - set_ocio_config(reader->text().toString()); - } else if (reader->name() == "default") { - reader->readNext(); - set_default_input_colorspace(reader->text().toString()); - } + // Read color management info + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("config")) { + set_ocio_config(reader->readElementText()); + } else if (reader->name() == QStringLiteral("default")) { + set_default_input_colorspace(reader->readElementText()); + } else { + reader->skipCurrentElement(); } } + + } else { + reader->skipCurrentElement(); } } @@ -97,7 +100,7 @@ QString Project::name() const if (filename_.isEmpty()) { return tr("(untitled)"); } else { - return QFileInfo(filename_).baseName(); + return QFileInfo(filename_).completeBaseName(); } } diff --git a/app/project/projectloadmanager.cpp b/app/project/projectloadmanager.cpp index a0062f61a..9dd6bbbfe 100644 --- a/app/project/projectloadmanager.cpp +++ b/app/project/projectloadmanager.cpp @@ -4,6 +4,8 @@ #include #include +#include "common/xmlutils.h" + ProjectLoadManager::ProjectLoadManager(const QString &filename) : filename_(filename) { @@ -17,28 +19,32 @@ void ProjectLoadManager::Action() if (project_file.open(QFile::ReadOnly | QFile::Text)) { QXmlStreamReader reader(&project_file); - while (!reader.atEnd()) { - reader.readNext(); + qDebug() << "Hello?"; - if (reader.isStartElement()) { - if (reader.name() == "version") { - reader.readNext(); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("olive")) { + while(XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("version")) { + qDebug() << "Project version:" << reader.readElementText(); + } else if (reader.name() == QStringLiteral("project")) { + ProjectPtr project = std::make_shared(); - qDebug() << "Project version:" << reader.text(); - } else if (reader.name() == "project") { - ProjectPtr project = std::make_shared(); + project->set_filename(filename_); - project->set_filename(filename_); + project->Load(&reader, &IsCancelled()); - project->Load(&reader, &IsCancelled()); + // Ensure project is in main thread + moveToThread(qApp->thread()); - // Ensure project is in main thread - moveToThread(qApp->thread()); - - if (!IsCancelled()) { - emit ProjectLoaded(project); + if (!IsCancelled()) { + emit ProjectLoaded(project); + } + } else { + reader.skipCurrentElement(); } } + } else { + reader.skipCurrentElement(); } } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 571559926..9a1ce07cc 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -186,17 +186,25 @@ void NodeView::Paste() QList desired_connections; QList footage_connections; - XMLReadLoop((&reader), QStringLiteral("olive")) { - if (reader.name() == QStringLiteral("node")) { - Node* node = XMLLoadNode(&reader); + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("olive")) { + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("node")) { + Node* node = XMLLoadNode(&reader); - if (node) { - node->Load(&reader, output_ptrs, desired_connections, footage_connections, nullptr, reader.name().toString()); + if (node) { + node->Load(&reader, output_ptrs, desired_connections, footage_connections, nullptr); - graph_->AddNode(node); + graph_->AddNode(node); - pasted_nodes.append(node); + pasted_nodes.append(node); + } + } else { + reader.skipCurrentElement(); + } } + } else { + reader.skipCurrentElement(); } } From 69e65f6784a002377bf7ef3c19fb5d3b4b5d848d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 17 Mar 2020 03:13:14 +1100 Subject: [PATCH 33/43] viewer: fixed occasional OCIO bug Addresses a flaw where if the user changed the OCIO "display", Olive wouldn't check whether the currently selected "view" still existed in the new display or not which would create an invalid color processor if not. This commit now checks whether the new display contains the current view, and sets the default view for the new display if not. --- app/widget/viewer/viewerglwidget.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/widget/viewer/viewerglwidget.cpp b/app/widget/viewer/viewerglwidget.cpp index ca34dafd8..ccd5ed595 100644 --- a/app/widget/viewer/viewerglwidget.cpp +++ b/app/widget/viewer/viewerglwidget.cpp @@ -129,6 +129,14 @@ void ViewerGLWidget::SetImage(const QString &fn) void ViewerGLWidget::SetOCIODisplay(const QString &display) { ocio_display_ = display; + + // Determine if the selected view is available in this display + if (color_manager_ + && !color_manager_->ListAvailableViews(ocio_display_).contains(ocio_view_)) { + // If not, set to the default view for this display + ocio_view_ = color_manager_->GetDefaultView(ocio_display_); + } + SetupColorProcessor(); update(); } From eb871ceccce1d7002a80e72bf78b02c02b500184 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 14:09:19 +1100 Subject: [PATCH 34/43] waveformview: implemented rectified waveforms --- app/config/config.cpp | 1 + app/widget/viewer/waveformview.cpp | 43 +++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/config/config.cpp b/app/config/config.cpp index f9c1bcb46..88d80d4c0 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -77,6 +77,7 @@ void Config::SetDefaults() config_map_["DefaultViewerDivider"] = 2; config_map_["AutoSelectDivider"] = false; config_map_["SetNameWithMarker"] = false; + config_map_["RectifiedWaveforms"] = false; config_map_["DropWithoutSequenceBehavior"] = TimelineWidget::kDWSAsk; config_map_["DiskCachePath"] = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp index e6305bc16..c57ea502f 100644 --- a/app/widget/viewer/waveformview.cpp +++ b/app/widget/viewer/waveformview.cpp @@ -5,6 +5,7 @@ #include #include "common/clamp.h" +#include "config/config.h" WaveformView::WaveformView(QWidget *parent) : SeekableWidget(parent), @@ -65,12 +66,23 @@ void WaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const doub int line_x = i + rect.x(); for (int j=0;jdrawLine(line_x, - channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), - line_x, - channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); + int diff = qRound((summary.at(j).max - summary.at(j).min) * channel_height); + + painter->drawLine(line_x, + channel_bottom - diff, + line_x, + channel_bottom); + } else{ + int channel_mid = rect.y() + channel_height * j + channel_half_height; + + painter->drawLine(line_x, + channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), + line_x, + channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); + } } } } @@ -120,12 +132,23 @@ void WaveformView::paintEvent(QPaintEvent *event) params.channel_count()); for (int i=0;i(channel_half_height), - x, - channel_mid + samples.at(i).max * static_cast(channel_half_height)); + int diff = qRound((samples.at(i).max - samples.at(i).min) * channel_height); + + p.drawLine(x, + channel_bottom - diff, + x, + channel_bottom); + } else { + int channel_mid = channel_height * i + channel_half_height; + + p.drawLine(x, + channel_mid + samples.at(i).min * static_cast(channel_half_height), + x, + channel_mid + samples.at(i).max * static_cast(channel_half_height)); + } drew++; } From abb4c440fb67413c0870bf0ad43d2d0dea2b5eba Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 14:09:31 +1100 Subject: [PATCH 35/43] core: added signalling for timecode display changes Allows targeted application-wide UI updates for when the user changes the timecode display so that the response is immediate. --- app/common/timecodefunctions.cpp | 10 ---------- app/common/timecodefunctions.h | 3 --- app/core.cpp | 12 ++++++++++++ app/core.h | 16 ++++++++++++++++ app/project/item/footage/footage.cpp | 8 +++----- app/project/item/sequence/sequence.cpp | 2 +- app/widget/playbackcontrols/playbackcontrols.cpp | 16 +++++++++++++--- app/widget/playbackcontrols/playbackcontrols.h | 4 +++- app/widget/slider/timeslider.cpp | 12 ++++++++++-- app/widget/slider/timeslider.h | 4 ++++ app/widget/timelinewidget/tool/import.cpp | 2 +- app/widget/timelinewidget/tool/pointer.cpp | 2 +- app/widget/timelinewidget/tool/slip.cpp | 2 +- app/widget/timeruler/timeruler.cpp | 5 ++++- app/window/mainwindow/mainmenu.cpp | 8 ++------ app/window/mainwindow/mainmenu.h | 1 - 16 files changed, 71 insertions(+), 36 deletions(-) diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index c2725290b..86c9eaf36 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -258,13 +258,3 @@ int64_t Timecode::time_to_timestamp(const double &time, const rational &timebase { return qRound64(time * timebase.flipped().toDouble()); } - -Timecode::Display Timecode::CurrentDisplay() -{ - return static_cast(Config::Current()["TimecodeDisplay"].toInt()); -} - -void Timecode::SetCurrentDisplay(Timecode::Display d) -{ - Config::Current()["TimecodeDisplay"] = d; -} diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index e9bb4d35d..0d1748394 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -45,9 +45,6 @@ public: kMilliseconds }; - static Display CurrentDisplay(); - static void SetCurrentDisplay(Display d); - /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ diff --git a/app/core.cpp b/app/core.cpp index e87e1d755..7854a5271 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -482,6 +482,18 @@ Folder *Core::GetSelectedFolderInActiveProject() } } +Timecode::Display Core::GetTimecodeDisplay() const +{ + return static_cast(Config::Current()["TimecodeDisplay"].toInt()); +} + +void Core::SetTimecodeDisplay(Timecode::Display d) +{ + Config::Current()["TimecodeDisplay"] = d; + + emit TimecodeDisplayChanged(d); +} + void Core::SetProjectModified(bool e) { main_window()->setWindowModified(e); diff --git a/app/core.h b/app/core.h index 9fdb85501..5cb9bc6cf 100644 --- a/app/core.h +++ b/app/core.h @@ -26,6 +26,7 @@ #include #include "common/rational.h" +#include "common/timecodefunctions.h" #include "project/item/footage/footage.h" #include "project/item/sequence/sequence.h" #include "project/project.h" @@ -125,6 +126,16 @@ public: ProjectViewModel* GetActiveProjectModel(); Folder* GetSelectedFolderInActiveProject(); + /** + * @brief Gets current timecode display mode + */ + Timecode::Display GetTimecodeDisplay() const; + + /** + * @brief Sets current timecode display mode + */ + void SetTimecodeDisplay(Timecode::Display d); + /** * @brief Sets state to "modified" so that the GUI will prompt the user to save before closing * @@ -269,6 +280,11 @@ signals: */ void SnappingChanged(const bool& b); + /** + * @brief Signal emitted when the default timecode display mode changed + */ + void TimecodeDisplayChanged(Timecode::Display d); + private: /** * @brief Get the file filter than can be used with QFileDialog to open and save compatible projects diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index e0b98a9ac..4434eb531 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -23,9 +23,9 @@ #include #include "codec/decoder.h" -#include "common/timecodefunctions.h" #include "common/xmlutils.h" #include "config/config.h" +#include "core.h" #include "ui/icons/icons.h" Footage::Footage() @@ -40,8 +40,6 @@ Footage::~Footage() void Footage::Load(QXmlStreamReader *reader, QHash& footage_ptrs, QList&, const QAtomicInt* cancelled) { - qDebug() << "Hello?"; - QXmlStreamAttributes attributes = reader->attributes(); foreach (const QXmlStreamAttribute& attr, attributes) { @@ -234,12 +232,12 @@ QString Footage::duration() return Timecode::timestamp_to_timecode(duration, frame_rate_timebase, - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); } else if (streams_.first()->type() == Stream::kAudio) { AudioStreamPtr audio_stream = std::static_pointer_cast(streams_.first()); // If we're showing in a timecode, we prefer showing audio in seconds instead - Timecode::Display display = Timecode::CurrentDisplay(); + Timecode::Display display = Core::instance()->GetTimecodeDisplay(); if (display == Timecode::kTimecodeDropFrame || display == Timecode::kTimecodeNonDropFrame) { display = Timecode::kTimecodeSeconds; diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index f15344310..00101f126 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -199,7 +199,7 @@ QString Sequence::duration() int64_t timestamp = Timecode::time_to_timestamp(timeline_length, video_params().time_base()); - return Timecode::timestamp_to_timecode(timestamp, video_params().time_base(), Timecode::CurrentDisplay()); + return Timecode::timestamp_to_timecode(timestamp, video_params().time_base(), Core::instance()->GetTimecodeDisplay()); } QString Sequence::rate() diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 523a7a9e5..400fdedba 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -24,7 +24,7 @@ #include #include -#include "common/timecodefunctions.h" +#include "core.h" #include "config/config.h" #include "ui/icons/icons.h" @@ -120,6 +120,8 @@ PlaybackControls::PlaybackControls(QWidget *parent) : UpdateIcons(); SetTimebase(0); + + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &PlaybackControls::TimecodeChanged); } void PlaybackControls::SetTimecodeEnabled(bool enabled) @@ -147,9 +149,11 @@ void PlaybackControls::SetEndTime(const int64_t &r) return; } - end_tc_lbl_->setText(Timecode::timestamp_to_timecode(r, + end_time_ = r; + + end_tc_lbl_->setText(Timecode::timestamp_to_timecode(end_time_, time_base_, - Timecode::CurrentDisplay())); + Core::instance()->GetTimecodeDisplay())); } void PlaybackControls::ShowPauseButton() @@ -181,3 +185,9 @@ void PlaybackControls::UpdateIcons() next_frame_btn_->setIcon(icon::NextFrame); go_to_end_btn_->setIcon(icon::GoToEnd); } + +void PlaybackControls::TimecodeChanged() +{ + // Update end time + SetEndTime(end_time_); +} diff --git a/app/widget/playbackcontrols/playbackcontrols.h b/app/widget/playbackcontrols/playbackcontrols.h index 2c9701d91..9ad983f0b 100644 --- a/app/widget/playbackcontrols/playbackcontrols.h +++ b/app/widget/playbackcontrols/playbackcontrols.h @@ -101,6 +101,8 @@ private: TimeSlider* cur_tc_lbl_; QLabel* end_tc_lbl_; + int64_t end_time_; + rational time_base_; QPushButton* go_to_start_btn_; @@ -113,7 +115,7 @@ private: QStackedWidget* playpause_stack_; private slots: - + void TimecodeChanged(); }; diff --git a/app/widget/slider/timeslider.cpp b/app/widget/slider/timeslider.cpp index 5899596db..4d59c007c 100644 --- a/app/widget/slider/timeslider.cpp +++ b/app/widget/slider/timeslider.cpp @@ -1,11 +1,14 @@ #include "timeslider.h" #include "common/timecodefunctions.h" +#include "core.h" TimeSlider::TimeSlider(QWidget *parent) : IntegerSlider(parent) { SetMinimum(0); + + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &TimeSlider::TimecodeDisplayChanged); } void TimeSlider::SetTimebase(const rational &timebase) @@ -25,10 +28,15 @@ QString TimeSlider::ValueToString(const QVariant &v) return Timecode::timestamp_to_timecode(v.toLongLong(), timebase_, - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); } QVariant TimeSlider::StringToValue(const QString &s, bool *ok) { - return QVariant::fromValue(Timecode::timecode_to_timestamp(s, timebase_, Timecode::CurrentDisplay(), ok)); + return QVariant::fromValue(Timecode::timecode_to_timestamp(s, timebase_, Core::instance()->GetTimecodeDisplay(), ok)); +} + +void TimeSlider::TimecodeDisplayChanged() +{ + UpdateLabel(Value()); } diff --git a/app/widget/slider/timeslider.h b/app/widget/slider/timeslider.h index 331572d96..a69c0f31b 100644 --- a/app/widget/slider/timeslider.h +++ b/app/widget/slider/timeslider.h @@ -6,6 +6,7 @@ class TimeSlider : public IntegerSlider { + Q_OBJECT public: TimeSlider(QWidget* parent = nullptr); @@ -19,6 +20,9 @@ protected: private: rational timebase_; +private slots: + void TimecodeDisplayChanged(); + }; #endif // TIMESLIDER_H diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 366f46277..92293cbd4 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -143,7 +143,7 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) int64_t earliest_timestamp = Timecode::time_to_timestamp(earliest_ghost, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay()); + Core::instance()->GetTimecodeDisplay()); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way // of the cursor) diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index e88cad283..80fd34194 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -404,7 +404,7 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay(), + Core::instance()->GetTimecodeDisplay(), true); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index b13f5297e..299ed5f22 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -54,7 +54,7 @@ void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->timebase()); QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, parent()->timebase(), - Timecode::CurrentDisplay(), + Core::instance()->GetTimecodeDisplay(), true); // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way // of the cursor) diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index c9ff31939..25b504995 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -47,6 +47,9 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare // Text visibility affects height, so we set that here UpdateHeight(); + + // Force update if the default timecode display mode changes + connect(Core::instance(), &Core::TimecodeDisplayChanged, this, static_cast(&TimeRuler::update)); } void TimeRuler::SetCacheStatusLength(const rational &length) @@ -203,7 +206,7 @@ void TimeRuler::paintEvent(QPaintEvent *) if (text_visible_) { QRect text_rect; Qt::Alignment text_align; - QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase(), Timecode::CurrentDisplay()); + QString timecode_str = Timecode::timestamp_to_timecode(ScreenToUnit(i), timebase(), Core::instance()->GetTimecodeDisplay()); int timecode_width = QFontMetricsWidth(fm, timecode_str); int timecode_left; diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 95c4eba68..1034fe378 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -100,9 +100,6 @@ MainMenu::MainMenu(QMainWindow *parent) : view_show_all_item_ = view_menu_->AddItem("showall", nullptr, nullptr, "\\"); view_show_all_item_->setCheckable(true); view_menu_->addSeparator(); - view_rectified_waveforms_item_ = view_menu_->AddItem("rectifiedwaveforms", nullptr, nullptr); - view_rectified_waveforms_item_->setCheckable(true); - view_menu_->addSeparator(); frame_view_mode_group_ = new QActionGroup(this); @@ -327,7 +324,7 @@ void MainMenu::TimecodeDisplayTriggered() Timecode::Display display = static_cast(action->data().toInt()); // Set the current display mode - Timecode::SetCurrentDisplay(display); + Core::instance()->SetTimecodeDisplay(display); } void MainMenu::FileMenuAboutToShow() @@ -343,7 +340,7 @@ void MainMenu::ViewMenuAboutToShow() // Ensure checked timecode display mode is correct QList timecode_display_actions = frame_view_mode_group_->actions(); foreach (QAction* a, timecode_display_actions) { - if (a->data() == Timecode::CurrentDisplay()) { + if (a->data() == Core::instance()->GetTimecodeDisplay()) { a->setChecked(true); break; } @@ -553,7 +550,6 @@ void MainMenu::Retranslate() view_increase_track_height_item_->setText(tr("Increase Track Height")); view_decrease_track_height_item_->setText(tr("Decrease Track Height")); view_show_all_item_->setText(tr("Toggle Show All")); - view_rectified_waveforms_item_->setText(tr("Rectified Waveforms")); view_timecode_view_frames_item_->setText(tr("Frames")); view_timecode_view_dropframe_item_->setText(tr("Drop Frame")); view_timecode_view_nondropframe_item_->setText(tr("Non-Drop Frame")); diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index 2f4eea741..d45b0818a 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -181,7 +181,6 @@ private: QAction* view_increase_track_height_item_; QAction* view_decrease_track_height_item_; QAction* view_show_all_item_; - QAction* view_rectified_waveforms_item_; QActionGroup* frame_view_mode_group_; QAction* view_timecode_view_dropframe_item_; QAction* view_timecode_view_nondropframe_item_; From 27d7822fa94284b4f1b154f8afe393c5fdf7b2d3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 14:23:10 +1100 Subject: [PATCH 36/43] preferences: added setting for rectified waveforms --- .../preferences/tabs/preferencesgeneraltab.cpp | 12 ++++++++++-- app/dialog/preferences/tabs/preferencesgeneraltab.h | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 2412619cd..cbb75d12e 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -2,8 +2,6 @@ #include #include -#include -#include #include #include "common/autoscroll.h" @@ -74,6 +72,14 @@ PreferencesGeneralTab::PreferencesGeneralTab() row++; + general_layout->addWidget(new QLabel(tr("Rectified Waveforms:")), row, 0); + + rectified_waveforms_ = new QCheckBox(); + rectified_waveforms_->setChecked(Config::Current()["RectifiedWaveforms"].toBool()); + general_layout->addWidget(rectified_waveforms_, row, 1); + + row++; + general_layout->addWidget(new QLabel(tr("Default Still Image Length:")), row, 0); default_still_length_ = new FloatSlider(); @@ -101,6 +107,8 @@ void PreferencesGeneralTab::Accept() Config::Current()["DefaultSequenceAudioFrequency"] = default_sequence_.audio_params().sample_rate(); Config::Current()["DefaultSequenceAudioLayout"] = QVariant::fromValue(default_sequence_.audio_params().channel_layout()); + Config::Current()["RectifiedWaveforms"] = rectified_waveforms_->isChecked(); + Config::Current()["Autoscroll"] = autoscroll_method_->currentData(); Config::Current()["DefaultStillLength"] = QVariant::fromValue(rational::fromDouble(default_still_length_->GetValue())); diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index b05633040..f6d6d8f2a 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -1,6 +1,7 @@ #ifndef PREFERENCESGENERALTAB_H #define PREFERENCESGENERALTAB_H +#include #include #include @@ -27,6 +28,8 @@ private: QComboBox* autoscroll_method_; + QCheckBox* rectified_waveforms_; + FloatSlider* default_still_length_; /** From 5bdee74205aac0f20147d4aab449e00a131b41d3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 14:23:35 +1100 Subject: [PATCH 37/43] waveformview: corrected rectified waveform algorithm Fixed bug where code erroneously multipled by full channel height. This is an incorrect assumption since the full range of -1.0 to 1.0 is 2.0 which means the difference needs to be halfed to create a range from 0.0 to 1.0. --- app/widget/viewer/waveformview.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp index c57ea502f..ec695a4c1 100644 --- a/app/widget/viewer/waveformview.cpp +++ b/app/widget/viewer/waveformview.cpp @@ -69,7 +69,7 @@ void WaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const doub if (Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool()) { int channel_bottom = rect.y() + channel_height * (j + 1); - int diff = qRound((summary.at(j).max - summary.at(j).min) * channel_height); + int diff = qRound((summary.at(j).max - summary.at(j).min) * channel_half_height); painter->drawLine(line_x, channel_bottom - diff, @@ -135,7 +135,7 @@ void WaveformView::paintEvent(QPaintEvent *event) if (Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool()) { int channel_bottom = channel_height * (i + 1); - int diff = qRound((samples.at(i).max - samples.at(i).min) * channel_height); + int diff = qRound((samples.at(i).max - samples.at(i).min) * channel_half_height); p.drawLine(x, channel_bottom - diff, From fed17e35fa2d496d9898165f12b1ddd536108ae0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 14:44:05 +1100 Subject: [PATCH 38/43] audiowaveformview: optimizes waveform to only redraw when necessary Draws to a pixmap only when the waveform changes so it doesn't have to be regenerated on every draw. Also renames WaveformView to AudioWaveformView --- .../view/timelineviewblockitem.cpp | 4 +- app/widget/viewer/CMakeLists.txt | 4 +- app/widget/viewer/audiowaveformview.cpp | 189 ++++++++++++++++++ .../{waveformview.h => audiowaveformview.h} | 9 +- app/widget/viewer/viewer.cpp | 10 +- app/widget/viewer/viewer.h | 4 +- app/widget/viewer/waveformview.cpp | 171 ---------------- 7 files changed, 207 insertions(+), 184 deletions(-) create mode 100644 app/widget/viewer/audiowaveformview.cpp rename app/widget/viewer/{waveformview.h => audiowaveformview.h} (78%) delete mode 100644 app/widget/viewer/waveformview.cpp diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index a0792c6a9..c70c979da 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -32,7 +32,7 @@ #include "common/qtutils.h" #include "config/config.h" #include "node/block/transition/transition.h" -#include "widget/viewer/waveformview.h" +#include "widget/viewer/audiowaveformview.h" TimelineViewBlockItem::TimelineViewBlockItem(Block *block, QGraphicsItem* parent) : TimelineViewRect(parent), @@ -93,7 +93,7 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI QByteArray w = wave_file.readAll(); // FIXME: Hardcoded channel count - WaveformView::DrawWaveform(painter, + AudioWaveformView::DrawWaveform(painter, rect().toRect(), this->GetScale(), reinterpret_cast(w.constData()), diff --git a/app/widget/viewer/CMakeLists.txt b/app/widget/viewer/CMakeLists.txt index 6d99220dc..16693f4de 100644 --- a/app/widget/viewer/CMakeLists.txt +++ b/app/widget/viewer/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/viewer/audiowaveformview.h + widget/viewer/audiowaveformview.cpp widget/viewer/footageviewer.h widget/viewer/footageviewer.cpp widget/viewer/viewer.h @@ -24,7 +26,5 @@ set(OLIVE_SOURCES widget/viewer/viewerglwidget.cpp widget/viewer/viewersizer.h widget/viewer/viewersizer.cpp - widget/viewer/waveformview.h - widget/viewer/waveformview.cpp PARENT_SCOPE ) diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp new file mode 100644 index 000000000..dd7ac643b --- /dev/null +++ b/app/widget/viewer/audiowaveformview.cpp @@ -0,0 +1,189 @@ +#include "audiowaveformview.h" + +#include +#include +#include + +#include "common/clamp.h" +#include "config/config.h" + +AudioWaveformView::AudioWaveformView(QWidget *parent) : + SeekableWidget(parent), + backend_(nullptr) +{ + setAutoFillBackground(true); + setBackgroundRole(QPalette::Base); +} + +void AudioWaveformView::SetBackend(AudioRenderBackend *backend) +{ + if (backend_) { + disconnect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&AudioWaveformView::update)); + disconnect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged); + + SetTimebase(0); + } + + backend_ = backend; + + if (backend_) { + connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&AudioWaveformView::update)); + connect(backend_, &AudioRenderBackend::ParamsChanged, this, &AudioWaveformView::BackendParamsChanged); + + SetTimebase(backend_->params().time_base()); + } + + update(); +} + +void AudioWaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const SampleSummer::Sum* samples, int nb_samples, int channels) +{ + int sample_index, next_sample_index = 0; + + QVector summary; + int summary_index = -1; + + int channel_height = rect.height() / channels; + int channel_half_height = channel_height / 2; + + for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / scale) * channels); + + if (summary_index != sample_index) { + summary = SampleSummer::ReSumSamples(&samples[sample_index], + qMax(channels, next_sample_index - sample_index), + channels); + summary_index = sample_index; + } + + int line_x = i + rect.x(); + + for (int j=0;jdrawLine(line_x, + channel_bottom - diff, + line_x, + channel_bottom); + } else{ + int channel_mid = rect.y() + channel_height * j + channel_half_height; + + painter->drawLine(line_x, + channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), + line_x, + channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); + } + } + } +} + +void AudioWaveformView::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + if (!backend_ || backend_->CachePathName().isEmpty() || !backend_->params().is_valid()) { + return; + } + + const AudioRenderingParams& params = backend_->params(); + + if (cached_size_ != size() + || cached_scale_ != GetScale() + || cached_scroll_ != GetScroll()) { + + cached_waveform_ = QPixmap(size()); + cached_waveform_.fill(Qt::transparent); + + QFile fs(backend_->CachePathName()); + + if (fs.open(QFile::ReadOnly)) { + + QPainter wave_painter(&cached_waveform_); + + // FIXME: Hardcoded color + wave_painter.setPen(Qt::green); + + int channel_height = height() / params.channel_count(); + int channel_half_height = channel_height / 2; + + int drew = 0; + + fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); + + for (int x=0; x samples = SampleSummer::SumSamples(reinterpret_cast(read_buffer.constData()), + samples_len, + params.channel_count()); + + for (int i=0;i(channel_half_height), + x, + channel_mid + samples.at(i).max * static_cast(channel_half_height)); + } + + drew++; + } + } + + cached_size_ = size(); + cached_scale_ = GetScale(); + cached_scroll_ = GetScroll(); + + fs.close(); + + } + } + + QPainter p(this); + + // Draw in/out points + DrawTimelinePoints(&p); + + // Draw cached waveform pixmap + p.drawPixmap(0, 0, cached_waveform_); + + // Draw playhead + p.setPen(GetPlayheadColor()); + + int playhead_x = UnitToScreen(GetTime()); + p.drawLine(playhead_x, 0, playhead_x, height()); +} + +void AudioWaveformView::BackendParamsChanged() +{ + SetTimebase(backend_->params().time_base()); +} diff --git a/app/widget/viewer/waveformview.h b/app/widget/viewer/audiowaveformview.h similarity index 78% rename from app/widget/viewer/waveformview.h rename to app/widget/viewer/audiowaveformview.h index 1e92e2d1d..283670c1d 100644 --- a/app/widget/viewer/waveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -8,11 +8,11 @@ #include "render/backend/audiorenderbackend.h" #include "widget/timeruler/seekablewidget.h" -class WaveformView : public SeekableWidget +class AudioWaveformView : public SeekableWidget { Q_OBJECT public: - WaveformView(QWidget* parent = nullptr); + AudioWaveformView(QWidget* parent = nullptr); //void SetData(const QString& file, const AudioRenderingParams& params); @@ -26,6 +26,11 @@ protected: private: AudioRenderBackend* backend_; + QPixmap cached_waveform_; + QSize cached_size_; + double cached_scale_; + int cached_scroll_; + private slots: void BackendParamsChanged(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 4ff69b2d2..d2408630b 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -61,7 +61,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : sizer_->SetWidget(gl_widget_); // Create waveform view when audio is connected and video isn't - waveform_view_ = new WaveformView(); + waveform_view_ = new AudioWaveformView(); stack_->addWidget(waveform_view_); // Create time ruler @@ -70,7 +70,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : // Create scrollbar layout->addWidget(scrollbar()); connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); - connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &WaveformView::SetScroll); + connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll); // Create lower controls controls_ = new PlaybackControls(); @@ -96,7 +96,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : audio_renderer_ = new AudioBackend(this); waveform_view_->SetBackend(audio_renderer_); - connect(waveform_view_, &WaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); + connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); connect(PixelFormat::instance(), &PixelFormat::FormatChanged, this, &ViewerWidget::UpdateRendererParameters); @@ -306,8 +306,8 @@ void ViewerWidget::PushScrubbedAudio() QIODevice* audio_src = audio_renderer_->GetAudioPullDevice(); if (audio_src && audio_src->open(QFile::ReadOnly)) { - // Try to get one "frame" of audio - int size_of_sample = audio_renderer_->params().time_to_bytes(timebase()); + // FIXME: Hardcoded scrubbing interval (20ms) + int size_of_sample = audio_renderer_->params().time_to_bytes(rational(20, 1000)); // Push audio audio_src->seek(audio_renderer_->params().time_to_bytes(GetTime())); diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 88aab38b7..4c3c642dd 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -28,6 +28,7 @@ #include #include +#include "audiowaveformview.h" #include "common/rational.h" #include "node/output/viewer/viewer.h" #include "render/backend/opengl/openglbackend.h" @@ -35,7 +36,6 @@ #include "render/backend/audio/audiobackend.h" #include "viewerglwidget.h" #include "viewersizer.h" -#include "waveformview.h" #include "widget/playbackcontrols/playbackcontrols.h" #include "widget/timebased/timebased.h" @@ -159,7 +159,7 @@ private: bool time_changed_from_timer_; - WaveformView* waveform_view_; + AudioWaveformView* waveform_view_; private slots: void PlaybackTimerUpdate(); diff --git a/app/widget/viewer/waveformview.cpp b/app/widget/viewer/waveformview.cpp deleted file mode 100644 index ec695a4c1..000000000 --- a/app/widget/viewer/waveformview.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "waveformview.h" - -#include -#include -#include - -#include "common/clamp.h" -#include "config/config.h" - -WaveformView::WaveformView(QWidget *parent) : - SeekableWidget(parent), - backend_(nullptr) -{ - setAutoFillBackground(true); - setBackgroundRole(QPalette::Base); -} - -void WaveformView::SetBackend(AudioRenderBackend *backend) -{ - if (backend_) { - disconnect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); - disconnect(backend_, &AudioRenderBackend::ParamsChanged, this, &WaveformView::BackendParamsChanged); - - SetTimebase(0); - } - - backend_ = backend; - - if (backend_) { - connect(backend_, &AudioRenderBackend::QueueComplete, this, static_cast(&WaveformView::update)); - connect(backend_, &AudioRenderBackend::ParamsChanged, this, &WaveformView::BackendParamsChanged); - - SetTimebase(backend_->params().time_base()); - } - - update(); -} - -void WaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const SampleSummer::Sum* samples, int nb_samples, int channels) -{ - int sample_index, next_sample_index = 0; - - QVector summary; - int summary_index = -1; - - int channel_height = rect.height() / channels; - int channel_half_height = channel_height / 2; - - for (int i=0;i(SampleSummer::kSumSampleRate) * static_cast(i+1) / scale) * channels); - - if (summary_index != sample_index) { - summary = SampleSummer::ReSumSamples(&samples[sample_index], - qMax(channels, next_sample_index - sample_index), - channels); - summary_index = sample_index; - } - - int line_x = i + rect.x(); - - for (int j=0;jdrawLine(line_x, - channel_bottom - diff, - line_x, - channel_bottom); - } else{ - int channel_mid = rect.y() + channel_height * j + channel_half_height; - - painter->drawLine(line_x, - channel_mid + clamp(qRound(summary.at(j).min * channel_half_height), -channel_half_height, channel_half_height), - line_x, - channel_mid + clamp(qRound(summary.at(j).max * channel_half_height), -channel_half_height, channel_half_height)); - } - } - } -} - -void WaveformView::paintEvent(QPaintEvent *event) -{ - QWidget::paintEvent(event); - - if (!backend_ || backend_->CachePathName().isEmpty() || !backend_->params().is_valid()) { - return; - } - - const AudioRenderingParams& params = backend_->params(); - - QFile fs(backend_->CachePathName()); - - if (fs.open(QFile::ReadOnly)) { - - QPainter p(this); - - DrawTimelinePoints(&p); - - // FIXME: Hardcoded color - p.setPen(Qt::green); - - int channel_height = height() / params.channel_count(); - int channel_half_height = channel_height / 2; - - int drew = 0; - - fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); - - for (int x=0; x samples = SampleSummer::SumSamples(reinterpret_cast(read_buffer.constData()), - samples_len, - params.channel_count()); - - for (int i=0;i(channel_half_height), - x, - channel_mid + samples.at(i).max * static_cast(channel_half_height)); - } - - drew++; - } - } - - fs.close(); - - // Draw playhead - p.setPen(GetPlayheadColor()); - - int playhead_x = UnitToScreen(GetTime()); - p.drawLine(playhead_x, 0, playhead_x, height()); - - } -} - -void WaveformView::BackendParamsChanged() -{ - SetTimebase(rational(1, backend_->params().sample_rate())); -} From 40073dde62ad3d8346dfb316e58ab15355d148bf Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 15:02:43 +1100 Subject: [PATCH 39/43] cmake: added crash handler target to install --- app/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 03494d590..99e1e6152 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -148,11 +148,6 @@ else() qt5_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) endif() -if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} RUNTIME DESTINATION bin) - install(FILES ${OLIVE_QM_FILES} DESTINATION share/olive-editor/ts) -endif() - add_subdirectory(packaging) if(DOXYGEN_FOUND) @@ -184,6 +179,11 @@ else() ) endif() +if(UNIX AND NOT APPLE) + install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) + install(FILES ${OLIVE_QM_FILES} DESTINATION share/olive-editor/ts) +endif() + target_link_libraries( ${OLIVE_CRASH_TARGET} PRIVATE From 04afeee9534fdd587a141bd17229539ce620400d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 08:17:23 -0700 Subject: [PATCH 40/43] crashhandler: enabled mac support --- app/common/crashhandler.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/common/crashhandler.cpp b/app/common/crashhandler.cpp index 4844c9736..27958f543 100644 --- a/app/common/crashhandler.cpp +++ b/app/common/crashhandler.cpp @@ -13,7 +13,7 @@ #include #include #include -#elif defined(Q_OS_LINUX) +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) #include #endif @@ -100,9 +100,7 @@ void crash_handler(int sig) { } SymCleanup(process); -#elif defined(Q_OS_MAC) - // FIXME: No Mac backtrace support yet -#elif defined(Q_OS_LINUX) +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) void *array[10]; size_t size; From 479f7efc66673484fa6ce0670e302d8483f9583e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 08:18:04 -0700 Subject: [PATCH 41/43] stream: commented out never used struct --- app/project/item/footage/stream.cpp | 8 ++++---- app/project/item/footage/stream.h | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/project/item/footage/stream.cpp b/app/project/item/footage/stream.cpp index 52a402851..4937481b2 100644 --- a/app/project/item/footage/stream.cpp +++ b/app/project/item/footage/stream.cpp @@ -137,10 +137,10 @@ QIcon Stream::IconFromType(const Stream::Type &type) return QIcon(); } -StreamID Stream::ToID() const +/*StreamID Stream::ToID() const { return StreamID(footage_->filename(), index_); -} +}*/ QMutex* Stream::index_process_lock() { @@ -160,8 +160,8 @@ void Stream::SaveCustomParameters(QXmlStreamWriter*) const { } -StreamID::StreamID(const QString &filename, const int &stream_index) : +/*StreamID::StreamID(const QString &filename, const int &stream_index) : filename_(filename), stream_index_(stream_index) { -} +}*/ diff --git a/app/project/item/footage/stream.h b/app/project/item/footage/stream.h index b3c774604..743b9e6c4 100644 --- a/app/project/item/footage/stream.h +++ b/app/project/item/footage/stream.h @@ -31,7 +31,7 @@ class Footage; -class StreamID { +/*class StreamID { public: StreamID(const QString& filename, const int& stream_index); @@ -40,7 +40,7 @@ private: int stream_index_; -}; +};*/ /** * @brief A base class for keeping metadata about a media stream. @@ -101,7 +101,7 @@ public: static QIcon IconFromType(const Type& type); - StreamID ToID() const; + //StreamID ToID() const; QMutex* index_process_lock(); From 215406c15d3928b27378f3f18513c5e63ac163f6 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 08:18:33 -0700 Subject: [PATCH 42/43] cmake: install icns file inside mac bundle --- app/CMakeLists.txt | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 99e1e6152..0c5b1fa10 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# Set Olive sources and resources set(OLIVE_SOURCES ${OLIVE_SOURCES} core.h @@ -21,9 +22,12 @@ set(OLIVE_SOURCES main.cpp ) -set(OLIVE_RESOURCES - ${OLIVE_RESOURCES} -) +if (WIN32) + set(OLIVE_RESOURCES + ${OLIVE_RESOURCES} + packaging/windows/resources.rc + ) +endif() add_subdirectory(audio) add_subdirectory(codec) @@ -43,18 +47,20 @@ add_subdirectory(undo) add_subdirectory(widget) add_subdirectory(window) +# Create main application target set(OLIVE_TARGET "olive-editor") if(APPLE) set(OLIVE_TARGET "Olive") -endif() -if (WIN32) + set(OLIVE_ICON packaging/macos/olive.icns) + set(OLIVE_RESOURCES ${OLIVE_RESOURCES} - packaging/windows/resources.rc + ${OLIVE_ICON} ) endif() +# Add executable add_executable(${OLIVE_TARGET} ${OLIVE_SOURCES} ${OLIVE_RESOURCES} @@ -62,14 +68,18 @@ add_executable(${OLIVE_TARGET} ) if(APPLE) - SET_TARGET_PROPERTIES(${OLIVE_TARGET} PROPERTIES + set_target_properties(${OLIVE_TARGET} PROPERTIES MACOSX_BUNDLE TRUE - MACOSX_FRAMEWORK_IDENTIFIER org.olivevideoeditor.Olive + MACOSX_BUNDLE_GUI_IDENTIFIER org.olivevideoeditor.Olive + MACOSX_BUNDLE_ICON_FILE olive.icns + RESOURCE "${OLIVE_ICON}" ) endif() +# Set compiler definitions target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) +# Set compiler options if(MSVC) target_compile_options( ${OLIVE_TARGET} @@ -100,6 +110,7 @@ else() ) endif() +# Set include directories target_include_directories( ${OLIVE_TARGET} PRIVATE @@ -109,6 +120,7 @@ target_include_directories( ${OPENEXR_INCLUDE_DIRS} ) +# Set link libraries target_link_libraries( ${OLIVE_TARGET} PRIVATE @@ -179,11 +191,6 @@ else() ) endif() -if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) - install(FILES ${OLIVE_QM_FILES} DESTINATION share/olive-editor/ts) -endif() - target_link_libraries( ${OLIVE_CRASH_TARGET} PRIVATE @@ -191,3 +198,11 @@ target_link_libraries( Qt5::Gui Qt5::Widgets ) + +if(UNIX AND NOT APPLE) + install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) +endif() + +if(APPLE) + # Move crash handler program inside Mac app bundle +endif() From 30b4ef8f0002c993f433267b22ecc1a8441fa7ce Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 18 Mar 2020 08:39:05 -0700 Subject: [PATCH 43/43] cmake/travis: add crash handler tool to mac app bundle and handle dependencies --- .travis/script.sh | 3 +++ app/CMakeLists.txt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.travis/script.sh b/.travis/script.sh index 9afa8e615..defeda5b2 100644 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -29,6 +29,9 @@ if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then wget -c -nv https://github.com/arl/macdeployqtfix/raw/master/macdeployqtfix.py python2 macdeployqtfix.py $BUNDLE_NAME/Contents/MacOS/Olive /usr/local/Cellar/qt5/5.*/ + # Fix deps on crash handler + python2 macdeployqtfix.py $BUNDLE_NAME/Contents/MacOS/olive-crashhandler /usr/local/Cellar/qt5/5.*/ + # Fix OpenEXR libs that seem to be missed by both macdeployqt _and_ macdeployqtfix cd $BUNDLE_NAME/Contents/Frameworks exrlib=(libImath-*.dylib libHalf-*.dylib libIexMath-*.dylib libIex-*.dylib libIlmThread-*.dylib) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0c5b1fa10..4cc08827f 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -205,4 +205,7 @@ endif() if(APPLE) # Move crash handler program inside Mac app bundle + add_custom_command(TARGET ${OLIVE_CRASH_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${OLIVE_CRASH_TARGET} $ + ) endif()