Splash behaviour does not need to be branded after all.
[dcpomatic.git] / src / wx / dcp_timeline.cc
1 /*
2     Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "check_box.h"
23 #include "colours.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"
28 #include "id.h"
29 #include "timecode.h"
30 #include "wx_util.h"
31 #include "lib/atmos_content.h"
32 #include "lib/audio_content.h"
33 #include "lib/constants.h"
34 #include "lib/film.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
41
42
43 using std::dynamic_pointer_cast;
44 using std::make_shared;
45 using std::shared_ptr;
46 using std::vector;
47 using boost::optional;
48 #if BOOST_VERSION >= 106100
49 using namespace boost::placeholders;
50 #endif
51 using namespace dcpomatic;
52
53
54 auto constexpr reel_marker_y_pos = 48;
55 auto constexpr content_y_pos = 112;
56 auto constexpr content_type_height = 12;
57
58 enum {
59         ID_add_reel_boundary = DCPOMATIC_DCP_TIMELINE_MENU
60 };
61
62
63 class ReelBoundary
64 {
65 public:
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))
69                 , _index(index)
70                 , _view(timeline, reel_marker_y_pos)
71                 , _fps(fps)
72         {
73                 sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
74                 sizer->Add(_timecode, wxGBPosition(index, 1));
75
76                 _timecode->set_maximum(maximum.split(fps));
77                 _timecode->set_editable(editable);
78                 _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this));
79         }
80
81         ~ReelBoundary()
82         {
83                 if (_label) {
84                         _label->Destroy();
85                 }
86
87                 if (_timecode) {
88                         _timecode->Destroy();
89                 }
90         }
91
92         ReelBoundary(ReelBoundary const&) = delete;
93         ReelBoundary& operator=(ReelBoundary const&) = delete;
94
95         ReelBoundary(ReelBoundary&& other) = delete;
96         ReelBoundary& operator=(ReelBoundary&& other) = delete;
97
98         void set_time(DCPTime time)
99         {
100                 if (_timecode) {
101                         _timecode->set(time, _fps);
102                 }
103                 _view.set_time(time);
104         }
105
106         dcpomatic::DCPTime time() const {
107                 return _view.time();
108         }
109
110         int index() const {
111                 return _index;
112         }
113
114         DCPTimelineReelMarkerView& view() {
115                 return _view;
116         }
117
118         DCPTimelineReelMarkerView const& view() const {
119                 return _view;
120         }
121
122         boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed;
123
124 private:
125         void timecode_changed() {
126                 set_time(_timecode->get(_fps));
127                 Changed(_index, time());
128         }
129
130         wxStaticText* _label = nullptr;
131         Timecode<dcpomatic::DCPTime>* _timecode = nullptr;
132         int _index = 0;
133         DCPTimelineReelMarkerView _view;
134         int _fps;
135 };
136
137
138 DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film)
139         : Timeline(parent)
140         , _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))
145 {
146 #ifndef __WXOSX__
147         _canvas->SetDoubleBuffered(true);
148 #endif
149         _reel_detail->SetSizer(_reel_detail_sizer);
150
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);
155         SetSizer(sizer);
156
157         SetMinSize(wxSize(640, 480));
158         _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4});
159
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));
166
167         _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2));
168
169         _menu = new wxMenu;
170         _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel"));
171         _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this));
172
173         setup_reel_settings();
174         setup_reel_boundaries();
175
176         sizer->Layout();
177         setup_pixels_per_second();
178         setup_sensitivity();
179 }
180
181
182 void
183 DCPTimeline::add_reel_boundary()
184 {
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);
188 }
189
190
191 void
192 DCPTimeline::film_changed(ChangeType type, FilmProperty property)
193 {
194         if (type != ChangeType::DONE) {
195                 return;
196         }
197
198         switch (property) {
199         case FilmProperty::REEL_TYPE:
200         case FilmProperty::REEL_LENGTH:
201         case FilmProperty::CUSTOM_REEL_BOUNDARIES:
202                 setup_sensitivity();
203                 setup_reel_boundaries();
204                 break;
205         case FilmProperty::CONTENT:
206         case FilmProperty::CONTENT_ORDER:
207                 setup_pixels_per_second();
208                 Refresh();
209                 break;
210         default:
211                 break;
212         }
213 }
214
215
216 void
217 DCPTimeline::setup_sensitivity()
218 {
219         _snap->Enable(editable());
220         _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH);
221 }
222
223
224 void
225 DCPTimeline::setup_reel_settings()
226 {
227         auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
228         _reel_settings->SetSizer(sizer);
229
230         int r = 0;
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));
238         ++r;
239
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);
243         {
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));
248         }
249         ++r;
250
251         _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
252         sizer->Add(_snap, wxGBPosition(r, 1));
253         ++r;
254
255         _reel_type->set(static_cast<int>(film()->reel_type()));
256         _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
257
258         _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
259         _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
260 }
261
262
263 void
264 DCPTimeline::reel_mode_changed()
265 {
266         film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
267 }
268
269
270 void
271 DCPTimeline::maximum_reel_size_changed()
272 {
273         film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
274 }
275
276
277 void
278 DCPTimeline::set_reel_boundary(int index, DCPTime time)
279 {
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);
284 }
285
286
287 void
288 DCPTimeline::setup_reel_boundaries()
289 {
290         auto const reels = film()->reels();
291         if (reels.empty()) {
292                 _reel_boundaries.clear();
293                 return;
294         }
295
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()
301                                 );
302
303                 boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
304                 _reel_boundaries.push_back(boundary);
305         }
306
307         _reel_boundaries.resize(boundaries);
308
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);
313         }
314
315         _reel_detail_sizer->Layout();
316         _canvas->Refresh();
317 }
318
319
320 void
321 DCPTimeline::paint()
322 {
323         wxPaintDC dc(_canvas);
324         dc.Clear();
325
326         if (film()->content().empty()) {
327                 return;
328         }
329
330         _canvas->DoPrepareDC(dc);
331
332         auto gc = wxGraphicsContext::Create(dc);
333         if (!gc) {
334                 return;
335         }
336
337         dcp::ScopeGuard sg = [gc]() { delete gc; };
338
339         gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
340
341         paint_reels(gc);
342         paint_content(gc);
343 }
344
345
346 void
347 DCPTimeline::paint_reels(wxGraphicsContext* gc)
348 {
349         constexpr int x_offset = 2;
350
351         for (auto const& boundary: _reel_boundaries) {
352                 boundary->view().paint(gc);
353         }
354
355         gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
356         gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
357
358         auto const pps = pixels_per_second().get_value_or(1);
359
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);
364
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);
369         gc->StrokePath(end);
370
371         auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
372
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);
378
379                 auto str = wxString::Format(wxT("#%d"), index + 1);
380                 wxDouble str_width;
381                 wxDouble str_height;
382                 wxDouble str_descent;
383                 wxDouble str_leading;
384                 gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
385
386                 if (str_width < (to - from)) {
387                         gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
388                 }
389         };
390
391         gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
392         int index = 0;
393         DCPTime last;
394         for (auto const& boundary: _reel_boundaries) {
395                 paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
396                 last = boundary->time();
397         }
398
399         paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
400 }
401
402
403 void
404 DCPTimeline::paint_content(wxGraphicsContext* gc)
405 {
406         auto const pps = pixels_per_second().get_value_or(1);
407         auto const film = this->film();
408
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);
411
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);
416
417         auto maybe_draw =
418                 [gc, film, pps, solid_pen, dotted_pen]
419                 (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
420                 if (part) {
421                         auto const y = content_y_pos + offset * content_type_height + 1;
422                         gc->SetPen(solid_pen);
423                         gc->SetBrush(brush);
424                         gc->DrawRectangle(
425                                 content->position().seconds() * pps,
426                                 y,
427                                 content->length_after_trim(film).seconds() * pps,
428                                 content_type_height - 2
429                                 );
430
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);
438                                 }
439                         }
440                 }
441         };
442
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);
448                 }
449                 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
450         }
451 }
452
453
454 void
455 DCPTimeline::setup_pixels_per_second()
456 {
457         set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
458 }
459
460
461 shared_ptr<ReelBoundary>
462 DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
463 {
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);
467         });
468
469         if (iter == _reel_boundaries.end()) {
470                 return {};
471         }
472
473         return *iter;
474 }
475
476
477 void
478 DCPTimeline::left_down(wxMouseEvent& ev)
479 {
480         if (!editable()) {
481                 return;
482         }
483
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(
487                         boundary,
488                         _reel_boundaries,
489                         film(),
490                         static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
491                         _snap->get(),
492                         snap_distance
493                         );
494         } else {
495                 _drag = boost::none;
496         }
497 }
498
499
500 void
501 DCPTimeline::right_down(wxMouseEvent& ev)
502 {
503         _right_down_position = ev.GetPosition();
504         _canvas->PopupMenu(_menu, _right_down_position);
505 }
506
507
508 void
509 DCPTimeline::left_up(wxMouseEvent&)
510 {
511         if (!_drag) {
512                 return;
513         }
514
515         set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
516         _drag = boost::none;
517 }
518
519
520 void
521 DCPTimeline::mouse_moved(wxMouseEvent& ev)
522 {
523         if (!_drag) {
524                 return;
525         }
526
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);
532 }
533
534
535 void
536 DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
537 {
538         _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
539 }
540
541
542 shared_ptr<Film>
543 DCPTimeline::film() const
544 {
545         auto film = _film.lock();
546         DCPOMATIC_ASSERT(film);
547         return film;
548 }
549
550
551 bool
552 DCPTimeline::editable() const
553 {
554         return film()->reel_type() == ReelType::CUSTOM;
555 }
556
557
558 DCPTimeline::Drag::Drag(
559         shared_ptr<ReelBoundary> reel_boundary_,
560         vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
561         shared_ptr<const Film> film,
562         int offset_,
563         bool snap,
564         DCPTime snap_distance
565         )
566         : reel_boundary(reel_boundary_)
567         , offset(offset_)
568         , _snap_distance(snap_distance)
569 {
570         auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
571         auto index = std::distance(reel_boundaries.begin(), iter);
572
573         if (index > 0) {
574                 previous = reel_boundaries[index - 1];
575         }
576         if (index < static_cast<int>(reel_boundaries.size() - 1)) {
577                 next = reel_boundaries[index + 1];
578         }
579
580         if (snap) {
581                 for (auto content: film->content()) {
582                         for (auto split: content->reel_split_points(film)) {
583                                 _snaps.push_back(split);
584                         }
585                 }
586         }
587 }
588
589
590 void
591 DCPTimeline::Drag::set_time(DCPTime time)
592 {
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();
599                         nearest_time = snap;
600                 }
601         }
602
603         if (nearest_distance && *nearest_distance < _snap_distance) {
604                 reel_boundary->set_time(*nearest_time);
605         } else {
606                 reel_boundary->set_time(time);
607         }
608 }
609
610
611 DCPTime
612 DCPTimeline::Drag::time() const
613 {
614         return reel_boundary->time();
615 }
616