Add new interface for setting reel breaks (#2678). 2678-reel-break
authorCarl Hetherington <cth@carlh.net>
Tue, 12 Dec 2023 23:42:22 +0000 (00:42 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 12 Mar 2024 22:41:00 +0000 (23:41 +0100)
src/lib/film.cc
src/wx/dcp_panel.cc
src/wx/dcp_panel.h
src/wx/dcp_timeline.cc [new file with mode: 0644]
src/wx/dcp_timeline.h [new file with mode: 0644]
src/wx/dcp_timeline_dialog.cc [new file with mode: 0644]
src/wx/dcp_timeline_dialog.h [new file with mode: 0644]
src/wx/dcp_timeline_reel_marker_view.cc [new file with mode: 0644]
src/wx/dcp_timeline_reel_marker_view.h [new file with mode: 0644]
src/wx/dcp_timeline_view.h [new file with mode: 0644]
src/wx/wscript

index 835f3efdf5653058ae3366c31506a2a16f643ffd..d2c73c8b5ef5e02269d27c2e11637e601eff7f39 100644 (file)
@@ -1616,6 +1616,23 @@ Film::check_settings_consistency ()
        if (change_made) {
                Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV.  Please review those settings to make sure they are what you want."));
        }
+
+       if (reel_type() == ReelType::CUSTOM) {
+               auto boundaries = custom_reel_boundaries();
+               auto too_late = std::find_if(boundaries.begin(), boundaries.end(), [this](dcpomatic::DCPTime const& time) {
+                       return time >= length();
+               });
+
+               if (too_late != boundaries.end()) {
+                       if (std::distance(too_late, boundaries.end()) > 1) {
+                               Message(_("DCP-o-matic had to remove some of your custom reel boundaries as they no longer lie within the film."));
+                       } else {
+                               Message(_("DCP-o-matic had to remove one of your custom reel boundaries as it no longer lies within the film."));
+                       }
+                       boundaries.erase(too_late, boundaries.end());
+                       set_custom_reel_boundaries(boundaries);
+               }
+       }
 }
 
 void
index 1afc1be3dd9ab251c79ae49f2c478caebee6614b..64b4935cc784459e732a04d4f73d214d033077f2 100644 (file)
@@ -23,6 +23,7 @@
 #include "check_box.h"
 #include "check_box.h"
 #include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
 #include "dcpomatic_button.h"
 #include "dcpomatic_choice.h"
 #include "dcpomatic_spin_ctrl.h"
@@ -111,6 +112,7 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
 
        _markers = new Button (_panel, _("Markers..."));
        _metadata = new Button (_panel, _("Metadata..."));
+       _reels = new Button(_panel, _("Reels..."));
 
        _notebook = new wxNotebook (_panel, wxID_ANY);
        _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6);
@@ -126,6 +128,8 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
        _standard->Bind              (wxEVT_CHOICE,   boost::bind(&DCPPanel::standard_changed, this));
        _markers->Bind               (wxEVT_BUTTON,   boost::bind(&DCPPanel::markers_clicked, this));
        _metadata->Bind              (wxEVT_BUTTON,   boost::bind(&DCPPanel::metadata_clicked, this));
+       _reels->Bind(wxEVT_BUTTON, boost::bind(&DCPPanel::reels_clicked, this));
+
        for (auto i: DCPContentType::all()) {
                _dcp_content_type->add(i->pretty_name());
        }
@@ -231,6 +235,7 @@ DCPPanel::add_to_grid ()
        auto extra = new wxBoxSizer (wxHORIZONTAL);
        extra->Add (_markers, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
        extra->Add (_metadata, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
+       extra->Add(_reels, 1, wxRIGHT, DCPOMATIC_SIZER_X_GAP);
        _grid->Add (extra, wxGBPosition(r, 0), wxGBSpan(1, 2));
        ++r;
 }
@@ -343,6 +348,14 @@ DCPPanel::metadata_clicked ()
 }
 
 
