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