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);
221 _add_reel_boundary->Enable(film()->reel_type() == ReelType::CUSTOM);
226 DCPTimeline::setup_reel_settings()
228 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
229 _reel_settings->SetSizer(sizer);
232 add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0));
233 _reel_type = new Choice(_reel_settings);
234 _reel_type->add_entry(_("Single reel"));
235 _reel_type->add_entry(_("Split by video content"));
236 _reel_type->add_entry(_("Split by maximum reel size"));
237 _reel_type->add_entry(_("Custom"));
238 sizer->Add(_reel_type, wxGBPosition(r, 1));
241 add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
242 _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
243 _maximum_reel_size->SetRange(1, 1000);
245 auto s = new wxBoxSizer(wxHORIZONTAL);
246 s->Add(_maximum_reel_size, 0);
247 add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
248 sizer->Add(s, wxGBPosition(r, 1));
252 _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
253 sizer->Add(_snap, wxGBPosition(r, 1));
256 _reel_type->set(static_cast<int>(film()->reel_type()));
257 _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
259 _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
260 _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
265 DCPTimeline::reel_mode_changed()
267 film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
272 DCPTimeline::maximum_reel_size_changed()
274 film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
279 DCPTimeline::set_reel_boundary(int index, DCPTime time)
281 auto boundaries = film()->custom_reel_boundaries();
282 DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
283 boundaries[index] = time.round(film()->video_frame_rate());
284 film()->set_custom_reel_boundaries(boundaries);
289 DCPTimeline::setup_reel_boundaries()
291 auto const reels = film()->reels();
293 _reel_boundaries.clear();
297 size_t const boundaries = reels.size() - 1;
298 auto const maximum = film()->length();
299 for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
300 auto boundary = std::make_shared<ReelBoundary>(
301 _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
304 boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
305 _reel_boundaries.push_back(boundary);
308 _reel_boundaries.resize(boundaries);
310 auto const active = editable();
311 for (size_t i = 0; i < boundaries; ++i) {
312 _reel_boundaries[i]->set_time(reels[i].to);
313 _reel_boundaries[i]->view().set_active(active);
316 _reel_detail_sizer->Layout();
324 wxPaintDC dc(_canvas);
327 if (film()->content().empty()) {
331 _canvas->DoPrepareDC(dc);
333 auto gc = wxGraphicsContext::Create(dc);
338 dcp::ScopeGuard sg = [gc]() { delete gc; };
340 gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
348 DCPTimeline::paint_reels(wxGraphicsContext* gc)
350 constexpr int x_offset = 2;
352 for (auto const& boundary: _reel_boundaries) {
353 boundary->view().paint(gc);
356 gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
357 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
359 auto const pps = pixels_per_second().get_value_or(1);
361 auto start = gc->CreatePath();
362 start.MoveToPoint(x_offset, reel_marker_y_pos);
363 start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
364 gc->StrokePath(start);
366 auto const length = film()->length().seconds() * pps;
367 auto end = gc->CreatePath();
368 end.MoveToPoint(x_offset + length, reel_marker_y_pos);
369 end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
372 auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
374 auto paint_reel = [gc](double from, double to, int index) {
375 auto path = gc->CreatePath();
376 path.MoveToPoint(from, y);
377 path.AddLineToPoint(to, y);
378 gc->StrokePath(path);
380 auto str = wxString::Format(wxT("#%d"), index + 1);
383 wxDouble str_descent;
384 wxDouble str_leading;
385 gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
387 if (str_width < (to - from)) {
388 gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
392 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
395 for (auto const& boundary: _reel_boundaries) {
396 paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
397 last = boundary->time();
400 paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
405 DCPTimeline::paint_content(wxGraphicsContext* gc)
407 auto const pps = pixels_per_second().get_value_or(1);
408 auto const film = this->film();
410 auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
411 auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
413 auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
414 auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
415 auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
416 auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
419 [gc, film, pps, solid_pen, dotted_pen]
420 (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
422 auto const y = content_y_pos + offset * content_type_height + 1;
423 gc->SetPen(solid_pen);
426 content->position().seconds() * pps,
428 content->length_after_trim(film).seconds() * pps,
429 content_type_height - 2
432 gc->SetPen(dotted_pen);
433 for (auto split: content->reel_split_points(film)) {
434 if (split != content->position()) {
435 auto path = gc->CreatePath();
436 path.MoveToPoint(split.seconds() * pps, y);
437 path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
438 gc->StrokePath(path);
444 for (auto content: film->content()) {
445 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
446 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
447 for (auto text: content->text) {
448 maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
450 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
456 DCPTimeline::setup_pixels_per_second()
458 set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
462 shared_ptr<ReelBoundary>
463 DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
465 Position<int> const position(ev.GetX(), ev.GetY());
466 auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
467 return boundary->view().bbox().contains(position);
470 if (iter == _reel_boundaries.end()) {
479 DCPTimeline::left_down(wxMouseEvent& ev)
485 if (auto boundary = event_to_reel_boundary(ev)) {
486 auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
487 _drag = DCPTimeline::Drag(
491 static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
502 DCPTimeline::right_down(wxMouseEvent& ev)
504 _right_down_position = ev.GetPosition();
505 _canvas->PopupMenu(_menu, _right_down_position);
510 DCPTimeline::left_up(wxMouseEvent&)
516 set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
522 DCPTimeline::mouse_moved(wxMouseEvent& ev)
528 auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
529 time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
530 time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
531 _drag->set_time(time);
532 _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
537 DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
539 _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
544 DCPTimeline::film() const
546 auto film = _film.lock();
547 DCPOMATIC_ASSERT(film);
553 DCPTimeline::editable() const
555 return film()->reel_type() == ReelType::CUSTOM;
559 DCPTimeline::Drag::Drag(
560 shared_ptr<ReelBoundary> reel_boundary_,
561 vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
562 shared_ptr<const Film> film,
565 DCPTime snap_distance
567 : reel_boundary(reel_boundary_)
569 , _snap_distance(snap_distance)
571 auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
572 auto index = std::distance(reel_boundaries.begin(), iter);
575 previous = reel_boundaries[index - 1];
577 if (index < static_cast<int>(reel_boundaries.size() - 1)) {
578 next = reel_boundaries[index + 1];
582 for (auto content: film->content()) {
583 for (auto split: content->reel_split_points(film)) {
584 _snaps.push_back(split);
592 DCPTimeline::Drag::set_time(DCPTime time)
594 optional<DCPTime> nearest_distance;
595 optional<DCPTime> nearest_time;
596 for (auto snap: _snaps) {
597 auto const distance = time - snap;
598 if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
599 nearest_distance = distance.abs();
604 if (nearest_distance && *nearest_distance < _snap_distance) {
605 reel_boundary->set_time(*nearest_time);
607 reel_boundary->set_time(time);
613 DCPTimeline::Drag::time() const
615 return reel_boundary->time();