+void
+DCPPanel::reels_clicked()
+{
+       _dcp_timeline.reset(_panel, _film);
+       _dcp_timeline->Show();
+}
+
+
 void
 DCPPanel::film_changed(FilmProperty p)
 {
index a5af589218c657520a9ccea37798c7238b13bb1a..6c97a41c31c5bea31783616e163e0c6532a1ed75 100644 (file)
@@ -39,6 +39,7 @@ class wxGridBagSizer;
 
 class AudioDialog;
 class Choice;
+class DCPTimelineDialog;
 class Film;
 class FilmViewer;
 class InteropMetadataDialog;
@@ -84,6 +85,7 @@ private:
        void show_audio_clicked ();
        void markers_clicked ();
        void metadata_clicked ();
+       void reels_clicked();
        void reencode_j2k_changed ();
        void enable_audio_language_toggled ();
        void edit_audio_language_clicked ();
@@ -153,12 +155,14 @@ private:
        CheckBox* _encrypted;
        wxButton* _markers;
        wxButton* _metadata;
+       Button* _reels;
        wxSizer* _audio_panel_sizer;
 
        wx_ptr<AudioDialog> _audio_dialog;
        wx_ptr<MarkersDialog> _markers_dialog;
        wx_ptr<InteropMetadataDialog> _interop_metadata_dialog;
        wx_ptr<SMPTEMetadataDialog> _smpte_metadata_dialog;
+       wx_ptr<DCPTimelineDialog> _dcp_timeline;
 
        std::shared_ptr<Film> _film;
        FilmViewer& _viewer;
diff --git a/src/wx/dcp_timeline.cc b/src/wx/dcp_timeline.cc
new file mode 100644 (file)
index 0000000..3974997
--- /dev/null
@@ -0,0 +1,615 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "check_box.h"
+#include "colours.h"
+#include "dcp_timeline.h"
+#include "dcp_timeline_reel_marker_view.h"
+#include "dcpomatic_choice.h"
+#include "dcpomatic_spin_ctrl.h"
+#include "timecode.h"
+#include "wx_util.h"
+#include "lib/atmos_content.h"
+#include "lib/audio_content.h"
+#include "lib/constants.h"
+#include "lib/film.h"
+#include "lib/text_content.h"
+#include "lib/video_content.h"
+#include <dcp/scope_guard.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using std::dynamic_pointer_cast;
+using std::make_shared;
+using std::shared_ptr;
+using std::vector;
+using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+using namespace dcpomatic;
+
+
+auto constexpr reel_marker_y_pos = 48;
+auto constexpr content_y_pos = 112;
+auto constexpr content_type_height = 12;
+
+enum {
+       ID_add_reel_boundary,
+};
+
+
+class ReelBoundary
+{
+public:
+       ReelBoundary(wxWindow* parent, wxGridBagSizer* sizer, int index, DCPTime maximum, int fps, DCPTimeline& timeline, bool editable)
+               : _label(new wxStaticText(parent, wxID_ANY, wxString::Format(_("Reel %d to reel %d"), index + 1, index + 2)))
+               , _timecode(new Timecode<DCPTime>(parent, true))
+               , _index(index)
+               , _view(timeline, reel_marker_y_pos)
+               , _fps(fps)
+       {
+               sizer->Add(_label, wxGBPosition(index, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
+               sizer->Add(_timecode, wxGBPosition(index, 1));
+
+               _timecode->set_maximum(maximum.split(fps));
+               _timecode->set_editable(editable);
+               _timecode->Changed.connect(boost::bind(&ReelBoundary::timecode_changed, this));
+       }
+
+       ~ReelBoundary()
+       {
+               if (_label) {
+                       _label->Destroy();
+               }
+
+               if (_timecode) {
+                       _timecode->Destroy();
+               }
+       }
+
+       ReelBoundary(ReelBoundary const&) = delete;
+       ReelBoundary& operator=(ReelBoundary const&) = delete;
+
+       ReelBoundary(ReelBoundary&& other) = delete;
+       ReelBoundary& operator=(ReelBoundary&& other) = delete;
+
+       void set_time(DCPTime time)
+       {
+               if (_timecode) {
+                       _timecode->set(time, _fps);
+               }
+               _view.set_time(time);
+       }
+
+       dcpomatic::DCPTime time() const {
+               return _view.time();
+       }
+
+       int index() const {
+               return _index;
+       }
+
+       DCPTimelineReelMarkerView& view() {
+               return _view;
+       }
+
+       DCPTimelineReelMarkerView const& view() const {
+               return _view;
+       }
+
+       boost::signals2::signal<void (int, dcpomatic::DCPTime)> Changed;
+
+private:
+       void timecode_changed() {
+               set_time(_timecode->get(_fps));
+               Changed(_index, time());
+       }
+
+       wxStaticText* _label = nullptr;
+       Timecode<dcpomatic::DCPTime>* _timecode = nullptr;
+       int _index = 0;
+       DCPTimelineReelMarkerView _view;
+       int _fps;
+};
+
+
+DCPTimeline::DCPTimeline(wxWindow* parent, shared_ptr<Film> film)
+       : Timeline(parent)
+       , _film(film)
+       , _canvas(new wxScrolledCanvas(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _reel_settings(new wxPanel(this, wxID_ANY))
+       , _reel_detail(new wxPanel(this, wxID_ANY))
+       , _reel_detail_sizer(new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP))
+{
+#ifndef __WXOSX__
+       _canvas->SetDoubleBuffered(true);
+#endif
+       _reel_detail->SetSizer(_reel_detail_sizer);
+
+       auto sizer = new wxBoxSizer(wxVERTICAL);
+       sizer->Add(_reel_settings, 0);
+       sizer->Add(_canvas, 0, wxEXPAND);
+       sizer->Add(_reel_detail, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER);
+       SetSizer(sizer);
+
+       SetMinSize(wxSize(640, 480));
+       _canvas->SetMinSize({-1, content_y_pos + content_type_height * 4});
+
+       _canvas->Bind(wxEVT_PAINT, boost::bind(&DCPTimeline::paint, this));
+       _canvas->Bind(wxEVT_SIZE, boost::bind(&DCPTimeline::setup_pixels_per_second, this));
+       _canvas->Bind(wxEVT_LEFT_DOWN, boost::bind(&DCPTimeline::left_down, this, _1));
+       _canvas->Bind(wxEVT_RIGHT_DOWN, boost::bind(&DCPTimeline::right_down, this, _1));
+       _canvas->Bind(wxEVT_LEFT_UP, boost::bind(&DCPTimeline::left_up, this, _1));
+       _canvas->Bind(wxEVT_MOTION, boost::bind(&DCPTimeline::mouse_moved, this, _1));
+
+       _film_connection = film->Change.connect(boost::bind(&DCPTimeline::film_changed, this, _1, _2));
+
+       _menu = new wxMenu;
+       _add_reel_boundary = _menu->Append(ID_add_reel_boundary, _("Add reel"));
+       _canvas->Bind(wxEVT_MENU, boost::bind(&DCPTimeline::add_reel_boundary, this));
+
+       setup_reel_settings();
+       setup_reel_boundaries();
+
+       sizer->Layout();
+       setup_pixels_per_second();
+       setup_sensitivity();
+}
+
+
+void
+DCPTimeline::add_reel_boundary()
+{
+       auto boundaries = film()->custom_reel_boundaries();
+       boundaries.push_back(DCPTime::from_seconds(_right_down_position.x / _pixels_per_second.get_value_or(1)));
+       film()->set_custom_reel_boundaries(boundaries);
+}
+
+
+void
+DCPTimeline::film_changed(ChangeType type, FilmProperty property)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       switch (property) {
+       case FilmProperty::REEL_TYPE:
+       case FilmProperty::REEL_LENGTH:
+       case FilmProperty::CUSTOM_REEL_BOUNDARIES:
+               setup_sensitivity();
+               setup_reel_boundaries();
+               break;
+       case FilmProperty::CONTENT:
+       case FilmProperty::CONTENT_ORDER:
+               setup_pixels_per_second();
+               Refresh();
+               break;
+       default:
+               break;
+       }
+}
+
+
+void
+DCPTimeline::setup_sensitivity()
+{
+       _snap->Enable(editable());
+       _maximum_reel_size->Enable(film()->reel_type() == ReelType::BY_LENGTH);
+}
+
+
+void
+DCPTimeline::setup_reel_settings()
+{
+       auto sizer = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       _reel_settings->SetSizer(sizer);
+
+       int r = 0;
+       add_label_to_sizer(sizer, _reel_settings, _("Reel mode"), true, wxGBPosition(r, 0));
+       _reel_type = new Choice(_reel_settings);
+       _reel_type->add(_("Single reel"));
+       _reel_type->add(_("Split by video content"));
+       _reel_type->add(_("Split by maximum reel size"));
+       _reel_type->add(_("Custom"));
+       sizer->Add(_reel_type, wxGBPosition(r, 1));
+       ++r;
+
+       add_label_to_sizer(sizer, _reel_settings, _("Maximum reel size"), true, wxGBPosition(r, 0));
+       _maximum_reel_size = new SpinCtrl(_reel_settings, DCPOMATIC_SPIN_CTRL_WIDTH);
+       _maximum_reel_size->SetRange(1, 1000);
+       {
+               auto s = new wxBoxSizer(wxHORIZONTAL);
+               s->Add(_maximum_reel_size, 0);
+               add_label_to_sizer(s, _reel_settings, _("GB"), false, 0, wxALIGN_CENTER_VERTICAL | wxLEFT);
+               sizer->Add(s, wxGBPosition(r, 1));
+       }
+       ++r;
+
+       _snap = new CheckBox(_reel_settings, _("Snap when dragging"));
+       sizer->Add(_snap, wxGBPosition(r, 1));
+       ++r;
+
+       _reel_type->set(static_cast<int>(film()->reel_type()));
+       _maximum_reel_size->SetValue(film()->reel_length() / 1000000000LL);
+
+       _reel_type->bind(&DCPTimeline::reel_mode_changed, this);
+       _maximum_reel_size->Bind(wxEVT_SPINCTRL, boost::bind(&DCPTimeline::maximum_reel_size_changed, this));
+}
+
+
+void
+DCPTimeline::reel_mode_changed()
+{
+       film()->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
+}
+
+
+void
+DCPTimeline::maximum_reel_size_changed()
+{
+       film()->set_reel_length(_maximum_reel_size->GetValue() * 1000000000LL);
+}
+
+
+void
+DCPTimeline::set_reel_boundary(int index, DCPTime time)
+{
+       auto boundaries = film()->custom_reel_boundaries();
+       DCPOMATIC_ASSERT(index >= 0 && index < static_cast<int>(boundaries.size()));
+       boundaries[index] = time.round(film()->video_frame_rate());
+       film()->set_custom_reel_boundaries(boundaries);
+}
+
+
+void
+DCPTimeline::setup_reel_boundaries()
+{
+       auto const reels = film()->reels();
+       if (reels.empty()) {
+               _reel_boundaries.clear();
+               return;
+       }
+
+       size_t const boundaries = reels.size() - 1;
+       auto const maximum = film()->length();
+       for (size_t i = _reel_boundaries.size(); i < boundaries; ++i) {
+               auto boundary = std::make_shared<ReelBoundary>(
+                               _reel_detail, _reel_detail_sizer, i, maximum, film()->video_frame_rate(), *this, editable()
+                               );
+
+               boundary->Changed.connect(boost::bind(&DCPTimeline::set_reel_boundary, this, _1, _2));
+               _reel_boundaries.push_back(boundary);
+       }
+
+       _reel_boundaries.resize(boundaries);
+
+       auto const active = editable();
+       for (size_t i = 0; i < boundaries; ++i) {
+               _reel_boundaries[i]->set_time(reels[i].to);
+               _reel_boundaries[i]->view().set_active(active);
+       }
+
+       _reel_detail_sizer->Layout();
+       _canvas->Refresh();
+}
+
+
+void
+DCPTimeline::paint()
+{
+       wxPaintDC dc(_canvas);
+       dc.Clear();
+
+       if (film()->content().empty()) {
+               return;
+       }
+
+       _canvas->DoPrepareDC(dc);
+
+       auto gc = wxGraphicsContext::Create(dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       gc->SetAntialiasMode(wxANTIALIAS_DEFAULT);
+
+       paint_reels(gc);
+       paint_content(gc);
+}
+
+
+void
+DCPTimeline::paint_reels(wxGraphicsContext* gc)
+{
+       constexpr int x_offset = 2;
+
+       for (auto const& boundary: _reel_boundaries) {
+               boundary->view().paint(gc);
+       }
+
+       gc->SetFont(gc->CreateFont(*wxNORMAL_FONT, wxColour(0, 0, 0)));
+       gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 2, wxPENSTYLE_SOLID));
+
+       auto const pps = pixels_per_second().get_value_or(1);
+
+       auto start = gc->CreatePath();
+       start.MoveToPoint(x_offset, reel_marker_y_pos);
+       start.AddLineToPoint(x_offset, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+       gc->StrokePath(start);
+
+       auto const length = film()->length().seconds() * pps;
+       auto end = gc->CreatePath();
+       end.MoveToPoint(x_offset + length, reel_marker_y_pos);
+       end.AddLineToPoint(x_offset + length, reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT);
+       gc->StrokePath(end);
+
+       auto const y = reel_marker_y_pos + DCPTimelineReelMarkerView::HEIGHT * 3 / 4;
+
+       auto paint_reel = [gc, y](double from, double to, int index) {
+               auto path = gc->CreatePath();
+               path.MoveToPoint(from, y);
+               path.AddLineToPoint(to, y);
+               gc->StrokePath(path);
+
+               auto str = wxString::Format(wxT("#%d"), index + 1);
+               wxDouble str_width;
+               wxDouble str_height;
+               wxDouble str_descent;
+               wxDouble str_leading;
+               gc->GetTextExtent(str, &str_width, &str_height, &str_descent, &str_leading);
+
+               if (str_width < (to - from)) {
+                       gc->DrawText(str, (from + to - str_width) / 2, y - str_height - 2);
+               }
+       };
+
+       gc->SetPen(*wxThePenList->FindOrCreatePen(wxColour(0, 0, 255), 2, wxPENSTYLE_DOT));
+       int index = 0;
+       DCPTime last;
+       for (auto const& boundary: _reel_boundaries) {
+               paint_reel(last.seconds() * pps + 2, boundary->time().seconds() * pps, index++);
+               last = boundary->time();
+       }
+
+       paint_reel(last.seconds() * pps + 2, film()->length().seconds() * pps, index);
+}
+
+
+void
+DCPTimeline::paint_content(wxGraphicsContext* gc)
+{
+       auto const pps = pixels_per_second().get_value_or(1);
+       auto const film = this->film();
+
+       auto const& solid_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_SOLID);
+       auto const& dotted_pen = *wxThePenList->FindOrCreatePen(wxColour(0, 0, 0), 1, wxPENSTYLE_DOT);
+
+       auto const& video_brush = *wxTheBrushList->FindOrCreateBrush(VIDEO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& audio_brush = *wxTheBrushList->FindOrCreateBrush(AUDIO_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& text_brush = *wxTheBrushList->FindOrCreateBrush(TEXT_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+       auto const& atmos_brush = *wxTheBrushList->FindOrCreateBrush(ATMOS_CONTENT_COLOUR, wxBRUSHSTYLE_SOLID);
+
+       auto maybe_draw =
+               [gc, film, pps, solid_pen, dotted_pen]
+               (shared_ptr<Content> content, shared_ptr<ContentPart> part, wxBrush brush, int offset) {
+               if (part) {
+                       auto const y = content_y_pos + offset * content_type_height + 1;
+                       gc->SetPen(solid_pen);
+                       gc->SetBrush(brush);
+                       gc->DrawRectangle(
+                               content->position().seconds() * pps,
+                               y,
+                               content->length_after_trim(film).seconds() * pps,
+                               content_type_height - 2
+                               );
+
+                       gc->SetPen(dotted_pen);
+                       for (auto split: content->reel_split_points(film)) {
+                               if (split != content->position()) {
+                                       auto path = gc->CreatePath();
+                                       path.MoveToPoint(split.seconds() * pps, y);
+                                       path.AddLineToPoint(split.seconds() * pps, y + content_type_height - 2);
+                                       gc->StrokePath(path);
+                               }
+                       }
+               }
+       };
+
+       for (auto content: film->content()) {
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->video), video_brush, 0);
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->audio), audio_brush, 1);
+               for (auto text: content->text) {
+                       maybe_draw(content, dynamic_pointer_cast<ContentPart>(text), text_brush, 2);
+               }
+               maybe_draw(content, dynamic_pointer_cast<ContentPart>(content->atmos), atmos_brush, 3);
+       }
+}
+
+
+void
+DCPTimeline::setup_pixels_per_second()
+{
+       set_pixels_per_second((_canvas->GetSize().GetWidth() - 4) / std::max(1.0, film()->length().seconds()));
+}
+
+
+shared_ptr<ReelBoundary>
+DCPTimeline::event_to_reel_boundary(wxMouseEvent& ev) const
+{
+       Position<int> const position(ev.GetX(), ev.GetY());
+       auto iter = std::find_if(_reel_boundaries.begin(), _reel_boundaries.end(), [position](shared_ptr<const ReelBoundary> boundary) {
+               return boundary->view().bbox().contains(position);
+       });
+
+       if (iter == _reel_boundaries.end()) {
+               return {};
+       }
+
+       return *iter;
+}
+
+
+void
+DCPTimeline::left_down(wxMouseEvent& ev)
+{
+       if (!editable()) {
+               return;
+       }
+
+       if (auto boundary = event_to_reel_boundary(ev)) {
+               auto const snap_distance = DCPTime::from_seconds((_canvas->GetSize().GetWidth() / _pixels_per_second.get_value_or(1)) / SNAP_SUBDIVISION);
+               _drag = DCPTimeline::Drag(
+                       boundary,
+                       _reel_boundaries,
+                       film(),
+                       static_cast<int>(ev.GetX() - boundary->time().seconds() * _pixels_per_second.get_value_or(0)),
+                       _snap->get(),
+                       snap_distance
+                       );
+       } else {
+               _drag = boost::none;
+       }
+}
+
+
+void
+DCPTimeline::right_down(wxMouseEvent& ev)
+{
+       _right_down_position = ev.GetPosition();
+       _canvas->PopupMenu(_menu, _right_down_position);
+}
+
+
+void
+DCPTimeline::left_up(wxMouseEvent&)
+{
+       if (!_drag) {
+               return;
+       }
+
+       set_reel_boundary(_drag->reel_boundary->index(), _drag->time());
+       _drag = boost::none;
+}
+
+
+void
+DCPTimeline::mouse_moved(wxMouseEvent& ev)
+{
+       if (!_drag) {
+               return;
+       }
+
+       auto time = DCPTime::from_seconds((ev.GetPosition().x - _drag->offset) / _pixels_per_second.get_value_or(1));
+       time = std::max(_drag->previous ? _drag->previous->time() : DCPTime(), time);
+       time = std::min(_drag->next ? _drag->next->time() : film()->length(), time);
+       _drag->set_time(time);
+       _canvas->RefreshRect({0, reel_marker_y_pos - 2, _canvas->GetSize().GetWidth(), DCPTimelineReelMarkerView::HEIGHT + 4}, true);
+}
+
+
+void
+DCPTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+       _canvas->RefreshRect(wxRect(r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<Film>
+DCPTimeline::film() const
+{
+       auto film = _film.lock();
+       DCPOMATIC_ASSERT(film);
+       return film;
+}
+
+
+bool
+DCPTimeline::editable() const
+{
+       return film()->reel_type() == ReelType::CUSTOM;
+}
+
+
+DCPTimeline::Drag::Drag(
+       shared_ptr<ReelBoundary> reel_boundary_,
+       vector<shared_ptr<ReelBoundary>> const& reel_boundaries,
+       shared_ptr<const Film> film,
+       int offset_,
+       bool snap,
+       DCPTime snap_distance
+       )
+       : reel_boundary(reel_boundary_)
+       , offset(offset_)
+       , _snap_distance(snap_distance)
+{
+       auto iter = std::find(reel_boundaries.begin(), reel_boundaries.end(), reel_boundary);
+       auto index = std::distance(reel_boundaries.begin(), iter);
+
+       if (index > 0) {
+               previous = reel_boundaries[index - 1];
+       }
+       if (index < static_cast<int>(reel_boundaries.size() - 1)) {
+               next = reel_boundaries[index + 1];
+       }
+
+       if (snap) {
+               for (auto content: film->content()) {
+                       for (auto split: content->reel_split_points(film)) {
+                               _snaps.push_back(split);
+                       }
+               }
+       }
+}
+
+
+void
+DCPTimeline::Drag::set_time(DCPTime time)
+{
+       optional<DCPTime> nearest_distance;
+       optional<DCPTime> nearest_time;
+       for (auto snap: _snaps) {
+               auto const distance = time - snap;
+               if (!nearest_distance || distance.abs() < nearest_distance->abs()) {
+                       nearest_distance = distance.abs();
+                       nearest_time = snap;
+               }
+       }
+
+       if (nearest_distance && *nearest_distance < _snap_distance) {
+               reel_boundary->set_time(*nearest_time);
+       } else {
+               reel_boundary->set_time(time);
+       }
+}
+
+
+DCPTime
+DCPTimeline::Drag::time() const
+{
+       return reel_boundary->time();
+}
+
diff --git a/src/wx/dcp_timeline.h b/src/wx/dcp_timeline.h
new file mode 100644 (file)
index 0000000..3413c28
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_DCP_TIMELINE_H
+#define DCPOMATIC_DCP_TIMELINE_H
+
+
+#include "timecode.h"
+#include "timeline.h"
+#include "lib/rect.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class CheckBox;
+class Choice;
+class Film;
+class ReelBoundary;
+class SpinCtrl;
+class wxGridBagSizer;
+
+
+class DCPTimeline : public Timeline
+{
+public:
+       DCPTimeline(wxWindow* parent, std::shared_ptr<Film> film);
+
+       void force_redraw(dcpomatic::Rect<int> const &);
+
+private:
+       void paint();
+       void paint_reels(wxGraphicsContext* gc);
+       void paint_content(wxGraphicsContext* gc);
+       void setup_pixels_per_second();
+       void left_down(wxMouseEvent& ev);
+       void right_down(wxMouseEvent& ev);
+       void left_up(wxMouseEvent& ev);
+       void mouse_moved(wxMouseEvent& ev);
+       void reel_mode_changed();
+       void maximum_reel_size_changed();
+       void film_changed(ChangeType type, FilmProperty property);
+       std::shared_ptr<Film> film() const;
+       void setup_sensitivity();
+
+       void add_reel_boundary();
+       void setup_reel_settings();
+       void setup_reel_boundaries();
+       std::shared_ptr<ReelBoundary> event_to_reel_boundary(wxMouseEvent& ev) const;
+       void set_reel_boundary(int index, dcpomatic::DCPTime time);
+       bool editable() const;
+
+       std::weak_ptr<Film> _film;
+
+       wxScrolledCanvas* _canvas;
+
+       class Drag
+       {
+       public:
+               Drag(
+                       std::shared_ptr<ReelBoundary> reel_boundary_,
+                       std::vector<std::shared_ptr<ReelBoundary>> const& reel_boundaries,
+                       std::shared_ptr<const Film> film,
+                       int offset_,
+                       bool snap,
+                       dcpomatic::DCPTime snap_distance
+                   );
+
+               std::shared_ptr<ReelBoundary> reel_boundary;
+               std::shared_ptr<ReelBoundary> previous;
+               std::shared_ptr<ReelBoundary> next;
+               int offset = 0;
+
+               void set_time(dcpomatic::DCPTime time);
+               dcpomatic::DCPTime time() const;
+
+       private:
+               std::vector<dcpomatic::DCPTime> _snaps;
+               dcpomatic::DCPTime _snap_distance;
+       };
+
+       boost::optional<Drag> _drag;
+
+       wxPoint _right_down_position;
+
+       wxPanel* _reel_settings;
+       Choice* _reel_type;
+       SpinCtrl* _maximum_reel_size;
+       CheckBox* _snap;
+       wxPanel* _reel_detail;
+       wxGridBagSizer* _reel_detail_sizer;
+
+       wxMenu* _menu;
+       wxMenuItem* _add_reel_boundary;
+
+       boost::signals2::scoped_connection _film_connection;
+
+       std::vector<std::shared_ptr<ReelBoundary>> _reel_boundaries;
+};
+
+
+#endif
+
diff --git a/src/wx/dcp_timeline_dialog.cc b/src/wx/dcp_timeline_dialog.cc
new file mode 100644 (file)
index 0000000..2cf6a74
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_panel.h"
+#include "dcp_timeline_dialog.h"
+#include "film_editor.h"
+#include "wx_util.h"
+#include "lib/compose.hpp"
+#include "lib/cross.h"
+#include "lib/film.h"
+#include "lib/playlist.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <list>
+
+
+using std::list;
+using std::shared_ptr;
+using std::string;
+using std::weak_ptr;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+DCPTimelineDialog::DCPTimelineDialog(wxWindow* parent, shared_ptr<Film> film)
+       : wxDialog(
+               parent,
+               wxID_ANY,
+               _("Reels"),
+               wxDefaultPosition,
+               wxSize(640, 512),
+#ifdef DCPOMATIC_OSX
+               /* I can't get wxFRAME_FLOAT_ON_PARENT to work on OS X, and although wxSTAY_ON_TOP keeps
+                  the window above all others (and not just our own) it's better than nothing for now.
+               */
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxSTAY_ON_TOP
+#else
+               wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE | wxFRAME_FLOAT_ON_PARENT
+#endif
+               )
+       , _timeline(this, film)
+{
+       auto sizer = new wxBoxSizer(wxVERTICAL);
+       sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12);
+
+#ifdef DCPOMATIC_LINUX
+       auto buttons = CreateSeparatedButtonSizer (wxCLOSE);
+       if (buttons) {
+               sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
+       }
+#endif
+
+       SetSizer(sizer);
+       sizer->Layout();
+       sizer->SetSizeHints(this);
+}
+
diff --git a/src/wx/dcp_timeline_dialog.h b/src/wx/dcp_timeline_dialog.h
new file mode 100644 (file)
index 0000000..d1293ca
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <memory>
+
+
+class DCPTimelineDialog : public wxDialog
+{
+public:
+       DCPTimelineDialog(wxWindow* parent, std::shared_ptr<Film> film);
+
+private:
+       std::weak_ptr<Film> _film;
+       DCPTimeline _timeline;
+};
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.cc b/src/wx/dcp_timeline_reel_marker_view.cc
new file mode 100644 (file)
index 0000000..1c97ca1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_reel_marker_view.h"
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+using namespace std;
+using namespace dcpomatic;
+
+
+DCPTimelineReelMarkerView::DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos)
+       : DCPTimelineView(timeline)
+       , _y_pos(y_pos)
+{
+
+}
+
+
+int
+DCPTimelineReelMarkerView::x_pos() const
+{
+       /* Nudge it over slightly so that the full line width is drawn on the left hand side */
+       return time_x(_time) + 2;
+}
+
+
+void
+DCPTimelineReelMarkerView::do_paint(wxGraphicsContext* gc)
+{
+       wxColour const outline = _active ? wxColour(0, 0, 0) : wxColour(128, 128, 128);
+       wxColour const fill = _active ? wxColour(255, 0, 0) : wxColour(192, 192, 192);
+       gc->SetPen(*wxThePenList->FindOrCreatePen(outline, 2, wxPENSTYLE_SOLID));
+       gc->SetBrush(*wxTheBrushList->FindOrCreateBrush(fill, wxBRUSHSTYLE_SOLID));
+
+       gc->DrawRectangle(x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE);
+
+       auto path = gc->CreatePath();
+       path.MoveToPoint(x_pos(), _y_pos + HEAD_SIZE + TAIL_LENGTH);
+       path.AddLineToPoint(x_pos(), _y_pos);
+       gc->StrokePath(path);
+       gc->FillPath(path);
+}
+
+
+dcpomatic::Rect<int>
+DCPTimelineReelMarkerView::bbox() const
+{
+       return { x_pos(), _y_pos, HEAD_SIZE, HEAD_SIZE + TAIL_LENGTH };
+}
+
diff --git a/src/wx/dcp_timeline_reel_marker_view.h b/src/wx/dcp_timeline_reel_marker_view.h
new file mode 100644 (file)
index 0000000..273d982
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline_view.h"
+
+
+class DCPTimeline;
+
+
+class DCPTimelineReelMarkerView : public DCPTimelineView
+{
+public:
+       DCPTimelineReelMarkerView(DCPTimeline& timeline, int y_pos);
+
+       dcpomatic::Rect<int> bbox() const override;
+
+       dcpomatic::DCPTime time() const {
+               return _time;
+       }
+
+       void set_time(dcpomatic::DCPTime time) {
+               _time = time;
+       }
+
+       void set_active(bool active) {
+               _active = active;
+       }
+
+       static auto constexpr HEAD_SIZE = 16;
+       static auto constexpr TAIL_LENGTH = 28;
+       static auto constexpr HEIGHT = HEAD_SIZE + TAIL_LENGTH;
+
+private:
+       void do_paint(wxGraphicsContext* gc) override;
+       int x_pos() const;
+
+       dcpomatic::DCPTime _time;
+       int _y_pos;
+       bool _active = false;
+};
+
diff --git a/src/wx/dcp_timeline_view.h b/src/wx/dcp_timeline_view.h
new file mode 100644 (file)
index 0000000..24a75ca
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_timeline.h"
+#include "timeline_view.h"
+
+
+class DCPTimelineView : public TimelineView<DCPTimeline>
+{
+public:
+       explicit DCPTimelineView(DCPTimeline& timeline)
+               : TimelineView(timeline)
+       {}
+
+       void paint(wxGraphicsContext* gc)
+       {
+               _last_paint_bbox = bbox();
+               do_paint(gc);
+       }
+
+protected:
+       virtual void do_paint(wxGraphicsContext* context) = 0;
+};
+
+
+
index c644af96c90070dec7150026205a17083d8a0257..cf05dfe7dbab731083361435c9e2d64e4f0251be 100644 (file)
@@ -62,6 +62,9 @@ sources = """
           custom_scale_dialog.cc
           dcp_referencing_dialog.cc
           dcp_panel.cc
+          dcp_timeline.cc
+          dcp_timeline_dialog.cc
+          dcp_timeline_reel_marker_view.cc
           dcp_text_track_dialog.cc
           dcpomatic_button.cc
           dcpomatic_choice.cc