Only allow adding reels in custom reel mode (#2823).
[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         _add_reel_boundary->Enable(film()->reel_type() == ReelType::CUSTOM);
222 }
223
224
225 void
226 DCPTimeline::setup_reel_settings()
227 {
228         auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
229         _reel_settings->SetSizer(sizer);
230
231         int r = 0;
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));
239         ++r;
240
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);
244         {
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));
249         }
250         ++r;
251
252         _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
253         sizer->Add(_snap, wxGBPosition(r, 1));
254         ++r;
255
256         _reel_type->set(static_cast<int>(film()->reel_type()));
257         _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
258
259         _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
260         _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
261 }
262
263
264 void
265 DCPTimeline::reel_mode_changed()
266 {
267         film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
268 }
269
270
271 void
272 DCPTimeline::maximum_reel_size_changed()
273 {
274         film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
275 }
276
277
278 void
279 DCPTimeline::set_reel_boundary(int index, DCPTime time)
280 {
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);
285 }
286
287
288 void
289 DCPTimeline::setup_reel_boundaries()
290 {
291         auto const reels = film()->reels();
292         if (reels.empty()) {
293                 _reel_boundaries.clear();
294                 return;
295         }
296
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()
302                                 );
303
304                 boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
305                 _reel_boundaries.push_back(boundary);
306         }
307
308         _reel_boundaries.resize(boundaries);
309
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);
314         }
315
316         _reel_detail_sizer->Layout();
317         _canvas->Refresh();
318 }
319
320
321 void
322 DCPTimeline::paint()
323 {
324         wxPaintDC dc(_canvas);
325         dc.Clear();
326
327         if (film()->content().empty()) {
328                 return;
329         }
330
331         _canvas->DoPrepareDC(dc);
332
333         auto gc = wxGraphicsContext::Create(dc);
334         if (!gc) {
335                 return;
336         }
337
338         dcp::ScopeGuard sg = [gc]() { delete gc; };
339
340         gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
341
342         paint_reels(gc);
343         paint_content(gc);
344 }
345
346
347 void
348 DCPTimeline::paint_reels(wxGraphicsContext* gc)
349 {
350         constexpr int x_offset = 2;
351
352         for (auto const& boundary: _reel_boundaries) {
353                 boundary->view().paint(gc);
354         }
355
356         gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
357         gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
358
359         auto const pps = pixels_per_second().get_value_or(1);
360
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);
365
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);
370         gc->StrokePath(end);
371
372         auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
373
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);
379
380                 auto str = wxString::Format(wxT("#%d"), index + 1);
381                 wxDouble str_width;
382                 wxDouble str_height;
383                 wxDouble str_descent;
384                 wxDouble str_leading;
385                 gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
386
387                 if (str_width < (to - from)) {
388                         gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
389                 }
390         };
391
392         gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
393         int index = 0;
394         DCPTime last;
395         for (auto const& boundary: _reel_boundaries) {
396                 paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
397                 last = boundary->time();
398         }
399
400         paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
401 }
402
403
404 void
405 DCPTimeline::paint_content(wxGraphicsContext* gc)
406 {
407         auto const pps = pixels_per_second().get_value_or(1);
408         auto const film = this->film();
409
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);
412
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);
417
418         auto maybe_draw =
419                 [gc, film, pps, solid_pen, dotted_pen]
420                 (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
421                 if (part) {
422                         auto const y = content_y_pos + offset * content_type_height + 1;
423                         gc->SetPen(solid_pen);
424                         gc->SetBrush(brush);
425                         gc->DrawRectangle(
426                                 content->position().seconds() * pps,
427                                 y,
428                                 content->length_after_trim(film).seconds() * pps,
429                                 content_type_height - 2
430                                 );
431
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);
439                                 }
440                         }
441                 }
442         };
443
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);
449                 }
450                 maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
451         }
452 }
453
454
455 void
456 DCPTimeline::setup_pixels_per_second()
457 {
458         set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
459 }
460
461
462 shared_ptr<ReelBoundary>
463 DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
464 {
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);
468         });
469
470         if (iter == _reel_boundaries.end()) {
471                 return {};
472         }
473
474         return *iter;
475 }
476
477
478 void
479 DCPTimeline::left_down(wxMouseEvent& ev)
480 {
481         if (!editable()) {
482                 return;
483         }
484
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(
488                         boundary,
489                         _reel_boundaries,
490                         film(),
491                         static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
492                         _snap->get(),
493                         snap_distance
494                         );
495         } else {
496                 _drag = boost::none;
497         }
498 }
499
500
501 void
502 DCPTimeline::right_down(wxMouseEvent& ev)
503 {
504         _right_down_position = ev.GetPosition();
505         _canvas->PopupMenu(_menu, _right_down_position);
506 }
507
508
509 void
510 DCPTimeline::left_up(wxMouseEvent&)
511 {
512         if (!_drag) {
513                 return;
514         }
515
516         set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
517         _drag = boost::none;
518 }
519
520
521 void
522 DCPTimeline::mouse_moved(wxMouseEvent& ev)
523 {
524         if (!_drag) {
525                 return;
526         }
527
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);
533 }
534
535
536 void
537 DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
538 {
539         _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
540 }
541
542
543 shared_ptr<Film>
544 DCPTimeline::film() const
545 {
546         auto film = _film.lock();
547         DCPOMATIC_ASSERT(film);
548         return film;
549 }
550
551
552 bool
553 DCPTimeline::editable() const
554 {
555         return film()->reel_type() == ReelType::CUSTOM;
556 }
557
558
559 DCPTimeline::Drag::Drag(
560         shared_ptr<ReelBoundary> reel_boundary_,
561         vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
562         shared_ptr<const Film> film,
563         int offset_,
564         bool snap,
565         DCPTime snap_distance
566         )
567         : reel_boundary(reel_boundary_)
568         , offset(offset_)
569         , _snap_distance(snap_distance)
570 {
571         auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
572         auto index = std::distance(reel_boundaries.begin(), iter);
573
574         if (index > 0) {
575                 previous = reel_boundaries[index - 1];
576         }
577         if (index < static_cast<int>(reel_boundaries.size() - 1)) {
578                 next = reel_boundaries[index + 1];
579         }
580
581         if (snap) {
582                 for (auto content: film->content()) {
583                         for (auto split: content->reel_split_points(film)) {
584                                 _snaps.push_back(split);
585                         }
586                 }
587         }
588 }
589
590
591 void
592 DCPTimeline::Drag::set_time(DCPTime time)
593 {
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();
600                         nearest_time = snap;
601                 }
602         }
603
604         if (nearest_distance && *nearest_distance < _snap_distance) {
605                 reel_boundary->set_time(*nearest_time);
606         } else {
607                 reel_boundary->set_time(time);
608         }
609 }
610
611
612 DCPTime
613 DCPTimeline::Drag::time() const
614 {
615         return reel_boundary->time();
616 }
617