summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2023-12-13 00:42:22 +0100
committerCarl Hetherington <cth@carlh.net>2024-03-12 23:41:00 +0100
commitda46a695431d3b573924e53ac1a0163056a1a5b5 (patch)
tree936e6ae61f3f1eac565e48babf169de1833f9ac8
parent1d028a206c999bf61df84544e3aeb70cec4e505c (diff)
Add new interface for setting reel breaks (#2678).2678-reel-break
-rw-r--r--src/lib/film.cc17
-rw-r--r--src/wx/dcp_panel.cc13
-rw-r--r--src/wx/dcp_panel.h4
-rw-r--r--src/wx/dcp_timeline.cc615
-rw-r--r--src/wx/dcp_timeline.h123
-rw-r--r--src/wx/dcp_timeline_dialog.cc78
-rw-r--r--src/wx/dcp_timeline_dialog.h39
-rw-r--r--src/wx/dcp_timeline_reel_marker_view.cc71
-rw-r--r--src/wx/dcp_timeline_reel_marker_view.h59
-rw-r--r--src/wx/dcp_timeline_view.h44
-rw-r--r--src/wx/wscript3
11 files changed, 1066 insertions, 0 deletions
diff --git a/src/lib/film.cc b/src/lib/film.cc
index 835f3efdf..d2c73c8b5 100644
--- a/src/lib/film.cc
+++ b/src/lib/film.cc
@@ -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
diff --git a/src/wx/dcp_panel.cc b/src/wx/dcp_panel.cc
index 1afc1be3d..64b4935cc 100644
--- a/src/wx/dcp_panel.cc
+++ b/src/wx/dcp_panel.cc
@@ -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;
}
@@ -344,6 +349,14 @@ DCPPanel::metadata_clicked ()
void
+DCPPanel::reels_clicked()
+{
+ _dcp_timeline.reset(_panel, _film);
+ _dcp_timeline->Show();
+}
+
+
+void
DCPPanel::film_changed(FilmProperty p)
{
switch (p) {
diff --git a/src/wx/dcp_panel.h b/src/wx/dcp_panel.h
index a5af58921..6c97a41c3 100644
--- a/src/wx/dcp_panel.h
+++ b/src/wx/dcp_panel.h
@@ -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
index 000000000..3974997eb
--- /dev/null
+++ b/src/wx/dcp_timeline.cc
@@ -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
index 000000000..3413c2814
--- /dev/null
+++ b/src/wx/dcp_timeline.h
@@ -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
index 000000000..2cf6a74f1
--- /dev/null
+++ b/src/wx/dcp_timeline_dialog.cc
@@ -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
index 000000000..d1293ca26
--- /dev/null
+++ b/src/wx/dcp_timeline_dialog.h
@@ -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
index 000000000..1c97ca175
--- /dev/null
+++ b/src/wx/dcp_timeline_reel_marker_view.cc
@@ -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
index 000000000..273d98259
--- /dev/null
+++ b/src/wx/dcp_timeline_reel_marker_view.h
@@ -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
index 000000000..24a75cad2
--- /dev/null
+++ b/src/wx/dcp_timeline_view.h
@@ -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;
+};
+
+
+
diff --git a/src/wx/wscript b/src/wx/wscript
index c644af96c..cf05dfe7d 100644
--- a/src/wx/wscript
+++ b/src/wx/wscript
@@ -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