2 Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "check_box.h"
24 #include "dcp_timeline.h"
25 #include "dcp_timeline_reel_marker_view.h"
26 #include "dcpomatic_choice.h"
27 #include "dcpomatic_spin_ctrl.h"
31 #include "lib/atmos_content.h"
32 #include "lib/audio_content.h"
33 #include "lib/constants.h"
35 #include "lib/text_content.h"
36 #include "lib/video_content.h"
37 #include <dcp/scope_guard.h>
38 LIBDCP_DISABLE_WARNINGS
39 #include <wx/graphics.h>
40 LIBDCP_ENABLE_WARNINGS
43 using std::dynamic_pointer_cast;
44 using std::make_shared;
45 using std::shared_ptr;
47 using boost::optional;
48 #if BOOST_VERSION >= 106100
49 using namespace boost::placeholders;
51 using namespace dcpomatic;
54 auto constexpr reel_marker_y_pos = 48;
55 auto constexpr content_y_pos = 112;
56 auto constexpr content_type_height = 12;
59 ID_add_reel_boundary = DCPOMATIC_DCP_TIMELINE_MENU
66 ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable)
67 : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2)))
68 , _timecode(new Timecode<DCPTime>(parent, true))
70 , _view(timeline, reel_marker_y_pos)
73 sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
74 sizer->Add(_timecode, wxGBPosition(index, 1));
76 _timecode->set_maximum(maximum.split(fps));
77 _timecode->set_editable(editable);
78 _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this));
92 ReelBoundary(ReelBoundary const&) = delete;
93 ReelBoundary& operator=(ReelBoundary const&) = delete;
95 ReelBoundary(ReelBoundary&& other) = delete;
96 ReelBoundary& operator=(ReelBoundary&& other) = delete;
98 void set_time(DCPTime time)
101 _timecode->set(time, _fps);
103 _view.set_time(time);
106 dcpomatic::DCPTime time() const {
114 DCPTimelineReelMarkerView& view() {
118 DCPTimelineReelMarkerView const& view() const {
122 boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed;
125 void timecode_changed() {
126 set_time(_timecode->get(_fps));
127 Changed(_index, time());
130 wxStaticText* _label = nullptr;
131 Timecode<dcpomatic::DCPTime>* _timecode = nullptr;
133 DCPTimelineReelMarkerView _view;
138 DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film)
141 , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
142 , _reel_settings(new wxPanel(this, wxID_ANY))
143 , _reel_detail(new wxPanel(this, wxID_ANY))
144 , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP))
147 _canvas->SetDoubleBuffered(true);
149 _reel_detail->SetSizer(_reel_detail_sizer);
151 auto sizer = new wxBoxSizer(wxVERTICAL);
152 sizer->Add(_reel_settings, 0);
153 sizer->Add(_canvas, 0, wxEXPAND);
154 sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
157 SetMinSize(wxSize(640, 480));
158 _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4});
160 _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this));
161 _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this));
162 _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1));
163 _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1));
164 _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1));
165 _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1));
167 _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2));
170 _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel"));
171 _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this));
173 setup_reel_settings();
174 setup_reel_boundaries();
177 setup_pixels_per_second();
183 DCPTimeline::add_reel_boundary()
185 auto boundaries = film()->custom_reel_boundaries();
186 boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1)));
187 film()->set_custom_reel_boundaries(boundaries);
192 DCPTimeline::film_changed(ChangeType type, FilmProperty property)
194 if (type != ChangeType::DONE) {
199 case FilmProperty::REEL_TYPE:
200 case FilmProperty::REEL_LENGTH:
201 case FilmProperty::CUSTOM_REEL_BOUNDARIES:
203 setup_reel_boundaries();
205 case FilmProperty::CONTENT:
206 case FilmProperty::CONTENT_ORDER:
207 setup_pixels_per_second();
217 DCPTimeline::setup_sensitivity()
219 _snap->Enable(editable());
220 _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH);
225 DCPTimeline::setup_reel_settings()
227 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
228 _reel_settings->SetSizer(sizer);
231 add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0));
232 _reel_type = new Choice(_reel_settings);
233 _reel_type->add_entry(_("Single reel"));
234 _reel_type->add_entry(_("Split by video content"));
235 _reel_type->add_entry(_("Split by maximum reel size"));
236 _reel_type->add_entry(_("Custom"));
237 sizer->Add(_reel_type, wxGBPosition(r, 1));
240 add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
241 _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
242 _maximum_reel_size->SetRange(1, 1000);
244 auto s = new wxBoxSizer(wxHORIZONTAL);
245 s->Add(_maximum_reel_size, 0);
246 add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
247 sizer->Add(s, wxGBPosition(r, 1));
251 _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
252 sizer->Add(_snap, wxGBPosition(r, 1));
255 _reel_type->set(static_cast<int>(film()->reel_type()));
256 _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
258 _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
259 _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
264 DCPTimeline::reel_mode_changed()
266 film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
271 DCPTimeline::maximum_reel_size_changed()
273 film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
278 DCPTimeline::set_reel_boundary(int index, DCPTime time)
280 auto boundaries = film()->custom_reel_boundaries();
281 DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
282 boundaries[index] = time.round(film()->video_frame_rate());
283 film()->set_custom_reel_boundaries(boundaries);
288 DCPTimeline::setup_reel_boundaries()
290 auto const reels = film()->reels();
292 _reel_boundaries.clear();
296 size_t const boundaries = reels.size() - 1;
297 auto const maximum = film()->length();
298 for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
299 auto boundary = std::make_shared<ReelBoundary>(
300 _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
303 boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
304 _reel_boundaries.push_back(boundary);
307 _reel_boundaries.resize(boundaries);
309 auto const active = editable();
310 for (size_t i = 0; i < boundaries; ++i) {
311 _reel_boundaries[i]->set_time(reels[i].to);
312 _reel_boundaries[i]->view().set_active(active);
315 _reel_detail_sizer->Layout();
323 wxPaintDC dc(_canvas);
326 if (film()->content().empty()) {
330 _canvas->DoPrepareDC(dc);
332 auto gc = wxGraphicsContext::Create(dc);
337 dcp::ScopeGuard sg = [gc]() { delete gc; };
339 gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
347 DCPTimeline::paint_reels(wxGraphicsContext* gc)
349 constexpr int x_offset = 2;
351 for (auto const& boundary: _reel_boundaries) {
352 boundary->view().paint(gc);
355 gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
356 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
358 auto const pps = pixels_per_second().get_value_or(1);
360 auto start = gc->CreatePath();
361 start.MoveToPoint(x_offset, reel_marker_y_pos);
362 start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
363 gc->StrokePath(start);
365 auto const length = film()->length().seconds() * pps;
366 auto end = gc->CreatePath();
367 end.MoveToPoint(x_offset + length, reel_marker_y_pos);
368 end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
371 auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
373 auto paint_reel = [gc](double from, double to, int index) {
374 auto path = gc->CreatePath();
375 path.MoveToPoint(from, y);
376 path.AddLineToPoint(to, y);
377 gc->StrokePath(path);
379 auto str = wxString::Format(wxT("#%d"), index + 1);
382 wxDouble str_descent;
383 wxDouble str_leading;
384 gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
386 if (str_width < (to - from)) {
387 gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
391 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
394 for (auto const& boundary: _reel_boundaries) {
395 paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
396 last = boundary->time();
399 paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
404 DCPTimeline::paint_content(wxGraphicsContext* gc)
406 auto const pps = pixels_per_second().get_value_or(1);
407 auto const film = this->film();
409 auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
410 auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
412 auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
413 auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
414 auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
415 auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
418 [gc, film, pps, solid_pen, dotted_pen]
419 (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
421 auto const y = content_y_pos + offset * content_type_height + 1;
422 gc->SetPen(solid_pen);
425 content->position().seconds() * pps,
427 content->length_after_trim(film).seconds() * pps,
428 content_type_height - 2
431 gc->SetPen(dotted_pen);
432 for (auto split: content->reel_split_points(film)) {
433 if (split != content->position()) {
434 auto path = gc->CreatePath();
435 path.MoveToPoint(split.seconds() * pps, y);
436 path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
437 gc->StrokePath(path);
443 for (auto content: film->content()) {
444 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
445 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
446 for (auto text: content->text) {
447 maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
449 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
455 DCPTimeline::setup_pixels_per_second()
457 set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
461 shared_ptr<ReelBoundary>
462 DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
464 Position<int> const position(ev.GetX(), ev.GetY());
465 auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
466 return boundary->view().bbox().contains(position);
469 if (iter == _reel_boundaries.end()) {
478 DCPTimeline::left_down(wxMouseEvent& ev)
484 if (auto boundary = event_to_reel_boundary(ev)) {
485 auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
486 _drag = DCPTimeline::Drag(
490 static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
501 DCPTimeline::right_down(wxMouseEvent& ev)
503 _right_down_position = ev.GetPosition();
504 _canvas->PopupMenu(_menu, _right_down_position);
509 DCPTimeline::left_up(wxMouseEvent&)
515 set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
521 DCPTimeline::mouse_moved(wxMouseEvent& ev)
527 auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
528 time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
529 time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
530 _drag->set_time(time);
531 _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
536 DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
538 _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
543 DCPTimeline::film() const
545 auto film = _film.lock();
546 DCPOMATIC_ASSERT(film);
552 DCPTimeline::editable() const
554 return film()->reel_type() == ReelType::CUSTOM;
558 DCPTimeline::Drag::Drag(
559 shared_ptr<ReelBoundary> reel_boundary_,
560 vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
561 shared_ptr<const Film> film,
564 DCPTime snap_distance
566 : reel_boundary(reel_boundary_)
568 , _snap_distance(snap_distance)
570 auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
571 auto index = std::distance(reel_boundaries.begin(), iter);
574 previous = reel_boundaries[index - 1];
576 if (index < static_cast<int>(reel_boundaries.size() - 1)) {
577 next = reel_boundaries[index + 1];
581 for (auto content: film->content()) {
582 for (auto split: content->reel_split_points(film)) {
583 _snaps.push_back(split);
591 DCPTimeline::Drag::set_time(DCPTime time)
593 optional<DCPTime> nearest_distance;
594 optional<DCPTime> nearest_time;
595 for (auto snap: _snaps) {
596 auto const distance = time - snap;
597 if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
598 nearest_distance = distance.abs();
603 if (nearest_distance && *nearest_distance < _snap_distance) {
604 reel_boundary->set_time(*nearest_time);
606 reel_boundary->set_time(time);
612 DCPTimeline::Drag::time() const
614 return reel_boundary->time();