diff --git a/app/common/rational.cpp b/app/common/rational.cpp index e7020f92b..d7d374230 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -16,7 +16,7 @@ rational rational::fromDouble(const double &flt, bool* ok) } // Use FFmpeg function for the time being - AVRational r = av_d2q(flt, INT_MAX); + AVRational r = av_d2q(flt, 65535); if (r.den == 0) { // If den == 0, we were unable to convert to a rational diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index a7ecf0f78..1f9df6e3d 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -24,6 +24,7 @@ add_subdirectory(export) add_subdirectory(footageproperties) add_subdirectory(footagerelink) add_subdirectory(keyframeproperties) +add_subdirectory(markerproperties) if(OpenTimelineIO_FOUND) add_subdirectory(otioproperties) endif() diff --git a/app/dialog/markerproperties/CMakeLists.txt b/app/dialog/markerproperties/CMakeLists.txt new file mode 100644 index 000000000..222c9246e --- /dev/null +++ b/app/dialog/markerproperties/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/markerproperties/markerpropertiesdialog.h + dialog/markerproperties/markerpropertiesdialog.cpp + PARENT_SCOPE +) diff --git a/app/dialog/markerproperties/markerpropertiesdialog.cpp b/app/dialog/markerproperties/markerpropertiesdialog.cpp new file mode 100644 index 000000000..5473af3ab --- /dev/null +++ b/app/dialog/markerproperties/markerpropertiesdialog.cpp @@ -0,0 +1,152 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "markerpropertiesdialog.h" + +#include +#include +#include +#include +#include + +#include "core.h" + +namespace olive { + +#define super QDialog + +MarkerPropertiesDialog::MarkerPropertiesDialog(const QVector &markers, const rational &timebase, QWidget *parent) : + super(parent), + markers_(markers) +{ + QGridLayout *layout = new QGridLayout(this); + + int row = 0; + + QGroupBox *time_group = new QGroupBox(tr("Time")); + QGridLayout *time_layout = new QGridLayout(time_group); + + { + int time_row = 0; + + time_layout->addWidget(new QLabel(tr("In:")), time_row, 0); + + in_slider_ = new RationalSlider(); + time_layout->addWidget(in_slider_, time_row, 1); + + time_row++; + + time_layout->addWidget(new QLabel(tr("Out:")), time_row, 0); + + out_slider_ = new RationalSlider(); + time_layout->addWidget(out_slider_, time_row, 1); + } + + if (markers.size() == 1) { + in_slider_->SetValue(markers.first()->time_range().in()); + in_slider_->SetDisplayType(RationalSlider::kTime); + in_slider_->SetTimebase(timebase); + out_slider_->SetValue(markers.first()->time_range().out()); + out_slider_->SetDisplayType(RationalSlider::kTime); + out_slider_->SetTimebase(timebase); + } else { + // Markers cannot be on the same time, so we disable setting time if multiple markers are selected + in_slider_->setEnabled(false); + in_slider_->SetTristate(); + out_slider_->setEnabled(false); + out_slider_->SetTristate(); + } + + layout->addWidget(time_group, row, 0, 1, 2); + + row++; + + layout->addWidget(new QLabel(tr("Color:")), row, 0); + + color_menu_ = new ColorCodingComboBox(); + layout->addWidget(color_menu_, row, 1); + + color_menu_->SetColor(markers.first()->color()); + for (int i=1; icolor() != color_menu_->GetSelectedColor()) { + color_menu_->SetColor(-1); + break; + } + } + + row++; + + layout->addWidget(new QLabel(tr("Name:")), row, 0); + + label_edit_ = new LineEditWithFocusSignal(); + connect(label_edit_, &LineEditWithFocusSignal::Focused, this, [this]{ + label_edit_->setPlaceholderText(QString()); + }); + layout->addWidget(label_edit_, row, 1); + + // Determine what the startup label text should be + label_edit_->setText(markers.first()->name()); + for (int i=1; iname() != label_edit_->text()) { + label_edit_->clear(); + label_edit_->setPlaceholderText(tr("(multiple)")); + break; + } + } + + row++; + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &MarkerPropertiesDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &MarkerPropertiesDialog::reject); + layout->addWidget(buttons, row, 0, 1, 2); +} + +void MarkerPropertiesDialog::accept() +{ + if (in_slider_->isEnabled() && in_slider_->GetValue() > out_slider_->GetValue()) { + QMessageBox::critical(this, tr("Invalid Values"), tr("In point must be less than or equal to out point.")); + return; + } + + MultiUndoCommand *command = new MultiUndoCommand(); + + int color = color_menu_->GetSelectedColor(); + + foreach (TimelineMarker *m, markers_) { + if (color != -1) { + command->add_child(new MarkerChangeColorCommand(m, color)); + } + + if (label_edit_->placeholderText().isEmpty()) { + command->add_child(new MarkerChangeNameCommand(m, label_edit_->text())); + } + } + + if (markers_.size() == 1) { + command->add_child(new MarkerChangeTimeCommand(markers_.first(), TimeRange(in_slider_->GetValue(), out_slider_->GetValue()))); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + super::accept(); +} + +} diff --git a/app/dialog/markerproperties/markerpropertiesdialog.h b/app/dialog/markerproperties/markerpropertiesdialog.h new file mode 100644 index 000000000..218339af1 --- /dev/null +++ b/app/dialog/markerproperties/markerpropertiesdialog.h @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef MARKERPROPERTIESDIALOG_H +#define MARKERPROPERTIESDIALOG_H + +#include +#include + +#include "timeline/timelinemarker.h" +#include "widget/colorlabelmenu/colorcodingcombobox.h" +#include "widget/slider/rationalslider.h" + +namespace olive { + +class LineEditWithFocusSignal : public QLineEdit +{ + Q_OBJECT +public: + LineEditWithFocusSignal(QWidget *parent = nullptr) : + QLineEdit(parent) + { + } + +protected: + virtual void focusInEvent(QFocusEvent *e) override + { + QLineEdit::focusInEvent(e); + emit Focused(); + } + +signals: + void Focused(); + +}; + +class MarkerPropertiesDialog : public QDialog +{ + Q_OBJECT +public: + MarkerPropertiesDialog(const QVector &markers, const rational &timebase, QWidget *parent = nullptr); + +public slots: + virtual void accept() override; + +private: + QVector markers_; + + LineEditWithFocusSignal *label_edit_; + + ColorCodingComboBox *color_menu_; + + RationalSlider *in_slider_; + + RationalSlider *out_slider_; + +}; + +} + +#endif // MARKERPROPERTIESDIALOG_H diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index f5afb79fa..bb5d7188d 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -208,10 +208,22 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int // Find connected viewer node auto viewers = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn)); - if (viewers.isEmpty()) { - connected_viewer_ = nullptr; - } else { - connected_viewer_ = viewers.first(); + ViewerOutput *new_connected_viewer = viewers.isEmpty() ? nullptr : viewers.first(); + + if (new_connected_viewer != connected_viewer_) { + if (connected_viewer_) { + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + } + + connected_viewer_ = new_connected_viewer; + + if (connected_viewer_) { + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + } } super::InvalidateCache(adj, from, element, options); diff --git a/app/node/keyframe.cpp b/app/node/keyframe.cpp index 30b6e2508..277a1ed61 100644 --- a/app/node/keyframe.cpp +++ b/app/node/keyframe.cpp @@ -195,4 +195,10 @@ NodeKeyframe::BezierType NodeKeyframe::get_opposing_bezier_type(NodeKeyframe::Be } } +bool NodeKeyframe::has_sibling_at_time(const rational &t) const +{ + NodeKeyframe *k = parent()->GetKeyframeAtTimeOnTrack(input(), t, track(), element()); + return k && k != this; +} + } diff --git a/app/node/keyframe.h b/app/node/keyframe.h index a142c5015..b4c7f08e6 100644 --- a/app/node/keyframe.h +++ b/app/node/keyframe.h @@ -167,6 +167,8 @@ public: next_ = keyframe; } + bool has_sibling_at_time(const rational &t) const; + signals: /** * @brief Signal emitted when this keyframe's time is changed diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 9944a0e49..ee458d7d5 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -68,6 +68,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream } SetFlags(kDontShowInParamView); + + timeline_points_ = new TimelinePoints(this); } Node *ViewerOutput::copy() const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index c16b86160..ee207c6ec 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -137,7 +137,7 @@ public: TimelinePoints* GetTimelinePoints() { - return &timeline_points_; + return timeline_points_; } QVector GetEnabledStreamsAsReferences() const; @@ -252,7 +252,7 @@ private: AudioParams cached_audio_params_; - TimelinePoints timeline_points_; + TimelinePoints *timeline_points_; bool video_cache_enabled_; bool audio_cache_enabled_; diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index 31d3ea726..aa80921dd 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -991,6 +991,7 @@ void ProjectSerializer220403::LoadMarkerList(QXmlStreamReader *reader, TimelineM if (reader->name() == QStringLiteral("marker")) { QString name; rational in, out; + int color = Config::Current()[QStringLiteral("MarkerColor")].toInt(); XMLAttributeLoop(reader, attr) { if (attr.name() == QStringLiteral("name")) { @@ -999,10 +1000,12 @@ void ProjectSerializer220403::LoadMarkerList(QXmlStreamReader *reader, TimelineM in = rational::fromString(attr.value().toString()); } else if (attr.name() == QStringLiteral("out")) { out = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("color")) { + color = attr.value().toInt(); } } - new TimelineMarker(Config::Current()[QStringLiteral("MarkerColor")].toInt(), TimeRange(in, out), name, markers); + new TimelineMarker(color, TimeRange(in, out), name, markers); } reader->skipCurrentElement(); @@ -1011,13 +1014,16 @@ void ProjectSerializer220403::LoadMarkerList(QXmlStreamReader *reader, TimelineM void ProjectSerializer220403::SaveMarkerList(QXmlStreamWriter *writer, TimelineMarkerList *markers) const { - foreach (TimelineMarker* marker, markers->list()) { + for (auto it=markers->cbegin(); it!=markers->cend(); it++) { + TimelineMarker* marker = *it; + writer->writeStartElement(QStringLiteral("marker")); writer->writeAttribute(QStringLiteral("name"), marker->name()); - writer->writeAttribute(QStringLiteral("in"), marker->time().in().toString()); - writer->writeAttribute(QStringLiteral("out"), marker->time().out().toString()); + writer->writeAttribute(QStringLiteral("in"), marker->time_range().in().toString()); + writer->writeAttribute(QStringLiteral("out"), marker->time_range().out().toString()); + writer->writeAttribute(QStringLiteral("color"), QString::number(marker->color())); writer->writeEndElement(); // marker } diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index 2251ab3a3..bb40b7790 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -20,18 +20,20 @@ #include "timelinemarker.h" +#include "common/qtutils.h" #include "common/xmlutils.h" #include "config/config.h" #include "core.h" +#include "ui/colorcoding.h" namespace olive { TimelineMarker::TimelineMarker(int color, const TimeRange &time, const QString &name, QObject *parent) : - QObject(parent), time_(time), name_(name), color_(color) { + setParent(parent); } void TimelineMarker::set_time(const TimeRange &time) @@ -40,6 +42,17 @@ void TimelineMarker::set_time(const TimeRange &time) emit TimeChanged(time_); } +void TimelineMarker::set_time(const rational &time) +{ + set_time(TimeRange(time, time + time_.length())); +} + +bool TimelineMarker::has_sibling_at_time(const rational &t) const +{ + TimelineMarker *m = static_cast(parent())->GetMarkerAtTime(t); + return m && m != this; +} + void TimelineMarker::set_name(const QString &name) { name_ = name; @@ -49,13 +62,65 @@ void TimelineMarker::set_name(const QString &name) void TimelineMarker::set_color(int c) { color_ = c; - emit ColorChanged(color_); } -const std::vector &TimelineMarkerList::list() const +int TimelineMarker::GetMarkerHeight(const QFontMetrics &fm) { - return markers_; + return fm.height(); +} + +QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool selected) +{ + QFontMetrics fm = p->fontMetrics(); + + int marker_height = GetMarkerHeight(fm); + int marker_width = QtUtils::QFontMetricsWidth(fm, QStringLiteral("H")); + + int half_width = marker_width / 2; + + QColor c = ColorCoding::GetColor(color()).toQColor(); + if (selected) { + p->setPen(Qt::white); + p->setBrush(c.lighter()); + } else { + p->setPen(Qt::black); + p->setBrush(c); + } + + int top = pt.y() - marker_height; + + if (time_.out() != time_.in()) { + QRect marker_rect(pt.x(), top, time_.length().toDouble() * scale, marker_height); + + p->drawRect(marker_rect); + + if (!name_.isEmpty()) { + p->setPen(ColorCoding::GetUISelectorColor(ColorCoding::GetColor(color_))); + p->drawText(marker_rect.adjusted(marker_width/4, 0, 0, 0), name_, Qt::AlignLeft | Qt::AlignVCenter); + } + + return marker_rect; + } else { + int half_marker_height = marker_height / 3; + int left = pt.x() - half_width; + int right = pt.x() + half_width; + int center_y = pt.y() - half_marker_height; + + QPoint points[] = { + pt, + QPoint(left, center_y), + QPoint(left, top), + QPoint(right, top), + QPoint(right, center_y), + pt, + }; + + p->setRenderHint(QPainter::Antialiasing); + p->drawPolygon(points, 6); + + return QRect(left, top, marker_width, marker_height); + } } void TimelineMarkerList::childEvent(QChildEvent *e) @@ -64,20 +129,93 @@ void TimelineMarkerList::childEvent(QChildEvent *e) if (TimelineMarker *marker = dynamic_cast(e->child())) { if (e->type() == QChildEvent::ChildAdded) { - markers_.push_back(marker); + + connect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerTimeChange); + connect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerModification); + connect(marker, &TimelineMarker::NameChanged, this, &TimelineMarkerList::HandleMarkerModification); + connect(marker, &TimelineMarker::ColorChanged, this, &TimelineMarkerList::HandleMarkerModification); + + InsertIntoList(marker); + + emit MarkerAdded(marker); + } else if (e->type() == QChildEvent::ChildRemoved) { - auto it = std::find(markers_.begin(), markers_.end(), marker); - if (it != markers_.end()) { - markers_.erase(it); - } + + RemoveFromList(marker); + + disconnect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerTimeChange); + disconnect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerModification); + disconnect(marker, &TimelineMarker::NameChanged, this, &TimelineMarkerList::HandleMarkerModification); + disconnect(marker, &TimelineMarker::ColorChanged, this, &TimelineMarkerList::HandleMarkerModification); + + emit MarkerRemoved(marker); + } } } -MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, const TimeRange &range, const QString &name, int color) : - marker_list_(marker_list) +void TimelineMarkerList::InsertIntoList(TimelineMarker *marker) { - added_marker_ = new TimelineMarker(color, range, name, &memory_manager_); + // Insertion sort by time to allow some loop optimizations + bool found = false; + for (auto it=markers_.begin(); it!=markers_.end(); it++) { + TimelineMarker *m = *it; + + Q_ASSERT(m->time() != marker->time()); + + if (m->time() > marker->time()) { + markers_.insert(it, marker); + found = true; + break; + } + } + + if (!found) { + markers_.push_back(marker); + } +} + +bool TimelineMarkerList::RemoveFromList(TimelineMarker *marker) +{ + auto it = std::find(markers_.begin(), markers_.end(), marker); + + if (it != markers_.end()) { + markers_.erase(it); + return true; + } + + return false; +} + +void TimelineMarkerList::HandleMarkerModification() +{ + emit MarkerModified(static_cast(sender())); +} + +void TimelineMarkerList::HandleMarkerTimeChange() +{ + TimelineMarker *m = static_cast(sender()); + + auto it = std::find(markers_.begin(), markers_.end(), m); + + if ((it+1 != markers_.end() && (*(it+1))->time() < m->time()) + || (it != markers_.begin() && (*(it-1))->time() > m->time())) { + // Re-sort into list + markers_.erase(it); + InsertIntoList(m); + } +} + +MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, const TimeRange &range, const QString &name, int color) : + MarkerAddCommand(marker_list, new TimelineMarker(color, range, name, &memory_manager_)) +{ +} + +MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, TimelineMarker *marker) : + marker_list_(marker_list), + added_marker_(marker) +{ + added_marker_->setParent(&memory_manager_); } Project* MarkerAddCommand::GetRelevantProject() const @@ -102,7 +240,7 @@ MarkerRemoveCommand::MarkerRemoveCommand(TimelineMarker *marker) : Project* MarkerRemoveCommand::GetRelevantProject() const { - return Project::GetProjectFromObject(marker_list_); + return Project::GetProjectFromObject(marker_); } void MarkerRemoveCommand::redo() @@ -173,7 +311,7 @@ Project* MarkerChangeTimeCommand::GetRelevantProject() const void MarkerChangeTimeCommand::redo() { - old_time_ = marker_->time(); + old_time_ = marker_->time_range(); marker_->set_time(new_time_); } diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h index 61822859c..d55af84bf 100644 --- a/app/timeline/timelinemarker.h +++ b/app/timeline/timelinemarker.h @@ -21,6 +21,7 @@ #ifndef TIMELINEMARKER_H #define TIMELINEMARKER_H +#include #include #include #include @@ -36,8 +37,12 @@ class TimelineMarker : public QObject public: TimelineMarker(int color, const TimeRange& time, const QString& name = QString(), QObject* parent = nullptr); - const TimeRange &time() const { return time_; } + const rational &time() const { return time_.in(); } + const TimeRange &time_range() const { return time_; } void set_time(const TimeRange& time); + void set_time(const rational& time); + + bool has_sibling_at_time(const rational &t) const; const QString& name() const { return name_; } void set_name(const QString& name); @@ -45,6 +50,9 @@ public: int color() const { return color_; } void set_color(int c); + static int GetMarkerHeight(const QFontMetrics &fm); + QRect Draw(QPainter *p, const QPoint &pt, double scale, bool selected); + signals: void TimeChanged(const TimeRange& time); @@ -70,24 +78,79 @@ public: { } - const std::vector &list() const; + inline bool empty() const { return markers_.empty(); } + inline std::vector::iterator begin() { return markers_.begin(); } + inline std::vector::iterator end() { return markers_.end(); } + inline std::vector::const_iterator cbegin() const { return markers_.cbegin(); } + inline std::vector::const_iterator cend() const { return markers_.cend(); } + inline TimelineMarker *back() const { return markers_.back(); } + inline TimelineMarker *front() const { return markers_.front(); } + inline size_t size() const { return markers_.size(); } + + TimelineMarker *GetMarkerAtTime(const rational &t) const + { + for (auto it=markers_.cbegin(); it!=markers_.cend(); it++) { + TimelineMarker *m = *it; + if (m->time() == t) { + return m; + } + } + + return nullptr; + } + + TimelineMarker *GetClosestMarkerToTime(const rational &t) const + { + TimelineMarker *closest = nullptr; + + for (auto it=markers_.cbegin(); it!=markers_.cend(); it++) { + TimelineMarker *m = *it; + + rational this_diff = qAbs(m->time() - t); + + if (closest) { + rational stored_diff = qAbs(closest->time() - t); + + if (this_diff > stored_diff) { + // Since the list is organized by time, if the diff increases, assume we are only going + // to move further away from here and there's no need to check + break; + } + } + + closest = m; + } + + return closest; + } signals: void MarkerAdded(TimelineMarker* marker); void MarkerRemoved(TimelineMarker* marker); + void MarkerModified(TimelineMarker* marker); + protected: virtual void childEvent(QChildEvent *e) override; private: + void InsertIntoList(TimelineMarker *m); + bool RemoveFromList(TimelineMarker *m); + std::vector markers_; +private slots: + void HandleMarkerModification(); + + void HandleMarkerTimeChange(); + }; class MarkerAddCommand : public UndoCommand { public: MarkerAddCommand(TimelineMarkerList* marker_list, const TimeRange& range, const QString& name, int color); + MarkerAddCommand(TimelineMarkerList* marker_list, TimelineMarker *marker); virtual Project* GetRelevantProject() const override; diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp index 59d50d0f1..0b06ac1bc 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp @@ -50,6 +50,7 @@ void ResizableTimelineScrollBar::ConnectTimelinePoints(TimelinePoints *points) disconnect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); disconnect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); disconnect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + disconnect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); } points_ = points; @@ -59,6 +60,7 @@ void ResizableTimelineScrollBar::ConnectTimelinePoints(TimelinePoints *points) connect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); connect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); connect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + connect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); } update(); @@ -77,7 +79,7 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) if (points_ && !timebase().isNull() - && (points_->workarea()->enabled() || !points_->markers()->list().empty())) { + && (points_->workarea()->enabled() || !points_->markers()->empty())) { QStyleOptionSlider opt; initStyleOption(&opt); @@ -110,11 +112,13 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) workarea_color); } - if (!points_->markers()->list().empty()) { - foreach (TimelineMarker* marker, points_->markers()->list()) { + if (!points_->markers()->empty()) { + for (auto it=points_->markers()->cbegin(); it!=points_->markers()->cend(); it++) { + TimelineMarker* marker = *it; + QColor marker_color = ColorCoding::GetColor(marker->color()).toQColor(); - int64_t in = qRound64(ratio * TimeToScene(marker->time().in())); - int64_t out = qRound64(ratio * TimeToScene(marker->time().out())); + int64_t in = qRound64(ratio * TimeToScene(marker->time_range().in())); + int64_t out = qRound64(ratio * TimeToScene(marker->time_range().out())); int64_t length = qMax(int64_t(1), out-in); p.fillRect(gr.x() + in, diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index ae5e32230..cc214e8dd 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -153,36 +153,54 @@ public: for (int i=0; itime(), view_->TimeToScene(obj->time())}; + dragging_[i] = obj->time(); } drag_mouse_start_ = view_->mapToScene(event->pos()); } - void DragMove(QMouseEvent *event, const QString &tip_format) + void DragMove(QMouseEvent *event, const QString &tip_format = QString()) { - QPointF diff = view_->mapToScene(event->pos()) - drag_mouse_start_; + rational time_diff = view_->SceneToTimeNoGrid(view_->mapToScene(event->pos()).x() - drag_mouse_start_.x()); + // Validate movement for (int i=0; iSceneToTimeNoGrid(dragging_.at(i).x + diff.x()); + rational proposed_time = dragging_.at(i) + time_diff; T *sel = selected_.at(i); // Magic number: use interval of 1ms to avoid collisions rational adj(1, 1000); - if (dragging_.at(i).time < proposed_time) { + if (dragging_.at(i) < proposed_time) { + // Negate adjustment value if origin is less than proposed time adj = -adj; } - while (true) { - NodeKeyframe *key_at_time = sel->parent()->GetKeyframeAtTimeOnTrack(sel->input(), proposed_time, sel->track(), sel->element()); - if (!key_at_time || key_at_time == sel) { - break; + bool loop; + do { + loop = false; + while (sel->has_sibling_at_time(proposed_time)) { + proposed_time += adj; } - proposed_time += adj; - } + if (proposed_time < 0) { + // Prevent any object from going below zero + proposed_time = 0; - sel->set_time(proposed_time); + // Setting our proposed time to zero may (re)introduce a conflict that we just avoided + // with the sibling check above, so we request it to happen again. To avoid a negative + // adj bringing us back below zero, we force adj to positive so it'll only nudge higher + adj = qAbs(adj); + + loop = true; + } + } while (loop); + + time_diff = proposed_time - dragging_.at(i); + } + + // Apply movement + for (int i=0; iset_time(dragging_.at(i) + time_diff); } // Show information about this keyframe @@ -202,7 +220,7 @@ public: QToolTip::hideText(); for (int i=0; iadd_child(new SetTimeCommand(selected_.at(i), selected_.at(i)->time(), dragging_.at(i).time)); + command->add_child(new SetTimeCommand(selected_.at(i), selected_.at(i)->time(), dragging_.at(i))); } dragging_.clear(); @@ -271,7 +289,7 @@ private: virtual Project* GetRelevantProject() const override { - return key_->parent()->project(); + return Project::GetProjectFromObject(key_); } protected: @@ -300,13 +318,7 @@ private: QVector selected_; - struct DragObject - { - rational time; - double x; - }; - - QVector dragging_; + QVector dragging_; T *initial_drag_item_; diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index b28398b45..81197e616 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -26,6 +26,7 @@ #include "common/timecodefunctions.h" #include "config/config.h" #include "core.h" +#include "dialog/markerproperties/markerpropertiesdialog.h" #include "node/project/sequence/sequence.h" #include "widget/timelinewidget/undo/timelineundoworkarea.h" @@ -588,29 +589,36 @@ void TimeBasedWidget::SetMarker() return; } - bool ok; - QString marker_name; + TimelineMarkerList *markers = GetConnectedNode()->GetTimelinePoints()->markers(); - if (Config::Current()[QStringLiteral("SetNameWithMarker")].toBool()) { - marker_name = QInputDialog::getText(this, tr("Add Marker"), tr("Name:"), QLineEdit::Normal, QString(), &ok); + if (TimelineMarker *existing = markers->GetMarkerAtTime(GetTime())) { + // We already have a marker here, so pop open the edit dialog + MarkerPropertiesDialog mpd({existing}, timebase(), this); + mpd.exec(); } else { - ok = true; - } - - if (ok) { + // Create a new marker and place it here int color; - - TimelineMarkerList *markers = GetConnectedNode()->GetTimelinePoints()->markers(); - - if (!markers->list().empty()) { - // Use last invoked color if applicable - color = markers->list().back()->color(); + if (TimelineMarker *closest = markers->GetClosestMarkerToTime(GetTime())) { + // Copy color of closest marker to this time + color = closest->color(); } else { // Fallback to default color in preferences color = Config::Current()[QStringLiteral("MarkerColor")].toInt(); } - Core::instance()->undo_stack()->push(new MarkerAddCommand(markers, TimeRange(GetTime(), GetTime()), marker_name, color)); + TimelineMarker *marker = new TimelineMarker(color, TimeRange(GetTime(), GetTime())); + + if (Config::Current()[QStringLiteral("SetNameWithMarker")].toBool()) { + MarkerPropertiesDialog mpd({marker}, timebase(), this); + if (mpd.exec() != QDialog::Accepted) { + delete marker; + marker = nullptr; + } + } + + if (marker) { + Core::instance()->undo_stack()->push(new MarkerAddCommand(markers, marker)); + } } } diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 0cb52f29a..273aa7446 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1798,13 +1798,15 @@ bool TimelineWidget::SnapPoint(QVector start_times, rational* movement } if ((snap_points & kSnapToMarkers)) { - foreach (TimelineMarker* m, GetConnectedNode()->GetTimelinePoints()->markers()->list()) { - qreal marker_pos = TimeToScene(m->time().in()); - potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().in())); + for (auto it=GetConnectedNode()->GetTimelinePoints()->markers()->cbegin(); it!=GetConnectedNode()->GetTimelinePoints()->markers()->cend(); it++) { + TimelineMarker* m = *it; - if (m->time().in() != m->time().out()) { - marker_pos = TimeToScene(m->time().out()); - potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().out())); + qreal marker_pos = TimeToScene(m->time_range().in()); + potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time_range().in())); + + if (m->time_range().in() != m->time_range().out()) { + marker_pos = TimeToScene(m->time_range().out()); + potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time_range().out())); } } } diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index d8b8a28c2..c00070b21 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -67,7 +67,7 @@ void TimelineView::mousePressEvent(QMouseEvent *event) if (rect.contains(mapToScene(event->pos()))) { TimelinePanel *timeline = PanelManager::instance()->MostRecentlyFocused(); if (timeline) { - timeline->timeline_widget()->SetTime(clip_marker_positions_.key(rect)->time().in()); + timeline->timeline_widget()->SetTime(clip_marker_positions_.key(rect)->time_range().in()); } } } @@ -510,58 +510,43 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base())); } - // Draw zebra stripes - if (clip->connected_viewer() && !clip->connected_viewer()->GetLength().isNull()) { - if (clip->media_in() < 0) { - // Draw stripes for sections of clip < 0 - qreal zebra_right = TimeToScene(-clip->media_in()); - if (zebra_right > GetTimelineLeftBound()) { - DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height)); + // Draw zebra stripes and markers + if (clip->connected_viewer()) { + if (!clip->connected_viewer()->GetLength().isNull()) { + if (clip->media_in() < 0) { + // Draw stripes for sections of clip < 0 + qreal zebra_right = TimeToScene(-clip->media_in()); + if (zebra_right > GetTimelineLeftBound()) { + DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height)); + } } - } - if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { - // Draw stripes for sections for clip > clip length - qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); - if (zebra_left < GetTimelineRightBound()) { - DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); - } - } - } - - // Draw markers - qDebug() << "MARKERS ON CLIP STUB"; - /*if (clip->connected_viewer() && !clip->connected_viewer()->GetLength().isNull()) { - if (clip->connected_viewer()->GetTimelinePoints()->markers()->list().size() > 0) { - std::vector marker_list = clip->connected_viewer()->GetTimelinePoints()->markers()->list(); - - int marker_width = QtUtils::QFontMetricsWidth(fm, "H"); - clip_marker_positions_.clear(); - - // Only draw markers if the block UI is large enough to draw all the markers - if (marker_list.size() * marker_width < block_right - block_left) { - - for (auto it=marker_list.cbegin(); it!=marker_list.cend(); it++) { - TimelineMarker *marker = *it; - // Make sure marker is within In/Out points of the clip - if (marker->time().in() >= clip->media_in() && marker->time().out() <= clip->media_in() + clip->length()) { - // Only draw names that we have room for - bool draw_name = true; - if (!marker->name().isEmpty()) { - int length = fm.horizontalAdvance(marker->name()); - if (iterator.hasNext()) { - if (TimeToScene(iterator.peekNext()->time().in()) - TimeToScene(marker->time().out()) < (double)length) { - draw_name = false; - } - } - } - DrawClipMarker(painter, TimeToScene(clip->in() - clip->media_in() + marker->time().in()), - block_top + block_height, marker, draw_name); - } + if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { + // Draw stripes for sections for clip > clip length + qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); + if (zebra_left < GetTimelineRightBound()) { + DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); } } } - }*/ + + TimelineMarkerList *marker_list = clip->connected_viewer()->GetTimelinePoints()->markers(); + if (!marker_list->empty()) { + + clip_marker_positions_.clear(); + + for (auto it=marker_list->cbegin(); it!=marker_list->cend(); it++) { + TimelineMarker *marker = *it; + // Make sure marker is within In/Out points of the clip + if (marker->time_range().in() >= clip->media_in() && marker->time_range().out() <= clip->media_in() + clip->length()) { + QPoint marker_pt(TimeToScene(clip->in() - clip->media_in() + marker->time_range().in()), block_top + block_height); + painter->setClipRect(r); + marker->Draw(painter, marker_pt, GetScale(), false); + painter->setClipping(false); + } + } + } + } } // For transitions, show lines representing a transition @@ -631,46 +616,6 @@ void TimelineView::DrawZebraStripes(QPainter *painter, const QRectF &r) painter->setClipping(false); } -void TimelineView::DrawClipMarker(QPainter* painter, double marker_x, qreal marker_y, TimelineMarker* marker, bool draw_name) -{ - QFontMetrics fm = fontMetrics(); - - int marker_height = fm.height(); - int marker_width = QtUtils::QFontMetricsWidth(fm, "H"); - - int y = marker_y - 1; - - int half_width = marker_width / 2; - - int x = marker_x + half_width; - - painter->setPen(Qt::black); - - painter->setBrush(ColorCoding::GetColor(marker->color()).toQColor()); - - painter->setRenderHint(QPainter::Antialiasing); - - int half_marker_height = marker_height / 3; - - QPoint points[] = { - QPoint(x, y), - QPoint(x - half_width, y - half_marker_height), - QPoint(x - half_width, y - marker_height), - QPoint(x + 1 + half_width, y - marker_height), - QPoint(x + 1 + half_width, y - half_marker_height), - QPoint(x + 1, y), - }; - - painter->drawPolygon(points, 6); - - if (!marker->name().isEmpty() && draw_name) { - painter->drawText(x + marker_width, y - half_marker_height, marker->name()); - } - - QPointF scenePos = QPoint(x - half_width, y); - clip_marker_positions_.insert(marker, (QRectF(scenePos, QSize(marker_width, -marker_height)))); -} - int TimelineView::GetHeightOfAllTracks() const { if (connected_track_list_) { diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index c22c5caba..ff8368add 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -132,8 +132,6 @@ private: void DrawZebraStripes(QPainter *painter, const QRectF &r); - void DrawClipMarker(QPainter* painter, double marker_x, qreal marker_y, TimelineMarker* marker, bool draw_name); - int GetHeightOfAllTracks() const; void UpdatePlayheadRect(); diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 467a9f649..fbbe714b9 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -20,12 +20,16 @@ #include "seekablewidget.h" +#include #include #include #include #include "common/qtutils.h" #include "core.h" +#include "dialog/markerproperties/markerpropertiesdialog.h" +#include "widget/colorlabelmenu/colorlabelmenu.h" +#include "widget/menu/menushared.h" #include "widget/timebased/timebasedwidget.h" namespace olive { @@ -34,8 +38,10 @@ namespace olive { SeekableWidget::SeekableWidget(QWidget* parent) : super(parent), + NodeCopyPasteService(QStringLiteral("markers")), timeline_points_(nullptr), dragging_(false), + ignore_next_focus_out_(false), selection_manager_(this) { QFontMetrics fm = fontMetrics(); @@ -54,19 +60,21 @@ void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) if (timeline_points_) { selection_manager_.ClearSelection(); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&SeekableWidget::update)); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::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)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&SeekableWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&SeekableWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); } viewport()->update(); @@ -102,7 +110,9 @@ void SeekableWidget::PasteMarkers(bool insert, rational insert_time) void SeekableWidget::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) { + if (TimelineMarker *initial = selection_manager_.MousePress(event)) { + selection_manager_.DragStart(initial, event); + } else if (!selection_manager_.GetObjectAtPoint(event->pos()) && event->button() == Qt::LeftButton) { SeekToScenePoint(mapToScene(event->pos()).x()); dragging_ = true; @@ -112,14 +122,20 @@ void SeekableWidget::mousePressEvent(QMouseEvent *event) void SeekableWidget::mouseMoveEvent(QMouseEvent *event) { - if (event->buttons() & Qt::LeftButton) { + if (selection_manager_.IsDragging()) { + selection_manager_.DragMove(event); + } else if (dragging_) { SeekToScenePoint(mapToScene(event->pos()).x()); } } void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) { - Q_UNUSED(event) + if (selection_manager_.IsDragging()) { + MultiUndoCommand *command = new MultiUndoCommand(); + selection_manager_.DragStop(command); + Core::instance()->undo_stack()->pushIfHasChildren(command); + } if (GetSnapService()) { GetSnapService()->HideSnaps(); @@ -128,12 +144,25 @@ void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) dragging_ = false; } +void SeekableWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + super::mouseDoubleClickEvent(event); + + if (selection_manager_.GetObjectAtPoint(event->pos()) && !selection_manager_.GetSelectedObjects().isEmpty()) { + ShowMarkerProperties(); + } +} + void SeekableWidget::focusOutEvent(QFocusEvent *event) { super::focusOutEvent(event); - // Deselect everything when we lose focus - DeselectAllMarkers(); + if (ignore_next_focus_out_) { + ignore_next_focus_out_ = false; + } else { + // Deselect everything when we lose focus + DeselectAllMarkers(); + } } TimelinePoints *SeekableWidget::timeline_points() const @@ -159,6 +188,20 @@ void SeekableWidget::SetMarkerColor(int c) Core::instance()->undo_stack()->pushIfHasChildren(command); } +void SeekableWidget::ShowMarkerProperties() +{ + MarkerPropertiesDialog mpd(selection_manager_.GetSelectedObjects(), timebase(), this); + ignore_next_focus_out_ = true; + mpd.exec(); +} + +void SeekableWidget::TimebaseChangedEvent(const rational &t) +{ + super::TimebaseChangedEvent(t); + + selection_manager_.SetTimebase(t); +} + void SeekableWidget::SeekToScenePoint(qreal scene) { if (timebase().isNull()) { @@ -171,8 +214,8 @@ void SeekableWidget::SeekToScenePoint(qreal scene) rational movement; GetSnapService()->SnapPoint({playhead_time}, - &movement, - SnapService::kSnapAll & ~SnapService::kSnapToPlayhead); + &movement, + SnapService::kSnapAll & ~SnapService::kSnapToPlayhead); playhead_time += movement; } @@ -184,56 +227,62 @@ void SeekableWidget::SeekToScenePoint(qreal scene) } } +void SeekableWidget::SelectionManagerSelectEvent(void *obj) +{ + super::SelectionManagerSelectEvent(obj); + + viewport()->update(); +} + +void SeekableWidget::SelectionManagerDeselectEvent(void *obj) +{ + super::SelectionManagerDeselectEvent(obj); + + viewport()->update(); +} + void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) { if (!timeline_points()) { return; } + int lim_left = GetScroll(); + int lim_right = lim_left + width(); + + selection_manager_.ClearDrawnObjects(); + // Draw in/out workarea if (timeline_points()->workarea()->enabled()) { - int workarea_left = qMax(0.0, TimeToScene(timeline_points()->workarea()->in())); + int workarea_left = qMax(qreal(lim_left), TimeToScene(timeline_points()->workarea()->in())); int workarea_right; if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { - workarea_right = width(); + workarea_right = lim_right; } else { - workarea_right = qMin(qreal(width()), TimeToScene(timeline_points()->workarea()->out())); + workarea_right = qMin(qreal(lim_right), TimeToScene(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().empty()) { + if (marker_bottom > 0 && !timeline_points()->markers()->empty()) { + for (auto it=timeline_points()->markers()->cbegin(); it!=timeline_points()->markers()->cend(); it++) { + TimelineMarker* marker = *it; - 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 = TimeToScene(marker->time().in()); - int marker_right = TimeToScene(marker->time().out()); - - if (marker_left >= width() || marker_right < 0) { + int marker_right = TimeToScene(marker->time_range().out()); + if (marker_right < lim_left) { continue; } - if (marker->time().length() != 0) { - // 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()); - } + int marker_left = TimeToScene(marker->time_range().in()); + if (marker_left >= lim_right) { + break; } + + QRect marker_rect = marker->Draw(p, QPoint(marker_left, marker_bottom), GetScale(), selection_manager_.IsSelected(marker)); + selection_manager_.DeclareDrawnObject(marker, marker_rect); } } } @@ -264,4 +313,31 @@ void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) p->setRenderHint(QPainter::Antialiasing, false); } +bool SeekableWidget::ShowContextMenu(const QPoint &p) +{ + if (selection_manager_.GetObjectAtPoint(p) && !selection_manager_.GetSelectedObjects().isEmpty()) { + // Show marker-specific menu + Menu m; + + ColorLabelMenu color_coding_menu; + connect(&color_coding_menu, &ColorLabelMenu::ColorSelected, this, &SeekableWidget::SetMarkerColor); + m.addMenu(&color_coding_menu); + + m.addSeparator(); + + MenuShared::instance()->AddItemsForEditMenu(&m, false); + + m.addSeparator(); + + QAction *properties_action = m.addAction(tr("Properties")); + connect(properties_action, &QAction::triggered, this, &SeekableWidget::ShowMarkerProperties); + + ignore_next_focus_out_ = true; + m.exec(mapToGlobal(p)); + return true; + } else { + return false; + } +} + } diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index 56d6f2660..d06401989 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -25,14 +25,15 @@ #include #include "common/rational.h" +#include "node/nodecopypaste.h" #include "timeline/timelinepoints.h" +#include "widget/menu/menu.h" #include "widget/snapservice/snapservice.h" #include "widget/timebased/timebasedviewselectionmanager.h" -//#include "widget/marker/markercopypaste.h" namespace olive { -class SeekableWidget : public TimeBasedView//, public MarkerCopyPasteService +class SeekableWidget : public TimeBasedView, public NodeCopyPasteService { Q_OBJECT public: @@ -60,22 +61,22 @@ public: void SeekToScenePoint(qreal scene); + virtual void SelectionManagerSelectEvent(void *obj) override; + virtual void SelectionManagerDeselectEvent(void *obj) override; + public slots: void SetScroll(int i) { horizontalScrollBar()->setValue(i); } - void SetMarkerColor(int c); - - /*void SetMarkerTime(); - - void SetMarkerName();*/ + virtual void TimebaseChangedEvent(const rational &) override; protected: virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; virtual void focusOutEvent(QFocusEvent *event) override; @@ -93,6 +94,9 @@ protected: return playhead_width_; } +protected slots: + virtual bool ShowContextMenu(const QPoint &p); + private: TimelinePoints* timeline_points_; @@ -102,8 +106,15 @@ private: bool dragging_; + bool ignore_next_focus_out_; + TimeBasedViewSelectionManager selection_manager_; +private slots: + void SetMarkerColor(int c); + + void ShowMarkerProperties(); + }; } diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index ff2b3281d..f50e67d36 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -32,8 +32,10 @@ namespace olive { +#define super SeekableWidget + TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* parent) : - SeekableWidget(parent), + super(parent), text_visible_(text_visible), centered_text_(true), show_cache_status_(cache_status_visible), @@ -102,18 +104,9 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) } // Draw timeline points if connected + int marker_height = TimelineMarker::GetMarkerHeight(p->fontMetrics()); if (timeline_points()) { - int marker_bottom = height() - text_height(); - - if (show_cache_status_) { - marker_bottom -= cache_status_height_; - } - - if (text_visible_) { - marker_bottom -= cache_status_height_; - } - - DrawTimelinePoints(p, marker_bottom); + DrawTimelinePoints(p, marker_height); } double width_of_frame = timebase_dbl() * GetScale(); @@ -222,11 +215,11 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) int timecode_left; if (centered_text_) { - text_rect = QRect(i - kAverageTextWidth/2, 0, kAverageTextWidth, fm.height()); + text_rect = QRect(i - kAverageTextWidth/2, marker_height, kAverageTextWidth, fm.height()); text_align = Qt::AlignCenter; timecode_left = i - timecode_width/2; } else { - text_rect = QRect(i, 0, kAverageTextWidth, fm.height()); + text_rect = QRect(i, marker_height, kAverageTextWidth, fm.height()); text_align = Qt::AlignLeft | Qt::AlignVCenter; timecode_left = i; @@ -265,13 +258,15 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) if (show_cache_status_ && playback_cache_) { // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change rational len = playback_cache_->viewer_parent()->GetVideoLength(); + int lim_left = GetScroll(); + int lim_right = lim_left + width(); - int cache_screen_length = qMin(TimeToScene(len), qreal(width())); + int cache_screen_length = TimeToScene(len); if (cache_screen_length > 0) { int cache_y = height() - cache_status_height_; - p->fillRect(0, cache_y, cache_screen_length , cache_status_height_, Qt::green); + p->fillRect(0, cache_y, cache_screen_length, cache_status_height_, Qt::green); foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges(len)) { int range_left = TimeToScene(range.in()); @@ -284,11 +279,11 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) continue; } - int adjusted_left = qMax(0, range_left); + int adjusted_left = qMax(lim_left, range_left); p->fillRect(adjusted_left, cache_y, - qMin(width(), range_right) - adjusted_left, + qMin(lim_right, range_right) - adjusted_left, cache_status_height_, Qt::red); } @@ -304,6 +299,8 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) void TimeRuler::TimebaseChangedEvent(const rational &tb) { + super::TimebaseChangedEvent(tb); + timebase_flipped_dbl_ = tb.flipped().toDouble(); update(); @@ -314,14 +311,20 @@ int TimeRuler::CacheStatusHeight() const return fontMetrics().height() / 4; } -void TimeRuler::ShowContextMenu() +bool TimeRuler::ShowContextMenu(const QPoint &p) { - Menu m(this); + if (super::ShowContextMenu(p)) { + return true; + } else { + Menu m(this); - MenuShared::instance()->AddItemsForTimeRulerMenu(&m); - MenuShared::instance()->AboutToShowTimeRulerActions(timebase()); + MenuShared::instance()->AddItemsForTimeRulerMenu(&m); + MenuShared::instance()->AboutToShowTimeRulerActions(timebase()); - m.exec(QCursor::pos()); + m.exec(mapToGlobal(p)); + + return true; + } } void TimeRuler::UpdateHeight() @@ -339,7 +342,7 @@ void TimeRuler::UpdateHeight() } // Add marker height - height += text_height(); + height += TimelineMarker::GetMarkerHeight(fontMetrics()); setFixedHeight(height); } diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index 3a475d398..b6bfc8858 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -45,6 +45,9 @@ protected: virtual void TimebaseChangedEvent(const rational& tb) override; +protected slots: + virtual bool ShowContextMenu(const QPoint &p) override; + private: void UpdateHeight(); @@ -64,9 +67,6 @@ private: PlaybackCache* playback_cache_; -private slots: - void ShowContextMenu(); - }; }