diff options
| author | Carl Hetherington <cth@carlh.net> | 2023-12-13 00:42:22 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2024-03-12 23:41:00 +0100 |
| commit | da46a695431d3b573924e53ac1a0163056a1a5b5 (patch) | |
| tree | 936e6ae61f3f1eac565e48babf169de1833f9ac8 /src/wx/dcp_timeline.cc | |
| parent | 1d028a206c999bf61df84544e3aeb70cec4e505c (diff) | |
Add new interface for setting reel breaks (#2678).2678-reel-break
Diffstat (limited to 'src/wx/dcp_timeline.cc')
| -rw-r--r-- | src/wx/dcp_timeline.cc | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/src/wx/dcp_timeline.cc b/src/wx/dcp_timeline.cc new file mode 100644 index 000000000..3974997eb --- /dev/null +++ b/src/wx/dcp_timeline.cc @@ -0,0 +1,615 @@ +/* + Copyright (C) 2023 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic 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 2 of the License, or + (at your option) any later version. + + DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "check_box.h" +#include "colours.h" +#include "dcp_timeline.h" +#include "dcp_timeline_reel_marker_view.h" +#include "dcpomatic_choice.h" +#include "dcpomatic_spin_ctrl.h" +#include "timecode.h" +#include "wx_util.h" +#include "lib/atmos_content.h" +#include "lib/audio_content.h" +#include "lib/constants.h" +#include "lib/film.h" +#include "lib/text_content.h" +#include "lib/video_content.h" +#include <dcp/scope_guard.h> +LIBDCP_DISABLE_WARNINGS +#include <wx/graphics.h> +LIBDCP_ENABLE_WARNINGS + + +using std::dynamic_pointer_cast; +using std::make_shared; +using std::shared_ptr; +using std::vector; +using boost::optional; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif +using namespace dcpomatic; + + +auto constexpr reel_marker_y_pos = 48; +auto constexpr content_y_pos = 112; +auto constexpr content_type_height = 12; + +enum { + ID_add_reel_boundary, +}; + + +class ReelBoundary +{ +public: + ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable) + : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2))) + , _timecode(new Timecode<DCPTime>(parent, true)) + , _index(index) + , _view(timeline, reel_marker_y_pos) + , _fps(fps) + { + sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + sizer->Add(_timecode, wxGBPosition(index, 1)); + + _timecode->set_maximum(maximum.split(fps)); + _timecode->set_editable(editable); + _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this)); + } + + ~ReelBoundary() + { + if (_label) { + _label->Destroy(); + } + + if (_timecode) { + _timecode->Destroy(); + } + } + + ReelBoundary(ReelBoundary const&) = delete; + ReelBoundary& operator=(ReelBoundary const&) = delete; + + ReelBoundary(ReelBoundary&& other) = delete; + ReelBoundary& operator=(ReelBoundary&& other) = delete; + + void set_time(DCPTime time) + { + if (_timecode) { + _timecode->set(time, _fps); + } + _view.set_time(time); + } + + dcpomatic::DCPTime time() const { + return _view.time(); + } + + int index() const { + return _index; + } + + DCPTimelineReelMarkerView& view() { + return _view; + } + + DCPTimelineReelMarkerView const& view() const { + return _view; + } + + boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed; + +private: + void timecode_changed() { + set_time(_timecode->get(_fps)); + Changed(_index, time()); + } + + wxStaticText* _label = nullptr; + Timecode<dcpomatic::DCPTime>* _timecode = nullptr; + int _index = 0; + DCPTimelineReelMarkerView _view; + int _fps; +}; + + +DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film) + : Timeline(parent) + , _film(film) + , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)) + , _reel_settings(new wxPanel(this, wxID_ANY)) + , _reel_detail(new wxPanel(this, wxID_ANY)) + , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP)) +{ +#ifndef __WXOSX__ + _canvas->SetDoubleBuffered(true); +#endif + _reel_detail->SetSizer(_reel_detail_sizer); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(_reel_settings, 0); + sizer->Add(_canvas, 0, wxEXPAND); + sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + SetSizer(sizer); + + SetMinSize(wxSize(640, 480)); + _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4}); + + _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this)); + _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this)); + _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1)); + _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1)); + _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1)); + _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1)); + + _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2)); + + _menu = new wxMenu; + _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel")); + _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this)); + + setup_reel_settings(); + setup_reel_boundaries(); + + sizer->Layout(); + setup_pixels_per_second(); + setup_sensitivity(); +} + + +void +DCPTimeline::add_reel_boundary() +{ + auto boundaries = film()->custom_reel_boundaries(); + boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1))); + film()->set_custom_reel_boundaries(boundaries); +} + + +void +DCPTimeline::film_changed(ChangeType type, FilmProperty property) +{ + if (type != ChangeType::DONE) { + return; + } + + switch (property) { + case FilmProperty::REEL_TYPE: + case FilmProperty::REEL_LENGTH: + case FilmProperty::CUSTOM_REEL_BOUNDARIES: + setup_sensitivity(); + setup_reel_boundaries(); + break; + case FilmProperty::CONTENT: + case FilmProperty::CONTENT_ORDER: + setup_pixels_per_second(); + Refresh(); + break; + default: + break; + } +} + + +void +DCPTimeline::setup_sensitivity() +{ + _snap->Enable(editable()); + _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH); +} + + +void +DCPTimeline::setup_reel_settings() +{ + auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _reel_settings->SetSizer(sizer); + + int r = 0; + add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0)); + _reel_type = new Choice(_reel_settings); + _reel_type->add(_("Single reel")); + _reel_type->add(_("Split by video content")); + _reel_type->add(_("Split by maximum reel size")); + _reel_type->add(_("Custom")); + sizer->Add(_reel_type, wxGBPosition(r, 1)); + ++r; + + add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0)); + _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH); + _maximum_reel_size->SetRange(1, 1000); + { + auto s = new wxBoxSizer(wxHORIZONTAL); + s->Add(_maximum_reel_size, 0); + add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT); + sizer->Add(s, wxGBPosition(r, 1)); + } + ++r; + + _snap = new CheckBox(_reel_settings, _("Snap when dragging")); + sizer->Add(_snap, wxGBPosition(r, 1)); + ++r; + + _reel_type->set(static_cast<int>(film()->reel_type())); + _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL); + + _reel_type->bind(&DCPTimeline::reel_mode_changed, this); + _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this)); +} + + +void +DCPTimeline::reel_mode_changed() +{ + film()->set_reel_type(static_cast<ReelType>(*_reel_type->get())); +} + + +void +DCPTimeline::maximum_reel_size_changed() +{ + film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL); +} + + +void +DCPTimeline::set_reel_boundary(int index, DCPTime time) +{ + auto boundaries = film()->custom_reel_boundaries(); + DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size())); + boundaries[index] = time.round(film()->video_frame_rate()); + film()->set_custom_reel_boundaries(boundaries); +} + + +void +DCPTimeline::setup_reel_boundaries() +{ + auto const reels = film()->reels(); + if (reels.empty()) { + _reel_boundaries.clear(); + return; + } + + size_t const boundaries = reels.size() - 1; + auto const maximum = film()->length(); + for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) { + auto boundary = std::make_shared<ReelBoundary>( + _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable() + ); + + boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2)); + _reel_boundaries.push_back(boundary); + } + + _reel_boundaries.resize(boundaries); + + auto const active = editable(); + for (size_t i = 0; i < boundaries; ++i) { + _reel_boundaries[i]->set_time(reels[i].to); + _reel_boundaries[i]->view().set_active(active); + } + + _reel_detail_sizer->Layout(); + _canvas->Refresh(); +} + + +void +DCPTimeline::paint() +{ + wxPaintDC dc(_canvas); + dc.Clear(); + + if (film()->content().empty()) { + return; + } + + _canvas->DoPrepareDC(dc); + + auto gc = wxGraphicsContext::Create(dc); + if (!gc) { + return; + } + + dcp::ScopeGuard sg = [gc]() { delete gc; }; + + gc->SetAntialiasMode(wxANTIALIAS_DEFAULT); + + paint_reels(gc); + paint_content(gc); +} + + +void +DCPTimeline::paint_reels(wxGraphicsContext* gc) +{ + constexpr int x_offset = 2; + + for (auto const& boundary: _reel_boundaries) { + boundary->view().paint(gc); + } + + gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0))); + gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID)); + + auto const pps = pixels_per_second().get_value_or(1); + + auto start = gc->CreatePath(); + start.MoveToPoint(x_offset, reel_marker_y_pos); + start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT); + gc->StrokePath(start); + + auto const length = film()->length().seconds() * pps; + auto end = gc->CreatePath(); + end.MoveToPoint(x_offset + length, reel_marker_y_pos); + end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT); + gc->StrokePath(end); + + auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4; + + auto paint_reel = [gc, y](double from, double to, int index) { + auto path = gc->CreatePath(); + path.MoveToPoint(from, y); + path.AddLineToPoint(to, y); + gc->StrokePath(path); + + auto str = wxString::Format(wxT("#%d"), index + 1); + wxDouble str_width; + wxDouble str_height; + wxDouble str_descent; + wxDouble str_leading; + gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading); + + if (str_width < (to - from)) { + gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2); + } + }; + + gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT)); + int index = 0; + DCPTime last; + for (auto const& boundary: _reel_boundaries) { + paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++); + last = boundary->time(); + } + + paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index); +} + + +void +DCPTimeline::paint_content(wxGraphicsContext* gc) +{ + auto const pps = pixels_per_second().get_value_or(1); + auto const film = this->film(); + + auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID); + auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT); + + auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID); + + auto maybe_draw = + [gc, film, pps, solid_pen, dotted_pen] + (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) { + if (part) { + auto const y = content_y_pos + offset * content_type_height + 1; + gc->SetPen(solid_pen); + gc->SetBrush(brush); + gc->DrawRectangle( + content->position().seconds() * pps, + y, + content->length_after_trim(film).seconds() * pps, + content_type_height - 2 + ); + + gc->SetPen(dotted_pen); + for (auto split: content->reel_split_points(film)) { + if (split != content->position()) { + auto path = gc->CreatePath(); + path.MoveToPoint(split.seconds() * pps, y); + path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2); + gc->StrokePath(path); + } + } + } + }; + + for (auto content: film->content()) { + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0); + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1); + for (auto text: content->text) { + maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2); + } + maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3); + } +} + + +void +DCPTimeline::setup_pixels_per_second() +{ + set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds())); +} + + +shared_ptr<ReelBoundary> +DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const +{ + Position<int> const position(ev.GetX(), ev.GetY()); + auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) { + return boundary->view().bbox().contains(position); + }); + + if (iter == _reel_boundaries.end()) { + return {}; + } + + return *iter; +} + + +void +DCPTimeline::left_down(wxMouseEvent& ev) +{ + if (!editable()) { + return; + } + + if (auto boundary = event_to_reel_boundary(ev)) { + auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION); + _drag = DCPTimeline::Drag( + boundary, + _reel_boundaries, + film(), + static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)), + _snap->get(), + snap_distance + ); + } else { + _drag = boost::none; + } +} + + +void +DCPTimeline::right_down(wxMouseEvent& ev) +{ + _right_down_position = ev.GetPosition(); + _canvas->PopupMenu(_menu, _right_down_position); +} + + +void +DCPTimeline::left_up(wxMouseEvent&) +{ + if (!_drag) { + return; + } + + set_reel_boundary(_drag->reel_boundary->index(), _drag->time()); + _drag = boost::none; +} + + +void +DCPTimeline::mouse_moved(wxMouseEvent& ev) +{ + if (!_drag) { + return; + } + + auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1)); + time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time); + time = std::min(_drag->next ? _drag->next->time() : film()->length(), time); + _drag->set_time(time); + _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true); +} + + +void +DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r) +{ + _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false); +} + + +shared_ptr<Film> +DCPTimeline::film() const +{ + auto film = _film.lock(); + DCPOMATIC_ASSERT(film); + return film; +} + + +bool +DCPTimeline::editable() const +{ + return film()->reel_type() == ReelType::CUSTOM; +} + + +DCPTimeline::Drag::Drag( + shared_ptr<ReelBoundary> reel_boundary_, + vector<shared_ptr<ReelBoundary>> const& reel_boundaries, + shared_ptr<const Film> film, + int offset_, + bool snap, + DCPTime snap_distance + ) + : reel_boundary(reel_boundary_) + , offset(offset_) + , _snap_distance(snap_distance) +{ + auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary); + auto index = std::distance(reel_boundaries.begin(), iter); + + if (index > 0) { + previous = reel_boundaries[index - 1]; + } + if (index < static_cast<int>(reel_boundaries.size() - 1)) { + next = reel_boundaries[index + 1]; + } + + if (snap) { + for (auto content: film->content()) { + for (auto split: content->reel_split_points(film)) { + _snaps.push_back(split); + } + } + } +} + + +void +DCPTimeline::Drag::set_time(DCPTime time) +{ + optional<DCPTime> nearest_distance; + optional<DCPTime> nearest_time; + for (auto snap: _snaps) { + auto const distance = time - snap; + if (!nearest_distance || distance.abs() < nearest_distance->abs()) { + nearest_distance = distance.abs(); + nearest_time = snap; + } + } + + if (nearest_distance && *nearest_distance < _snap_distance) { + reel_boundary->set_time(*nearest_time); + } else { + reel_boundary->set_time(time); + } +} + + +DCPTime +DCPTimeline::Drag::time() const +{ + return reel_boundary->time(); +} + |
