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"
30 #include "lib/atmos_content.h"
31 #include "lib/audio_content.h"
32 #include "lib/constants.h"
34 #include "lib/text_content.h"
35 #include "lib/video_content.h"
36 #include <dcp/scope_guard.h>
37 LIBDCP_DISABLE_WARNINGS
38 #include <wx/graphics.h>
39 LIBDCP_ENABLE_WARNINGS
42 using std::dynamic_pointer_cast;
43 using std::make_shared;
44 using std::shared_ptr;
46 using boost::optional;
47 #if BOOST_VERSION >= 106100
48 using namespace boost::placeholders;
50 using namespace dcpomatic;
53 auto constexpr reel_marker_y_pos = 48;
54 auto constexpr content_y_pos = 112;
55 auto constexpr content_type_height = 12;
65 ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable)
66 : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2)))
67 , _timecode(new Timecode<DCPTime>(parent, true))
69 , _view(timeline, reel_marker_y_pos)
72 sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
73 sizer->Add(_timecode, wxGBPosition(index, 1));
75 _timecode->set_maximum(maximum.split(fps));
76 _timecode->set_editable(editable);
77 _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this));
91 ReelBoundary(ReelBoundary const&) = delete;
92 ReelBoundary& operator=(ReelBoundary const&) = delete;
94 ReelBoundary(ReelBoundary&& other) = delete;
95 ReelBoundary& operator=(ReelBoundary&& other) = delete;
97 void set_time(DCPTime time)
100 _timecode->set(time, _fps);
102 _view.set_time(time);
105 dcpomatic::DCPTime time() const {
113 DCPTimelineReelMarkerView& view() {
117 DCPTimelineReelMarkerView const& view() const {
121 boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed;
124 void timecode_changed() {
125 set_time(_timecode->get(_fps));
126 Changed(_index, time());
129 wxStaticText* _label = nullptr;
130 Timecode<dcpomatic::DCPTime>* _timecode = nullptr;
132 DCPTimelineReelMarkerView _view;
137 DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film)
140 , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
141 , _reel_settings(new wxPanel(this, wxID_ANY))
142 , _reel_detail(new wxPanel(this, wxID_ANY))
143 , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP))
146 _canvas->SetDoubleBuffered(true);
148 _reel_detail->SetSizer(_reel_detail_sizer);
150 auto sizer = new wxBoxSizer(wxVERTICAL);
151 sizer->Add(_reel_settings, 0);
152 sizer->Add(_canvas, 0, wxEXPAND);
153 sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
156 SetMinSize(wxSize(640, 480));
157 _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4});
159 _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this));
160 _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this));
161 _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1));
162 _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1));
163 _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1));
164 _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1));
166 _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2));
169 _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel"));
170 _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this));
172 setup_reel_settings();
173 setup_reel_boundaries();
176 setup_pixels_per_second();
182 DCPTimeline::add_reel_boundary()
184 auto boundaries = film()->custom_reel_boundaries();
185 boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1)));
186 film()->set_custom_reel_boundaries(boundaries);
191 DCPTimeline::film_changed(ChangeType type, FilmProperty property)
193 if (type != ChangeType::DONE) {
198 case FilmProperty::REEL_TYPE:
199 case FilmProperty::REEL_LENGTH:
200 case FilmProperty::CUSTOM_REEL_BOUNDARIES:
202 setup_reel_boundaries();
204 case FilmProperty::CONTENT:
205 case FilmProperty::CONTENT_ORDER:
206 setup_pixels_per_second();
216 DCPTimeline::setup_sensitivity()
218 _snap->Enable(editable());
219 _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH);
224 DCPTimeline::setup_reel_settings()
226 auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
227 _reel_settings->SetSizer(sizer);
230 add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0));
231 _reel_type = new Choice(_reel_settings);
232 _reel_type->add(_("Single reel"));
233 _reel_type->add(_("Split by video content"));
234 _reel_type->add(_("Split by maximum reel size"));
235 _reel_type->add(_("Custom"));
236 sizer->Add(_reel_type, wxGBPosition(r, 1));
239 add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
240 _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
241 _maximum_reel_size->SetRange(1, 1000);
243 auto s = new wxBoxSizer(wxHORIZONTAL);
244 s->Add(_maximum_reel_size, 0);
245 add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
246 sizer->Add(s, wxGBPosition(r, 1));
250 _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
251 sizer->Add(_snap, wxGBPosition(r, 1));
254 _reel_type->set(static_cast<int>(film()->reel_type()));
255 _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
257 _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
258 _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
263 DCPTimeline::reel_mode_changed()
265 film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
270 DCPTimeline::maximum_reel_size_changed()
272 film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
277 DCPTimeline::set_reel_boundary(int index, DCPTime time)
279 auto boundaries = film()->custom_reel_boundaries();
280 DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
281 boundaries[index] = time.round(film()->video_frame_rate());
282 film()->set_custom_reel_boundaries(boundaries);
287 DCPTimeline::setup_reel_boundaries()
289 auto const reels = film()->reels();
291 _reel_boundaries.clear();
295 size_t const boundaries = reels.size() - 1;
296 auto const maximum = film()->length();
297 for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
298 auto boundary = std::make_shared<ReelBoundary>(
299 _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
302 boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
303 _reel_boundaries.push_back(boundary);
306 _reel_boundaries.resize(boundaries);
308 auto const active = editable();
309 for (size_t i = 0; i < boundaries; ++i) {
310 _reel_boundaries[i]->set_time(reels[i].to);
311 _reel_boundaries[i]->view().set_active(active);
314 _reel_detail_sizer->Layout();
322 wxPaintDC dc(_canvas);
325 if (film()->content().empty()) {
329 _canvas->DoPrepareDC(dc);
331 auto gc = wxGraphicsContext::Create(dc);
336 dcp::ScopeGuard sg = [gc]() { delete gc; };
338 gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
346 DCPTimeline::paint_reels(wxGraphicsContext* gc)
348 constexpr int x_offset = 2;
350 for (auto const& boundary: _reel_boundaries) {
351 boundary->view().paint(gc);
354 gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
355 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
357 auto const pps = pixels_per_second().get_value_or(1);
359 auto start = gc->CreatePath();
360 start.MoveToPoint(x_offset, reel_marker_y_pos);
361 start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
362 gc->StrokePath(start);
364 auto const length = film()->length().seconds() * pps;
365 auto end = gc->CreatePath();
366 end.MoveToPoint(x_offset + length, reel_marker_y_pos);
367 end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
370 auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
372 auto paint_reel = [gc, y](double from, double to, int index) {
373 auto path = gc->CreatePath();
374 path.MoveToPoint(from, y);
375 path.AddLineToPoint(to, y);
376 gc->StrokePath(path);
378 auto str = wxString::Format(wxT("#%d"), index + 1);
381 wxDouble str_descent;
382 wxDouble str_leading;
383 gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
385 if (str_width < (to - from)) {
386 gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
390 gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
393 for (auto const& boundary: _reel_boundaries) {
394 paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
395 last = boundary->time();
398 paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
403 DCPTimeline::paint_content(wxGraphicsContext* gc)
405 auto const pps = pixels_per_second().get_value_or(1);
406 auto const film = this->film();
408 auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
409 auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
411 auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
412 auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
413 auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
414 auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
417 [gc, film, pps, solid_pen, dotted_pen]
418 (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
420 auto const y = content_y_pos + offset * content_type_height + 1;
421 gc->SetPen(solid_pen);
424 content->position().seconds() * pps,
426 content->length_after_trim(film).seconds() * pps,
427 content_type_height - 2
430 gc->SetPen(dotted_pen);
431 for (auto split: content->reel_split_points(film)) {
432 if (split != content->position()) {
433 auto path = gc->CreatePath();
434 path.MoveToPoint(split.seconds() * pps, y);
435 path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
436 gc->StrokePath(path);
442 for (auto content: film->content()) {
443 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
444 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
445 for (auto text: content->text) {
446 maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
448 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
454 DCPTimeline::setup_pixels_per_second()
456 set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
460 shared_ptr<ReelBoundary>
461 DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
463 Position<int> const position(ev.GetX(), ev.GetY());
464 auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
465 return boundary->view().bbox().contains(position);
468 if (iter == _reel_boundaries.end()) {
477 DCPTimeline::left_down(wxMouseEvent& ev)
483 if (auto boundary = event_to_reel_boundary(ev)) {
484 auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
485 _drag = DCPTimeline::Drag(
489 static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
500 DCPTimeline::right_down(wxMouseEvent& ev)
502 _right_down_position = ev.GetPosition();
503 _canvas->PopupMenu(_menu, _right_down_position);
508 DCPTimeline::left_up(wxMouseEvent&)
514 set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
520 DCPTimeline::mouse_moved(wxMouseEvent& ev)
526 auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
527 time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
528 time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
529 _drag->set_time(time);
530 _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
535 DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
537 _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
542 DCPTimeline::film() const
544 auto film = _film.lock();
545 DCPOMATIC_ASSERT(film);
551 DCPTimeline::editable() const
553 return film()->reel_type() == ReelType::CUSTOM;
557 DCPTimeline::Drag::Drag(
558 shared_ptr<ReelBoundary> reel_boundary_,
559 vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
560 shared_ptr<const Film> film,
563 DCPTime snap_distance
565 : reel_boundary(reel_boundary_)
567 , _snap_distance(snap_distance)
569 auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
570 auto index = std::distance(reel_boundaries.begin(), iter);
573 previous = reel_boundaries[index - 1];
575 if (index < static_cast<int>(reel_boundaries.size() - 1)) {
576 next = reel_boundaries[index + 1];
580 for (auto content: film->content()) {
581 for (auto split: content->reel_split_points(film)) {
582 _snaps.push_back(split);
590 DCPTimeline::Drag::set_time(DCPTime time)
592 optional<DCPTime> nearest_distance;
593 optional<DCPTime> nearest_time;
594 for (auto snap: _snaps) {
595 auto const distance = time - snap;
596 if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
597 nearest_distance = distance.abs();
602 if (nearest_distance && *nearest_distance < _snap_distance) {
603 reel_boundary->set_time(*nearest_time);
605 reel_boundary->set_time(time);
611 DCPTimeline::Drag::time() const
613 return reel_boundary->time();