diff --git a/cmake/SuperBuild/Builddtk-deps.cmake b/cmake/SuperBuild/Builddtk-deps.cmake index e35f0b5..ab6f4f2 100644 --- a/cmake/SuperBuild/Builddtk-deps.cmake +++ b/cmake/SuperBuild/Builddtk-deps.cmake @@ -1,7 +1,7 @@ include(ExternalProject) set(dtk_GIT_REPOSITORY "https://github.com/darbyjohnston/dtk.git") -set(dtk_GIT_TAG "11e0c38016c8c7b860a7bc03dbc0adec7a1f0d91") +set(dtk_GIT_TAG "957d7bb8e6f7e12fc291e8c31a4180ee4a9dfd8f") set(dtk-deps_ARGS ${toucan_EXTERNAL_PROJECT_ARGS} diff --git a/cmake/SuperBuild/Builddtk.cmake b/cmake/SuperBuild/Builddtk.cmake index c041596..ae7b410 100644 --- a/cmake/SuperBuild/Builddtk.cmake +++ b/cmake/SuperBuild/Builddtk.cmake @@ -1,7 +1,7 @@ include(ExternalProject) set(dtk_GIT_REPOSITORY "https://github.com/darbyjohnston/dtk.git") -set(dtk_GIT_TAG "11e0c38016c8c7b860a7bc03dbc0adec7a1f0d91") +set(dtk_GIT_TAG "957d7bb8e6f7e12fc291e8c31a4180ee4a9dfd8f") set(dtk_DEPS dtk-deps) set(dtk_ARGS diff --git a/lib/toucan/CMakeLists.txt b/lib/toucan/CMakeLists.txt index 598b70c..1a20074 100644 --- a/lib/toucan/CMakeLists.txt +++ b/lib/toucan/CMakeLists.txt @@ -13,6 +13,7 @@ set(HEADERS PropertySet.h Read.h TimeWarp.h + TimelineAlgo.h TimelineWrapper.h Util.h) set(HEADERS_PRIVATE @@ -30,6 +31,7 @@ set(SOURCE PropertySet.cpp Read.cpp TimeWarp.cpp + TimelineAlgo.cpp TimelineWrapper.cpp Util.cpp) if(WIN32) diff --git a/lib/toucan/ImageGraph.cpp b/lib/toucan/ImageGraph.cpp index 1853b4f..23cea0d 100644 --- a/lib/toucan/ImageGraph.cpp +++ b/lib/toucan/ImageGraph.cpp @@ -7,6 +7,7 @@ #include "ImageEffectHost.h" #include "Read.h" #include "TimeWarp.h" +#include "TimelineAlgo.h" #include "Util.h" #include @@ -21,6 +22,19 @@ namespace toucan namespace { const std::string logPrefix = "toucan::ImageGraph"; + + std::string toImageDataType(const OIIO::TypeDesc& value) + { + std::string out = "Unknown"; + switch (value.basetype) + { + case OIIO::TypeDesc::UINT8: out = "u8"; break; + case OIIO::TypeDesc::UINT16: out = "u16"; break; + case OIIO::TypeDesc::HALF: out = "half"; break; + case OIIO::TypeDesc::FLOAT: out = "float"; break; + } + return out; + } } ImageGraph::ImageGraph( @@ -34,54 +48,55 @@ namespace toucan { _loadCache.setMax(10); - // Get the image size from the first video clip. - for (auto track : _timelineWrapper->getTimeline()->find_children()) + // Get the image information from the first video clip. + for (auto clip : getVideoClips(_timelineWrapper->getTimeline())) { - if (OTIO_NS::Track::Kind::video == track->kind()) + if (auto externalRef = dynamic_cast(clip->media_reference())) { - for (auto clip : track->find_clips()) + auto read = std::make_shared( + _timelineWrapper->getMediaPath(externalRef->target_url()), + _timelineWrapper->getMemoryReference(externalRef->target_url())); + const auto& spec = read->getSpec(); + if (spec.width > 0) { - if (auto externalRef = dynamic_cast(clip->media_reference())) - { - auto read = std::make_shared( - _timelineWrapper->getMediaPath(externalRef->target_url()), - _timelineWrapper->getMemoryReference(externalRef->target_url())); - const auto& spec = read->getSpec(); - if (spec.width > 0) - { - _imageSize.x = spec.width; - _imageSize.y = spec.height; - break; - } - } - else if (auto sequenceRef = dynamic_cast(clip->media_reference())) - { - auto read = std::make_shared( - _timelineWrapper->getMediaPath(sequenceRef->target_url_base()), - sequenceRef->name_prefix(), - sequenceRef->name_suffix(), - sequenceRef->start_frame(), - sequenceRef->frame_step(), - sequenceRef->rate(), - sequenceRef->frame_zero_padding(), - _timelineWrapper->getMemoryReferences()); - const auto& spec = read->getSpec(); - if (spec.width > 0) - { - _imageSize.x = spec.width; - _imageSize.y = spec.height; - break; - } - } - else if (auto generatorRef = dynamic_cast(clip->media_reference())) - { - auto parameters = generatorRef->parameters(); - auto i = parameters.find("size"); - if (i != parameters.end() && i->second.has_value()) - { - anyToVec(std::any_cast(i->second), _imageSize); - } - } + _imageSize.x = spec.width; + _imageSize.y = spec.height; + _imageChannels = spec.nchannels; + _imageDataType = toImageDataType(spec.format); + break; + } + } + else if (auto sequenceRef = dynamic_cast(clip->media_reference())) + { + auto read = std::make_shared( + _timelineWrapper->getMediaPath(sequenceRef->target_url_base()), + sequenceRef->name_prefix(), + sequenceRef->name_suffix(), + sequenceRef->start_frame(), + sequenceRef->frame_step(), + sequenceRef->rate(), + sequenceRef->frame_zero_padding(), + _timelineWrapper->getMemoryReferences()); + const auto& spec = read->getSpec(); + if (spec.width > 0) + { + _imageSize.x = spec.width; + _imageSize.y = spec.height; + _imageChannels = spec.nchannels; + _imageDataType = toImageDataType(spec.format); + break; + } + } + else if (auto generatorRef = dynamic_cast(clip->media_reference())) + { + auto parameters = generatorRef->parameters(); + auto i = parameters.find("size"); + if (i != parameters.end() && i->second.has_value()) + { + anyToVec(std::any_cast(i->second), _imageSize); + //! \bug Hard coded: + _imageChannels = 4; + _imageDataType = toImageDataType(OIIO::TypeDesc::UINT8); } } } @@ -95,6 +110,16 @@ namespace toucan return _imageSize; } + int ImageGraph::getImageChannels() const + { + return _imageChannels; + } + + const std::string& ImageGraph::getImageDataType() const + { + return _imageDataType; + } + std::shared_ptr ImageGraph::exec( const std::shared_ptr& host, const OTIO_NS::RationalTime& time) diff --git a/lib/toucan/ImageGraph.h b/lib/toucan/ImageGraph.h index 277ac92..db6978c 100644 --- a/lib/toucan/ImageGraph.h +++ b/lib/toucan/ImageGraph.h @@ -39,6 +39,12 @@ namespace toucan //! Get the timeline image size. const IMATH_NAMESPACE::V2i& getImageSize() const; + //! Get the timeline image channels. + int getImageChannels() const; + + //! Get the timeline image data type. + const std::string& getImageDataType() const; + //! Get an image graph for the given time. std::shared_ptr exec( const std::shared_ptr&, @@ -66,6 +72,8 @@ namespace toucan OTIO_NS::TimeRange _timeRange; ImageGraphOptions _options; IMATH_NAMESPACE::V2i _imageSize = IMATH_NAMESPACE::V2i(0, 0); + int _imageChannels = 0; + std::string _imageDataType; LRUCache > _loadCache; }; } diff --git a/lib/toucan/TimelineAlgo.cpp b/lib/toucan/TimelineAlgo.cpp new file mode 100644 index 0000000..07f9aa6 --- /dev/null +++ b/lib/toucan/TimelineAlgo.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the toucan project. + +#include "TimelineAlgo.h" + +#include + +namespace toucan +{ + std::vector > + getVideoClips(const OTIO_NS::SerializableObject::Retainer& timeline) + { + std::vector > out; + for (const auto& child : timeline->tracks()->children()) + { + if (auto track = OTIO_NS::dynamic_retainer_cast(child)) + { + if (OTIO_NS::Track::Kind::video == track->kind()) + { + const auto clips = track->find_clips(nullptr, std::nullopt, true); + out.insert(out.end(), clips.begin(), clips.end()); + } + } + } + return out; + } + + std::optional getTimeRange( + const std::vector >& items, + const OTIO_NS::RationalTime& startTime, + double rate) + { + std::optional out; + for (const auto& item : items) + { + //! \bug Shouldn't trimmed_range_in_parent() check whether the parent + //! is null? + if (item->parent()) + { + const auto timeRangeOpt = item->trimmed_range_in_parent(); + if (timeRangeOpt.has_value()) + { + const OTIO_NS::TimeRange timeRange( + timeRangeOpt.value().start_time() + startTime, + timeRangeOpt.value().duration()); + if (out.has_value()) + { + out = OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + std::min( + out.value().start_time(), + timeRange.start_time().rescaled_to(rate).round()), + std::max( + out.value().end_time_inclusive(), + timeRange.end_time_inclusive().rescaled_to(rate).round())); + } + else + { + out = OTIO_NS::TimeRange( + timeRange.start_time().rescaled_to(rate).round(), + timeRange.duration().rescaled_to(rate).round()); + } + } + } + } + return out; + } +} diff --git a/lib/toucan/TimelineAlgo.h b/lib/toucan/TimelineAlgo.h new file mode 100644 index 0000000..5d4ef93 --- /dev/null +++ b/lib/toucan/TimelineAlgo.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Contributors to the toucan project. + +#pragma once + +#include + +namespace toucan +{ + //! Get the video clips in a timeline. + std::vector > + getVideoClips(const OTIO_NS::SerializableObject::Retainer&); + + //! Get the range of multiple items. + std::optional getTimeRange( + const std::vector >&, + const OTIO_NS::RationalTime& startTime, + double rate); +} diff --git a/lib/toucan/Util.cpp b/lib/toucan/Util.cpp index 5bb7571..6837979 100644 --- a/lib/toucan/Util.cpp +++ b/lib/toucan/Util.cpp @@ -3,6 +3,8 @@ #include "Util.h" +#include + #include #include diff --git a/lib/toucanView/Document.cpp b/lib/toucanView/Document.cpp index bb6bcd9..c007079 100644 --- a/lib/toucanView/Document.cpp +++ b/lib/toucanView/Document.cpp @@ -103,6 +103,16 @@ namespace toucan return _graph->getImageSize(); } + int Document::getImageChannels() const + { + return _graph->getImageChannels(); + } + + const std::string& Document::getImageDataType() const + { + return _graph->getImageDataType(); + } + std::shared_ptr > > Document::observeCurrentImage() const { return _currentImage; diff --git a/lib/toucanView/Document.h b/lib/toucanView/Document.h index 53cc8ec..a0fe59c 100644 --- a/lib/toucanView/Document.h +++ b/lib/toucanView/Document.h @@ -41,6 +41,8 @@ namespace toucan const std::shared_ptr& getThumbnailGenerator() const; const IMATH_NAMESPACE::V2i& getImageSize() const; + int getImageChannels() const; + const std::string& getImageDataType() const; std::shared_ptr > > observeCurrentImage() const; diff --git a/lib/toucanView/InfoBar.cpp b/lib/toucanView/InfoBar.cpp index 8312a0e..d29f41d 100644 --- a/lib/toucanView/InfoBar.cpp +++ b/lib/toucanView/InfoBar.cpp @@ -35,18 +35,22 @@ namespace toucan const IMATH_NAMESPACE::V2i& imageSize = document->getImageSize(); const size_t trackCount = document->getTimeline()->find_children().size(); - text = dtk::Format("{0}: {1}x{2}, {3} tracks"). + text = dtk::Format("{0}: {1}x{2}, {3} channels, {4} data, {5} tracks"). arg(dtk::elide(document->getPath().filename().string())). arg(imageSize.x). arg(imageSize.y). + arg(document->getImageChannels()). + arg(document->getImageDataType()). arg(trackCount); tooltip = dtk::Format( - "path: {0}\n" - "resolution: {1}x{2}\n" - "tracks: {3}"). + "Path: {0}\n" + "Render: {1}x{2}, {3} channels, {4} data\n" + "Tracks: {5}"). arg(document->getPath().string()). arg(imageSize.x). arg(imageSize.y). + arg(document->getImageChannels()). + arg(document->getImageDataType()). arg(trackCount); } _label->setText(text); diff --git a/lib/toucanView/JSONTool.cpp b/lib/toucanView/JSONTool.cpp index aac6e6c..8600ae8 100644 --- a/lib/toucanView/JSONTool.cpp +++ b/lib/toucanView/JSONTool.cpp @@ -8,9 +8,9 @@ #include "SelectionModel.h" #include -#include #include #include +#include namespace toucan { @@ -23,13 +23,16 @@ namespace toucan _item = item; - std::string text = item->to_json_string(); - auto label = dtk::Label::create(context, text); - label->setMarginRole(dtk::SizeRole::MarginSmall); + _text = dtk::split(item->to_json_string(), { '\n' }); + + _label = dtk::Label::create(context); + _label->setMarginRole(dtk::SizeRole::MarginSmall); _bellows = dtk::Bellows::create(context, item->name(), shared_from_this()); - _bellows->setWidget(label); + _bellows->setWidget(_label); _bellows->setOpen(true); + + _textUpdate(); } JSONWidget::~JSONWidget() @@ -50,6 +53,14 @@ namespace toucan _bellows->setOpen(value); } + void JSONWidget::setFilter(const std::string& value) + { + if (value == _filter) + return; + _filter = value; + _textUpdate(); + } + void JSONWidget::setGeometry(const dtk::Box2I& value) { IWidget::setGeometry(value); @@ -62,6 +73,26 @@ namespace toucan _setSizeHint(_bellows->getSizeHint()); } + void JSONWidget::_textUpdate() + { + if (!_filter.empty()) + { + std::vector text; + for (const auto& line : _text) + { + if (dtk::contains(line, _filter, dtk::CaseCompare::Insensitive)) + { + text.push_back(line); + } + } + _label->setText(dtk::join(text, '\n')); + } + else + { + _label->setText(dtk::join(_text, '\n')); + } + } + void JSONTool::_init( const std::shared_ptr& context, const std::shared_ptr& app, @@ -72,11 +103,26 @@ namespace toucan _layout = dtk::VerticalLayout::create(context, shared_from_this()); _layout->setSpacingRole(dtk::SizeRole::None); - auto hLayout = dtk::HorizontalLayout::create(context, _layout); + _scrollWidget = dtk::ScrollWidget::create(context, dtk::ScrollType::Both, _layout); + _scrollWidget->setBorder(false); + _scrollWidget->setVStretch(dtk::Stretch::Expanding); + + _scrollLayout = dtk::VerticalLayout::create(context); + _scrollLayout->setSpacingRole(dtk::SizeRole::None); + _scrollWidget->setWidget(_scrollLayout); + + dtk::Divider::create(context, dtk::Orientation::Vertical, _layout); + + _bottomLayout = dtk::HorizontalLayout::create(context, _layout); + _bottomLayout->setMarginRole(dtk::SizeRole::MarginInside); + _bottomLayout->setSpacingRole(dtk::SizeRole::SpacingSmall); + + _searchBox = dtk::SearchBox::create(context, _bottomLayout); + _searchBox->setHStretch(dtk::Stretch::Expanding); + _searchBox->setTooltip("Filter the JSON text"); + + auto hLayout = dtk::HorizontalLayout::create(context, _bottomLayout); hLayout->setSpacingRole(dtk::SizeRole::SpacingTool); - auto spacer = dtk::Spacer::create(context, dtk::Orientation::Horizontal, hLayout); - spacer->setSpacingRole(dtk::SizeRole::None); - spacer->setStretch(dtk::Stretch::Expanding); auto openButton = dtk::ToolButton::create(context, hLayout); openButton->setMarginRole(dtk::SizeRole::MarginSmall); openButton->setIcon("BellowsOpen"); @@ -86,16 +132,6 @@ namespace toucan closeButton->setIcon("BellowsClosed"); closeButton->setTooltip("Close all"); - dtk::Divider::create(context, dtk::Orientation::Vertical, _layout); - - _scrollWidget = dtk::ScrollWidget::create(context, dtk::ScrollType::Both, _layout); - _scrollWidget->setBorder(false); - _scrollWidget->setVStretch(dtk::Stretch::Expanding); - - _scrollLayout = dtk::VerticalLayout::create(context); - _scrollLayout->setSpacingRole(dtk::SizeRole::None); - _scrollWidget->setWidget(_scrollLayout); - openButton->setClickedCallback( [this] { @@ -114,6 +150,15 @@ namespace toucan } }); + _searchBox->setCallback( + [this](const std::string& text) + { + for (const auto& widget : _widgets) + { + widget->setFilter(text); + } + }); + _documentObserver = dtk::ValueObserver >::create( app->getDocumentsModel()->observeCurrent(), [this](const std::shared_ptr& document) @@ -133,6 +178,7 @@ namespace toucan for (const auto& item : selection) { auto widget = JSONWidget::create(context, item, _scrollLayout); + widget->setFilter(_searchBox->getText()); _widgets.push_back(widget); } }); diff --git a/lib/toucanView/JSONTool.h b/lib/toucanView/JSONTool.h index c7bb08b..dfc192a 100644 --- a/lib/toucanView/JSONTool.h +++ b/lib/toucanView/JSONTool.h @@ -6,8 +6,10 @@ #include "IToolWidget.h" #include +#include #include #include +#include #include #include @@ -33,12 +35,18 @@ namespace toucan const std::shared_ptr& parent = nullptr); void setOpen(bool); + void setFilter(const std::string&); void setGeometry(const dtk::Box2I&) override; void sizeHintEvent(const dtk::SizeHintEvent&) override; private: + void _textUpdate(); + OTIO_NS::SerializableObject::Retainer _item; + std::vector _text; + std::string _filter; + std::shared_ptr _label; std::shared_ptr _bellows; }; @@ -65,9 +73,11 @@ namespace toucan std::shared_ptr _document; std::shared_ptr _layout; + std::shared_ptr _searchBox; std::shared_ptr _scrollWidget; std::shared_ptr _scrollLayout; std::vector > _widgets; + std::shared_ptr _bottomLayout; std::shared_ptr > > _documentObserver; std::shared_ptr > > _selectionObserver; diff --git a/lib/toucanView/MenuBar.cpp b/lib/toucanView/MenuBar.cpp index 3dc1c21..a83853d 100644 --- a/lib/toucanView/MenuBar.cpp +++ b/lib/toucanView/MenuBar.cpp @@ -9,6 +9,8 @@ #include "SelectionModel.h" #include "ViewModel.h" +#include + #include #include #include @@ -219,6 +221,32 @@ namespace toucan }); _menus["Select"]->addItem(_actions["Select/All"]); + _actions["Select/AllTracks"] = std::make_shared( + "All Tracks", + [this] + { + if (_document) + { + _document->getSelectionModel()->selectAll( + _document->getTimeline(), + SelectionType::Tracks); + } + }); + _menus["Select"]->addItem(_actions["Select/AllTracks"]); + + _actions["Select/AllClips"] = std::make_shared( + "All Clips", + [this] + { + if (_document) + { + _document->getSelectionModel()->selectAll( + _document->getTimeline(), + SelectionType::Clips); + } + }); + _menus["Select"]->addItem(_actions["Select/AllClips"]); + _actions["Select/None"] = std::make_shared( "None", dtk::Key::A, @@ -254,7 +282,7 @@ namespace toucan _menus["Time"] = dtk::Menu::create(context); addMenu("Time", _menus["Time"]); - _actions["Time/Start"] = std::make_shared( + _actions["Time/FrameStart"] = std::make_shared( "Start Frame", "FrameStart", dtk::Key::Up, @@ -263,12 +291,14 @@ namespace toucan { if (_document) { - _document->getPlaybackModel()->frameAction(FrameAction::Start); + _document->getPlaybackModel()->timeAction( + TimeAction::FrameStart, + _document->getTimeline()); } }); - _menus["Time"]->addItem(_actions["Time/Start"]); + _menus["Time"]->addItem(_actions["Time/FrameStart"]); - _actions["Time/Prev"] = std::make_shared( + _actions["Time/FramePrev"] = std::make_shared( "Previous Frame", "FramePrev", dtk::Key::Left, @@ -277,12 +307,14 @@ namespace toucan { if (_document) { - _document->getPlaybackModel()->frameAction(FrameAction::Prev); + _document->getPlaybackModel()->timeAction( + TimeAction::FramePrev, + _document->getTimeline()); } }); - _menus["Time"]->addItem(_actions["Time/Prev"]); + _menus["Time"]->addItem(_actions["Time/FramePrev"]); - _actions["Time/Next"] = std::make_shared( + _actions["Time/FrameNext"] = std::make_shared( "Next Frame", "FrameNext", dtk::Key::Right, @@ -291,12 +323,14 @@ namespace toucan { if (_document) { - _document->getPlaybackModel()->frameAction(FrameAction::Next); + _document->getPlaybackModel()->timeAction( + TimeAction::FrameNext, + _document->getTimeline()); } }); - _menus["Time"]->addItem(_actions["Time/Next"]); + _menus["Time"]->addItem(_actions["Time/FrameNext"]); - _actions["Time/End"] = std::make_shared( + _actions["Time/FrameEnd"] = std::make_shared( "End Frame", "FrameEnd", dtk::Key::Down, @@ -305,56 +339,139 @@ namespace toucan { if (_document) { - _document->getPlaybackModel()->frameAction(FrameAction::End); + _document->getPlaybackModel()->timeAction( + TimeAction::FrameEnd, + _document->getTimeline()); } }); - _menus["Time"]->addItem(_actions["Time/End"]); + _menus["Time"]->addItem(_actions["Time/FrameEnd"]); _menus["Time"]->addDivider(); - _menus["TimeUnits"] = _menus["Time"]->addSubMenu("Time Units"); + _actions["Time/ClipNext"] = std::make_shared( + "Next Clip", + dtk::Key::Right, + static_cast(dtk::KeyModifier::Control), + [this] + { + if (_document) + { + _document->getPlaybackModel()->timeAction( + TimeAction::ClipNext, + _document->getTimeline()); + } + }); + _menus["Time"]->addItem(_actions["Time/ClipNext"]); - _actions["TimeUnits/Timecode"] = std::make_shared( - "Timecode", - [this](bool value) + _actions["Time/ClipPrev"] = std::make_shared( + "Previous Clip", + dtk::Key::Left, + static_cast(dtk::KeyModifier::Control), + [this] { - if (auto app = _app.lock()) + if (_document) { - app->getTimeUnitsModel()->setTimeUnits(TimeUnits::Timecode); + _document->getPlaybackModel()->timeAction( + TimeAction::ClipPrev, + _document->getTimeline()); } }); - _menus["TimeUnits"]->addItem(_actions["TimeUnits/Timecode"]); + _menus["Time"]->addItem(_actions["Time/ClipPrev"]); - _actions["TimeUnits/Frames"] = std::make_shared( - "Frames", - [this](bool value) + _menus["Time"]->addDivider(); + + _actions["Time/InPointSet"] = std::make_shared( + "Set In Point", + dtk::Key::I, + 0, + [this] { - if (auto app = _app.lock()) + if (_document) { - app->getTimeUnitsModel()->setTimeUnits(TimeUnits::Frames); + _document->getPlaybackModel()->setInPoint( + _document->getPlaybackModel()->getCurrentTime()); } }); - _menus["TimeUnits"]->addItem(_actions["TimeUnits/Frames"]); + _menus["Time"]->addItem(_actions["Time/InPointSet"]); - _actions["TimeUnits/Seconds"] = std::make_shared( - "Seconds", - [this](bool value) + _actions["Time/InPointReset"] = std::make_shared( + "Reset In Point", + dtk::Key::I, + static_cast(dtk::KeyModifier::Shift), + [this] + { + if (_document) + { + _document->getPlaybackModel()->resetInPoint(); + } + }); + _menus["Time"]->addItem(_actions["Time/InPointReset"]); + + _actions["Time/OutPointSet"] = std::make_shared( + "Set Out Point", + dtk::Key::O, + 0, + [this] + { + if (_document) + { + _document->getPlaybackModel()->setOutPoint( + _document->getPlaybackModel()->getCurrentTime()); + } + }); + _menus["Time"]->addItem(_actions["Time/OutPointSet"]); + + _actions["Time/OutPointReset"] = std::make_shared( + "Reset Out Point", + dtk::Key::O, + static_cast(dtk::KeyModifier::Shift), + [this] { - if (auto app = _app.lock()) + if (_document) + { + _document->getPlaybackModel()->resetOutPoint(); + } + }); + _menus["Time"]->addItem(_actions["Time/OutPointReset"]); + + _actions["Time/InOutPointReset"] = std::make_shared( + "Reset In/Out Points", + dtk::Key::P, + static_cast(dtk::KeyModifier::Shift), + [this] + { + if (_document) { - app->getTimeUnitsModel()->setTimeUnits(TimeUnits::Seconds); + _document->getPlaybackModel()->resetInOutPoints(); } }); - _menus["TimeUnits"]->addItem(_actions["TimeUnits/Seconds"]); + _menus["Time"]->addItem(_actions["Time/InOutPointReset"]); - _timeUnitsObserver = dtk::ValueObserver::create( - app->getTimeUnitsModel()->observeTimeUnits(), - [this](TimeUnits value) + _actions["Time/InOutPointSelection"] = std::make_shared( + "Set In/Out Points To Selection", + dtk::Key::P, + static_cast(dtk::KeyModifier::Shift) | static_cast(dtk::KeyModifier::Control), + [this] { - _menus["TimeUnits"]->setItemChecked(_actions["TimeUnits/Timecode"], TimeUnits::Timecode == value); - _menus["TimeUnits"]->setItemChecked(_actions["TimeUnits/Frames"], TimeUnits::Frames == value); - _menus["TimeUnits"]->setItemChecked(_actions["TimeUnits/Seconds"], TimeUnits::Seconds == value); + if (_document) + { + const auto selection = _document->getSelectionModel()->getSelection(); + const OTIO_NS::TimeRange& timeRange = _document->getTimelineWrapper()->getTimeRange(); + const auto timeRangeOpt = getTimeRange( + selection, + timeRange.start_time(), + timeRange.duration().rate()); + if (timeRangeOpt.has_value()) + { + _document->getPlaybackModel()->setInOutRange(timeRangeOpt.value()); + } + else + { + _document->getPlaybackModel()->resetInOutPoints(); + } + } }); + _menus["Time"]->addItem(_actions["Time/InOutPointSelection"]); } void MenuBar::_playbackMenuInit( @@ -685,31 +802,39 @@ namespace toucan void MenuBar::_fileMenuUpdate() { - _menus["File"]->setItemEnabled(_actions["File/Close"], _document.get()); - _menus["File"]->setItemEnabled(_actions["File/CloseAll"], _document.get()); - _menus["File"]->setSubMenuEnabled(_menus["Files"], _document.get()); + const bool document = _document.get(); + _menus["File"]->setItemEnabled(_actions["File/Close"], document); + _menus["File"]->setItemEnabled(_actions["File/CloseAll"], document); + _menus["File"]->setSubMenuEnabled(_menus["Files"], document); _menus["File"]->setItemEnabled(_actions["File/Next"], _filesActions.size() > 1); _menus["File"]->setItemEnabled(_actions["File/Prev"], _filesActions.size() > 1); } void MenuBar::_selectMenuUpdate() { - _menus["Select"]->setItemEnabled(_actions["Select/All"], _document.get()); - _menus["Select"]->setItemEnabled(_actions["Select/None"], _document.get()); - _menus["Select"]->setItemEnabled(_actions["Select/Invert"], _document.get()); + const bool document = _document.get(); + _menus["Select"]->setItemEnabled(_actions["Select/All"], document); + _menus["Select"]->setItemEnabled(_actions["Select/AllTracks"], document); + _menus["Select"]->setItemEnabled(_actions["Select/AllClips"], document); + _menus["Select"]->setItemEnabled(_actions["Select/None"], document); + _menus["Select"]->setItemEnabled(_actions["Select/Invert"], document); } void MenuBar::_timeMenuUpdate() { - _menus["Time"]->setItemEnabled(_actions["Time/Start"], _document.get()); - _menus["Time"]->setItemEnabled(_actions["Time/Prev"], _document.get()); - _menus["Time"]->setItemEnabled(_actions["Time/Next"], _document.get()); - _menus["Time"]->setItemEnabled(_actions["Time/End"], _document.get()); + const bool document = _document.get(); + _menus["Time"]->setItemEnabled(_actions["Time/FrameStart"], document); + _menus["Time"]->setItemEnabled(_actions["Time/FramePrev"], document); + _menus["Time"]->setItemEnabled(_actions["Time/FrameNext"], document); + _menus["Time"]->setItemEnabled(_actions["Time/FrameEnd"], document); + _menus["Time"]->setItemEnabled(_actions["Time/ClipPrev"], document); + _menus["Time"]->setItemEnabled(_actions["Time/ClipNext"], document); } void MenuBar::_playbackMenuUpdate() { - if (_document) + const bool document = _document.get(); + if (document) { _playbackObserver = dtk::ValueObserver::create( _document->getPlaybackModel()->observePlayback(), @@ -725,10 +850,10 @@ namespace toucan _playbackObserver.reset(); } - _menus["Playback"]->setItemEnabled(_actions["Playback/Stop"], _document.get()); - _menus["Playback"]->setItemEnabled(_actions["Playback/Forward"], _document.get()); - _menus["Playback"]->setItemEnabled(_actions["Playback/Reverse"], _document.get()); - _menus["Playback"]->setItemEnabled(_actions["Playback/Toggle"], _document.get()); + _menus["Playback"]->setItemEnabled(_actions["Playback/Stop"], document); + _menus["Playback"]->setItemEnabled(_actions["Playback/Forward"], document); + _menus["Playback"]->setItemEnabled(_actions["Playback/Reverse"], document); + _menus["Playback"]->setItemEnabled(_actions["Playback/Toggle"], document); } void MenuBar::_windowMenuUpdate() @@ -737,7 +862,8 @@ namespace toucan void MenuBar::_viewMenuUpdate() { - if (_document) + const bool document = _document.get(); + if (document) { _frameViewObserver = dtk::ValueObserver::create( _document->getViewModel()->observeFrame(), @@ -751,9 +877,9 @@ namespace toucan _frameViewObserver.reset(); } - _menus["View"]->setItemEnabled(_actions["View/ZoomIn"], _document.get()); - _menus["View"]->setItemEnabled(_actions["View/ZoomOut"], _document.get()); - _menus["View"]->setItemEnabled(_actions["View/ZoomReset"], _document.get()); - _menus["View"]->setItemEnabled(_actions["View/Frame"], _document.get()); + _menus["View"]->setItemEnabled(_actions["View/ZoomIn"], document); + _menus["View"]->setItemEnabled(_actions["View/ZoomOut"], document); + _menus["View"]->setItemEnabled(_actions["View/ZoomReset"], document); + _menus["View"]->setItemEnabled(_actions["View/Frame"], document); } } diff --git a/lib/toucanView/MenuBar.h b/lib/toucanView/MenuBar.h index 0e5062c..643322c 100644 --- a/lib/toucanView/MenuBar.h +++ b/lib/toucanView/MenuBar.h @@ -4,7 +4,6 @@ #pragma once #include "PlaybackModel.h" -#include "TimeUnitsModel.h" #include "WindowModel.h" #include @@ -82,7 +81,6 @@ namespace toucan std::shared_ptr > _controlsObserver; std::shared_ptr > _displayScaleObserver; std::shared_ptr > _tooltipsObserver; - std::shared_ptr > _timeUnitsObserver; std::shared_ptr > _frameViewObserver; }; } diff --git a/lib/toucanView/PlaybackBar.cpp b/lib/toucanView/PlaybackBar.cpp index 57939e8..3daedcb 100644 --- a/lib/toucanView/PlaybackBar.cpp +++ b/lib/toucanView/PlaybackBar.cpp @@ -29,12 +29,19 @@ namespace toucan _durationLabel = TimeLabel::create(context, app->getTimeUnitsModel(), _layout); _durationLabel->setTooltip("Timeline duration"); + _timeUnitsComboBox = dtk::ComboBox::create( + context, + { "Timecode", "Frames", "Seconds" }, + _layout); + _frameButtons->setCallback( - [this](FrameAction value) + [this](TimeAction value) { if (_document) { - _document->getPlaybackModel()->frameAction(value); + _document->getPlaybackModel()->timeAction( + value, + _document->getTimeline()); } }); @@ -56,6 +63,16 @@ namespace toucan } }); + auto appWeak = std::weak_ptr(app); + _timeUnitsComboBox->setIndexCallback( + [appWeak](int value) + { + if (auto app = appWeak.lock()) + { + app->getTimeUnitsModel()->setTimeUnits(static_cast(value)); + } + }); + _documentObserver = dtk::ValueObserver >::create( app->getDocumentsModel()->observeCurrent(), [this](const std::shared_ptr& document) @@ -107,6 +124,13 @@ namespace toucan _timeEdit->setEnabled(document.get()); _durationLabel->setEnabled(document.get()); }); + + _timeUnitsObserver = dtk::ValueObserver::create( + app->getTimeUnitsModel()->observeTimeUnits(), + [this](TimeUnits value) + { + _timeUnitsComboBox->setCurrentIndex(static_cast(value)); + }); } PlaybackBar::~PlaybackBar() diff --git a/lib/toucanView/PlaybackBar.h b/lib/toucanView/PlaybackBar.h index 8446f5d..9f05d64 100644 --- a/lib/toucanView/PlaybackBar.h +++ b/lib/toucanView/PlaybackBar.h @@ -3,8 +3,10 @@ #pragma once +#include "TimeUnitsModel.h" #include "TimeWidgets.h" +#include #include #include @@ -48,11 +50,13 @@ namespace toucan std::shared_ptr _playbackButtons; std::shared_ptr _timeEdit; std::shared_ptr _durationLabel; + std::shared_ptr _timeUnitsComboBox; std::shared_ptr > > _documentObserver; std::shared_ptr > _timeRangeObserver; std::shared_ptr > _currentTimeObserver; std::shared_ptr > _playbackObserver; + std::shared_ptr > _timeUnitsObserver; }; } diff --git a/lib/toucanView/PlaybackModel.cpp b/lib/toucanView/PlaybackModel.cpp index 3cc35e3..73e4667 100644 --- a/lib/toucanView/PlaybackModel.cpp +++ b/lib/toucanView/PlaybackModel.cpp @@ -3,12 +3,17 @@ #include "PlaybackModel.h" +#include + +#include + namespace toucan { PlaybackModel::PlaybackModel(const std::shared_ptr& context) { _timeRange = dtk::ObservableValue::create(); _currentTime = dtk::ObservableValue::create(OTIO_NS::RationalTime(-1.0, -1.0)); + _inOutRange = dtk::ObservableValue::create(); _playback = dtk::ObservableValue::create(Playback::Stop); _timer = dtk::Timer::create(context); _timer->setRepeating(true); @@ -32,6 +37,7 @@ namespace toucan if (_timeRange->setIfChanged(value)) { _currentTime->setIfChanged(value.start_time()); + _inOutRange->setIfChanged(value); } } @@ -45,50 +51,239 @@ namespace toucan return _currentTime; } - void PlaybackModel::setCurrentTime(const OTIO_NS::RationalTime& value) + void PlaybackModel::setCurrentTime( + const OTIO_NS::RationalTime& value, + CurrentTime behavior) { OTIO_NS::RationalTime time = value; if (!time.is_invalid_time()) { - const OTIO_NS::TimeRange& range = _timeRange->get(); - if (time > range.end_time_inclusive()) + const OTIO_NS::TimeRange& range = _inOutRange->get(); + switch (behavior) { - time = range.start_time(); - } - else if (time < range.start_time()) - { - time = range.end_time_inclusive(); + case CurrentTime::Clamp: + if (time > range.end_time_inclusive()) + { + time = range.end_time_inclusive(); + } + else if (time < range.start_time()) + { + time = range.start_time(); + } + break; + case CurrentTime::Loop: + if (time > range.end_time_inclusive()) + { + time = range.start_time(); + } + else if (time < range.start_time()) + { + time = range.end_time_inclusive(); + } + break; + default: break; } } - if (time != _currentTime->get()) - { - _playback->setIfChanged(Playback::Stop); - _currentTime->setIfChanged(time); - } + _currentTime->setIfChanged(time); } - void PlaybackModel::frameAction(FrameAction value) + void PlaybackModel::timeAction( + TimeAction value, + const OTIO_NS::SerializableObject::Retainer& timeline) { const OTIO_NS::TimeRange& timeRange = _timeRange->get(); - const OTIO_NS::RationalTime& time = _currentTime->get(); + const OTIO_NS::RationalTime& currentTime = _currentTime->get(); switch (value) { - case FrameAction::Start: + case TimeAction::FrameStart: setCurrentTime(timeRange.start_time()); break; - case FrameAction::Prev: - setCurrentTime(time - OTIO_NS::RationalTime(1.0, time.rate())); + case TimeAction::FramePrev: + setCurrentTime(currentTime - OTIO_NS::RationalTime(1.0, currentTime.rate())); break; - case FrameAction::Next: - setCurrentTime(time + OTIO_NS::RationalTime(1.0, time.rate())); + case TimeAction::FrameNext: + setCurrentTime(currentTime + OTIO_NS::RationalTime(1.0, currentTime.rate())); break; - case FrameAction::End: + case TimeAction::FrameEnd: setCurrentTime(timeRange.end_time_inclusive()); break; + case TimeAction::ClipNext: + { + std::optional min; + std::optional diff; + std::optional t; + const auto clips = getVideoClips(timeline); + auto i = clips.begin(); + if (i != clips.end()) + { + const auto clipRangeOpt = (*i)->trimmed_range_in_parent(); + if (clipRangeOpt.has_value()) + { + const OTIO_NS::RationalTime start = + clipRangeOpt.value().start_time() + + timeRange.start_time(); + min = start; + if (start > currentTime) + { + diff = currentTime - start; + diff = OTIO_NS::RationalTime(std::abs(diff.value().value()), diff.value().rate()); + t = start; + } + } + ++i; + } + for (; i != clips.end(); ++i) + { + const auto clipRangeOpt = (*i)->trimmed_range_in_parent(); + if (clipRangeOpt.has_value()) + { + const OTIO_NS::RationalTime start = + clipRangeOpt.value().start_time() + + timeRange.start_time(); + min = min.has_value() ? std::min(min.value(), start) : start; + if (start > currentTime) + { + OTIO_NS::RationalTime clipDiff = currentTime - start; + clipDiff = OTIO_NS::RationalTime(std::abs(clipDiff.value()), clipDiff.rate()); + if (!diff.has_value() || clipDiff < diff) + { + diff = clipDiff; + t = start; + } + } + } + } + if (t.has_value()) + { + setCurrentTime(t.value(), CurrentTime::Free); + } + else if (min.has_value()) + { + setCurrentTime(min.value(), CurrentTime::Free); + } + break; + } + case TimeAction::ClipPrev: + { + std::optional max; + std::optional diff; + std::optional t; + const auto clips = getVideoClips(timeline); + auto i = clips.begin(); + if (i != clips.end()) + { + const auto clipRangeOpt = (*i)->trimmed_range_in_parent(); + if (clipRangeOpt.has_value()) + { + const OTIO_NS::RationalTime start = + clipRangeOpt.value().start_time() + + timeRange.start_time(); + max = start; + if (start < currentTime) + { + diff = currentTime - start; + diff = OTIO_NS::RationalTime(std::abs(diff.value().value()), diff.value().rate()); + t = start; + } + } + ++i; + } + for (; i != clips.end(); ++i) + { + const auto clipRangeOpt = (*i)->trimmed_range_in_parent(); + if (clipRangeOpt.has_value()) + { + const OTIO_NS::RationalTime start = + clipRangeOpt.value().start_time() + + timeRange.start_time(); + max = max.has_value() ? std::max(max.value(), start) : start; + if (start < currentTime) + { + OTIO_NS::RationalTime clipDiff = currentTime - start; + clipDiff = OTIO_NS::RationalTime(std::abs(clipDiff.value()), clipDiff.rate()); + if (!diff.has_value() || clipDiff < diff) + { + diff = clipDiff; + t = start; + } + } + } + } + if (t.has_value()) + { + setCurrentTime(t.value(), CurrentTime::Free); + } + else if (max.has_value()) + { + setCurrentTime(max.value(), CurrentTime::Free); + } + break; + } default: break; } } + const OTIO_NS::TimeRange& PlaybackModel::getInOutRange() const + { + return _inOutRange->get(); + } + + std::shared_ptr > PlaybackModel::observeInOutRange() const + { + return _inOutRange; + } + + void PlaybackModel::setInOutRange(const OTIO_NS::TimeRange& value) + { + //! \bug OTIO_NS::TimeRange::clamped() seems to be off by one? + //const OTIO_NS::TimeRange clamped = value.clamped(_timeRange->get()); + const OTIO_NS::TimeRange clamped = OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + std::max(value.start_time(), _timeRange->get().start_time()), + std::min(value.end_time_inclusive(), _timeRange->get().end_time_inclusive())); + _inOutRange->setIfChanged(clamped); + if (_currentTime->get() < clamped.start_time()) + { + setCurrentTime(clamped.start_time()); + } + else if (_currentTime->get() > clamped.end_time_inclusive()) + { + setCurrentTime(clamped.end_time_inclusive()); + } + } + + void PlaybackModel::setInPoint(const OTIO_NS::RationalTime& value) + { + setInOutRange(OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + value, + _inOutRange->get().end_time_inclusive())); + } + + void PlaybackModel::resetInPoint() + { + setInOutRange(OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + _timeRange->get().start_time(), + _inOutRange->get().end_time_inclusive())); + } + + void PlaybackModel::setOutPoint(const OTIO_NS::RationalTime& value) + { + setInOutRange(OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + _inOutRange->get().start_time(), + value)); + } + + void PlaybackModel::resetOutPoint() + { + setInOutRange(OTIO_NS::TimeRange::range_from_start_end_time_inclusive( + _inOutRange->get().start_time(), + _timeRange->get().end_time_inclusive())); + } + + void PlaybackModel::resetInOutPoints() + { + setInOutRange(_timeRange->get()); + } + Playback PlaybackModel::getPlayback() const { return _playback->get(); @@ -112,6 +307,7 @@ namespace toucan break; case Playback::Forward: case Playback::Reverse: + setCurrentTime(_currentTime->get()); _timer->start( std::chrono::microseconds(static_cast(1000 / _currentTime->get().rate())), [this] @@ -137,25 +333,15 @@ namespace toucan switch (_playback->get()) { case Playback::Forward: - { - auto time = _currentTime->get() + OTIO_NS::RationalTime(1.0, _currentTime->get().rate()); - if (time > _timeRange->get().end_time_inclusive()) - { - time = _timeRange->get().start_time(); - } - _currentTime->setIfChanged(time); + setCurrentTime( + _currentTime->get() + OTIO_NS::RationalTime(1.0, _currentTime->get().rate()), + CurrentTime::Loop); break; - } case Playback::Reverse: - { - auto time = _currentTime->get() - OTIO_NS::RationalTime(1.0, _currentTime->get().rate()); - if (time < _timeRange->get().start_time()) - { - time = _timeRange->get().end_time_inclusive(); - } - _currentTime->setIfChanged(time); + setCurrentTime( + _currentTime->get() - OTIO_NS::RationalTime(1.0, _currentTime->get().rate()), + CurrentTime::Loop); break; - } default: break; } } diff --git a/lib/toucanView/PlaybackModel.h b/lib/toucanView/PlaybackModel.h index 3cc7e6e..b1327ef 100644 --- a/lib/toucanView/PlaybackModel.h +++ b/lib/toucanView/PlaybackModel.h @@ -10,14 +10,26 @@ namespace toucan { - enum FrameAction + //! Current time behavior. + enum CurrentTime { - Start, - Prev, - Next, - End + Free, + Clamp, + Loop }; + //! Time actions. + enum TimeAction + { + FrameStart, + FramePrev, + FrameNext, + FrameEnd, + ClipNext, + ClipPrev + }; + + //! Playback actions. enum class Playback { Stop, @@ -25,6 +37,7 @@ namespace toucan Reverse }; + //! Playback model. class PlaybackModel : public std::enable_shared_from_this { public: @@ -32,19 +45,65 @@ namespace toucan virtual ~PlaybackModel(); + //! Get the time range. const OTIO_NS::TimeRange& getTimeRange() const; + + //! Observe the time range. std::shared_ptr > observeTimeRange() const; + + //! Set the time range. void setTimeRange(const OTIO_NS::TimeRange&); + //! Get the current time. const OTIO_NS::RationalTime& getCurrentTime() const; + + //! Observe the current time. std::shared_ptr > observeCurrentTime() const; - void setCurrentTime(const OTIO_NS::RationalTime&); - void frameAction(FrameAction); + //! Set the current time. + void setCurrentTime( + const OTIO_NS::RationalTime&, + CurrentTime = CurrentTime::Clamp); + + //! Time actions. + void timeAction( + TimeAction, + const OTIO_NS::SerializableObject::Retainer&); + + //! Get the in/out range. + const OTIO_NS::TimeRange& getInOutRange() const; + + //! Observe the in/out range. + std::shared_ptr > observeInOutRange() const; + //! Set the in/out range. + void setInOutRange(const OTIO_NS::TimeRange&); + + //! Set the in point. + void setInPoint(const OTIO_NS::RationalTime&); + + //! Reset the in point. + void resetInPoint(); + + //! Set the out point. + void setOutPoint(const OTIO_NS::RationalTime&); + + //! Reset the in point. + void resetOutPoint(); + + //! Reset the in/out points. + void resetInOutPoints(); + + //! Get the playback. Playback getPlayback() const; + + //! Observe the playback. std::shared_ptr > observePlayback() const; + + //! Set the playback. void setPlayback(Playback); + + //! Toggle the playback. void togglePlayback(); private: @@ -52,6 +111,7 @@ namespace toucan std::shared_ptr > _timeRange; std::shared_ptr > _currentTime; + std::shared_ptr > _inOutRange; std::shared_ptr > _playback; Playback _playbackPrev = Playback::Forward; std::shared_ptr _timer; diff --git a/lib/toucanView/SelectionModel.cpp b/lib/toucanView/SelectionModel.cpp index c3c0a65..39c39e1 100644 --- a/lib/toucanView/SelectionModel.cpp +++ b/lib/toucanView/SelectionModel.cpp @@ -3,6 +3,8 @@ #include "SelectionModel.h" +#include + #include namespace OTIO_NS @@ -51,14 +53,33 @@ namespace toucan _selection->setIfChanged(tmp); } - void SelectionModel::selectAll(const OTIO_NS::SerializableObject::Retainer& timeline) + void SelectionModel::selectAll( + const OTIO_NS::SerializableObject::Retainer& timeline, + SelectionType type) { std::vector > items; - //! \bug The stack is not returned by find_children? - items.push_back(timeline->tracks()); - for (auto& item : timeline->find_children()) + switch (type) { - items.push_back(item); + case SelectionType::All: + //! \bug The stack is not returned by find_children? + items.push_back(timeline->tracks()); + for (auto& item : timeline->find_children()) + { + items.push_back(item); + } + break; + case SelectionType::Tracks: + for (auto& track : timeline->find_children()) + { + items.push_back(OTIO_NS::dynamic_retainer_cast(track)); + } + break; + case SelectionType::Clips: + for (auto& clip : timeline->find_children()) + { + items.push_back(OTIO_NS::dynamic_retainer_cast(clip)); + } + break; } _selection->setIfChanged(items); } diff --git a/lib/toucanView/SelectionModel.h b/lib/toucanView/SelectionModel.h index 9f73129..978f9df 100644 --- a/lib/toucanView/SelectionModel.h +++ b/lib/toucanView/SelectionModel.h @@ -20,6 +20,15 @@ namespace OTIO_NS namespace toucan { + //! Selection type. + enum class SelectionType + { + All, + Tracks, + Clips + }; + + //! Selection model. class SelectionModel : public std::enable_shared_from_this { public: @@ -27,12 +36,24 @@ namespace toucan virtual ~SelectionModel(); + //! Get the selection. const std::vector >& getSelection() const; + + //! Observe the selection. std::shared_ptr > > observeSelection() const; + + //! Set the seldction. void setSelection(const std::vector >&); - void selectAll(const OTIO_NS::SerializableObject::Retainer&); + //! Select all of the given type. + void selectAll( + const OTIO_NS::SerializableObject::Retainer&, + SelectionType = SelectionType::All); + + //! Clear the selection. void clearSelection(); + + //! Invert the selection. void invertSelection(const OTIO_NS::SerializableObject::Retainer&); private: diff --git a/lib/toucanView/TimeWidgets.cpp b/lib/toucanView/TimeWidgets.cpp index ff4be21..98b1a39 100644 --- a/lib/toucanView/TimeWidgets.cpp +++ b/lib/toucanView/TimeWidgets.cpp @@ -47,7 +47,7 @@ namespace toucan { if (_callback) { - _callback(static_cast(index)); + _callback(static_cast(index)); } }); } @@ -64,7 +64,7 @@ namespace toucan return out; } - void FrameButtons::setCallback(const std::function& value) + void FrameButtons::setCallback(const std::function& value) { _callback = value; } diff --git a/lib/toucanView/TimeWidgets.h b/lib/toucanView/TimeWidgets.h index b909ba0..f95a34d 100644 --- a/lib/toucanView/TimeWidgets.h +++ b/lib/toucanView/TimeWidgets.h @@ -30,13 +30,13 @@ namespace toucan const std::shared_ptr&, const std::shared_ptr& parent = nullptr); - void setCallback(const std::function&); + void setCallback(const std::function&); void setGeometry(const dtk::Box2I&) override; void sizeHintEvent(const dtk::SizeHintEvent&) override; private: - std::function _callback; + std::function _callback; std::shared_ptr _layout; std::shared_ptr _buttonGroup; }; diff --git a/lib/toucanView/TimelineItem.cpp b/lib/toucanView/TimelineItem.cpp index ded6461..34deac7 100644 --- a/lib/toucanView/TimelineItem.cpp +++ b/lib/toucanView/TimelineItem.cpp @@ -33,6 +33,7 @@ namespace toucan 0 | static_cast(dtk::KeyModifier::Shift) | static_cast(dtk::KeyModifier::Control)); _timeline = document->getTimeline(); + _timeRange = document->getTimelineWrapper()->getTimeRange(); _timeUnitsModel = app->getTimeUnitsModel(); _selectionModel = document->getSelectionModel(); _thumbnails.setMax(100); @@ -88,6 +89,14 @@ namespace toucan _currentTimeCallback = value; } + void TimelineItem::setInOutRange(const OTIO_NS::TimeRange& value) + { + if (value == _inOutRange) + return; + _inOutRange = value; + _setDrawUpdate(); + } + void TimelineItem::setGeometry(const dtk::Box2I& value) { IItem::setGeometry(value); @@ -240,6 +249,21 @@ namespace toucan _size.fontMetrics.lineHeight + _size.margin * 2); event.render->drawRect(g2, event.style->getColorRole(dtk::ColorRole::Base)); + if (_inOutRange != _timeRange) + { + const int x0 = timeToPos(_inOutRange.start_time()); + const int x1 = timeToPos(_inOutRange.end_time_exclusive()); + dtk::Color4F color = event.style->getColorRole(dtk::ColorRole::Yellow); + color.a = .5F; + event.render->drawRect( + dtk::Box2I( + x0, + g.min.y + _size.scrollPos.y, + x1 - x0, + _size.fontMetrics.lineHeight + _size.margin * 2), + color); + } + _drawTimeTicks(drawRect, event); _drawTimeLabels(drawRect, event); @@ -289,7 +313,6 @@ namespace toucan static_cast(dtk::KeyModifier::Shift) == event.modifiers || static_cast(dtk::KeyModifier::Control) == event.modifiers)) { - event.accept = true; auto selection = _select(shared_from_this(), event.pos); OTIO_NS::SerializableObject::Retainer item; if (selection) @@ -298,6 +321,8 @@ namespace toucan } if (selection && item) { + event.accept = true; + takeKeyFocus(); _mouse.mode = MouseMode::Select; auto selectionPrev = _selectionModel->getSelection(); std::vector > selectionNew; @@ -321,8 +346,10 @@ namespace toucan } _selectionModel->setSelection(selectionNew); } - else + else if (0 == event.modifiers) { + event.accept = true; + takeKeyFocus(); _mouse.mode = MouseMode::CurrentTime; _currentTime = posToTime(_getMousePos().x); if (_currentTimeCallback) diff --git a/lib/toucanView/TimelineItem.h b/lib/toucanView/TimelineItem.h index e74f1ec..9553a9e 100644 --- a/lib/toucanView/TimelineItem.h +++ b/lib/toucanView/TimelineItem.h @@ -37,6 +37,8 @@ namespace toucan void setCurrentTime(const OTIO_NS::RationalTime&); void setCurrentTimeCallback(const std::function&); + void setInOutRange(const OTIO_NS::TimeRange&); + void setGeometry(const dtk::Box2I&) override; void tickEvent( bool parentsVisible, @@ -71,8 +73,10 @@ namespace toucan const std::vector >&); OTIO_NS::SerializableObject::Retainer _timeline; + OTIO_NS::TimeRange _timeRange; OTIO_NS::RationalTime _currentTime = OTIO_NS::RationalTime(-1.0, -1.0); std::function _currentTimeCallback; + OTIO_NS::TimeRange _inOutRange; std::shared_ptr _timeUnitsModel; std::shared_ptr _selectionModel; std::shared_ptr _thumbnailGenerator; diff --git a/lib/toucanView/TimelineWidget.cpp b/lib/toucanView/TimelineWidget.cpp index ab892c3..2331b34 100644 --- a/lib/toucanView/TimelineWidget.cpp +++ b/lib/toucanView/TimelineWidget.cpp @@ -23,7 +23,10 @@ namespace toucan IWidget::_init(context, "toucan::TimelineWidget", parent); _setMouseHoverEnabled(true); - _setMousePressEnabled(true, 0, static_cast(dtk::KeyModifier::Control)); + _setMousePressEnabled( + true, + 0, + static_cast(dtk::KeyModifier::Control)); _frameView = dtk::ObservableValue::create(true); @@ -51,7 +54,7 @@ namespace toucan { if (_document) { - _document->getPlaybackModel()->setCurrentTime(value); + _document->getPlaybackModel()->setCurrentTime(value, CurrentTime::Free); } }); _scrollWidget->setWidget(_timelineItem); @@ -67,6 +70,17 @@ namespace toucan } _scrollUpdate(); }); + + _inOutRangeObserver = dtk::ValueObserver::create( + document->getPlaybackModel()->observeInOutRange(), + [this](const OTIO_NS::TimeRange& value) + { + _inOutRange = value; + if (_timelineItem) + { + _timelineItem->setInOutRange(_inOutRange); + } + }); } else { diff --git a/lib/toucanView/TimelineWidget.h b/lib/toucanView/TimelineWidget.h index d73d1d9..dcee4f4 100644 --- a/lib/toucanView/TimelineWidget.h +++ b/lib/toucanView/TimelineWidget.h @@ -64,6 +64,7 @@ namespace toucan std::shared_ptr _document; OTIO_NS::TimeRange _timeRange; OTIO_NS::RationalTime _currentTime; + OTIO_NS::TimeRange _inOutRange; double _scale = 100.0; bool _sizeInit = true; std::shared_ptr > _frameView; @@ -86,5 +87,6 @@ namespace toucan std::shared_ptr > > _documentObserver; std::shared_ptr > _currentTimeObserver; + std::shared_ptr > _inOutRangeObserver; }; }