Merge branch '2678-reel-break' into v2.17.x
authorCarl Hetherington <cth@carlh.net>
Tue, 12 Mar 2024 22:42:45 +0000 (23:42 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 12 Mar 2024 22:42:45 +0000 (23:42 +0100)
60 files changed:
src/lib/constants.h
src/lib/dcpomatic_time.cc
src/lib/dcpomatic_time.h
src/lib/film.cc
src/lib/film.h
src/lib/film_property.h
src/lib/types.h
src/wx/colours.h [new file with mode: 0644]
src/wx/content_menu.cc
src/wx/content_panel.cc
src/wx/content_panel.h
src/wx/content_timeline.cc [new file with mode: 0644]
src/wx/content_timeline.h [new file with mode: 0644]
src/wx/content_timeline_atmos_view.cc [new file with mode: 0644]
src/wx/content_timeline_atmos_view.h [new file with mode: 0644]
src/wx/content_timeline_audio_view.cc [new file with mode: 0644]
src/wx/content_timeline_audio_view.h [new file with mode: 0644]
src/wx/content_timeline_dialog.cc [new file with mode: 0644]
src/wx/content_timeline_dialog.h [new file with mode: 0644]
src/wx/content_timeline_text_view.cc [new file with mode: 0644]
src/wx/content_timeline_text_view.h [new file with mode: 0644]
src/wx/content_timeline_video_view.cc [new file with mode: 0644]
src/wx/content_timeline_video_view.h [new file with mode: 0644]
src/wx/content_timeline_view.cc [new file with mode: 0644]
src/wx/content_timeline_view.h [new file with mode: 0644]
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/timecode.cc
src/wx/timecode.h
src/wx/timeline.cc
src/wx/timeline.h
src/wx/timeline_atmos_content_view.cc [deleted file]
src/wx/timeline_atmos_content_view.h [deleted file]
src/wx/timeline_audio_content_view.cc [deleted file]
src/wx/timeline_audio_content_view.h [deleted file]
src/wx/timeline_content_view.cc
src/wx/timeline_content_view.h
src/wx/timeline_dialog.cc [deleted file]
src/wx/timeline_dialog.h [deleted file]
src/wx/timeline_labels_view.cc
src/wx/timeline_labels_view.h
src/wx/timeline_reels_view.cc
src/wx/timeline_reels_view.h
src/wx/timeline_text_content_view.cc [deleted file]
src/wx/timeline_text_content_view.h [deleted file]
src/wx/timeline_time_axis_view.cc
src/wx/timeline_time_axis_view.h
src/wx/timeline_video_content_view.cc [deleted file]
src/wx/timeline_video_content_view.h [deleted file]
src/wx/timeline_view.cc [deleted file]
src/wx/timeline_view.h
src/wx/wscript
src/wx/wx_util.h

index 3b187155453d32b0a6f822e799fbbe08f918432a..bfe144420281ae7805408884bbebaf68eed01f59 100644 (file)
@@ -47,6 +47,7 @@
 #define MAX_CLOSED_CAPTION_XML_SIZE (256 * 1024)
 #define MAX_CLOSED_CAPTION_XML_SIZE_TEXT "256KB"
 #define CERTIFICATE_VALIDITY_PERIOD (10 * 365)
+#define SNAP_SUBDIVISION 64
 
 
 #endif
index ac797f8f4205589c5ad605dc768deb83bbfed54e..60fc5342a33437414fec6f1b76ad38086320324c 100644 (file)
@@ -27,6 +27,25 @@ using std::string;
 using namespace dcpomatic;
 
 
+bool
+dcpomatic::operator<=(HMSF const& a, HMSF const& b)
+{
+       if (a.h != b.h) {
+               return a.h <= b.h;
+       }
+
+       if (a.m != b.m) {
+               return a.m <= b.m;
+       }
+
+       if (a.s != b.s) {
+               return a.s <= b.s;
+       }
+
+       return a.f <= b.f;
+}
+
+
 template <>
 Time<ContentTimeDifferentiator, DCPTimeDifferentiator>::Time (DCPTime d, FrameRateChange f)
        : _t (llrint(d.get() * f.speed_up))
index 9ebb334fe118e284e9cb609d53b75e3e29b4cf3a..63bb865491f32cecc957c2181e550492fbdd8b29 100644 (file)
@@ -64,6 +64,9 @@ public:
 };
 
 
+bool operator<=(HMSF const& a, HMSF const& b);
+
+
 /** A time in seconds, expressed as a number scaled up by Time::HZ.  We want two different
  *  versions of this class, dcpomatic::ContentTime and dcpomatic::DCPTime, and we want it to be impossible to
  *  convert implicitly between the two.  Hence there's this template hack.  I'm not
index d747efb0e1706de0122f50fe42fa92f3828be7cc..d2c73c8b5ef5e02269d27c2e11637e601eff7f39 100644 (file)
@@ -413,6 +413,9 @@ Film::metadata (bool with_content_paths) const
        }
        root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type)));
        root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
+       for (auto boundary: _custom_reel_boundaries) {
+               root->add_child("CustomReelBoundary")->add_child_text(raw_convert<string>(boundary.get()));
+       }
        root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0");
        root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
        for (auto const& marker: _markers) {
@@ -600,6 +603,9 @@ Film::read_metadata (optional<boost::filesystem::path> path)
 
        _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(ReelType::SINGLE)));
        _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000);
+       for (auto boundary: f.node_children("CustomReelBoundary")) {
+               _custom_reel_boundaries.push_back(DCPTime(raw_convert<int64_t>(boundary->content())));
+       }
        _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false);
        _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
 
@@ -1233,6 +1239,16 @@ Film::set_reel_length (int64_t r)
        _reel_length = r;
 }
 
+
+void
+Film::set_custom_reel_boundaries(vector<DCPTime> boundaries)
+{
+       FilmChangeSignaller ch(this, FilmProperty::CUSTOM_REEL_BOUNDARIES);
+       std::sort(boundaries.begin(), boundaries.end());
+       _custom_reel_boundaries = std::move(boundaries);
+}
+
+
 void
 Film::set_reencode_j2k (bool r)
 {
@@ -1600,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
@@ -1804,15 +1837,16 @@ Film::audio_analysis_finished ()
        /* XXX */
 }
 
-list<DCPTimePeriod>
+
+vector<DCPTimePeriod>
 Film::reels () const
 {
-       list<DCPTimePeriod> p;
+       vector<DCPTimePeriod> periods;
        auto const len = length();
 
        switch (reel_type ()) {
        case ReelType::SINGLE:
-               p.push_back (DCPTimePeriod (DCPTime (), len));
+               periods.emplace_back(DCPTime(), len);
                break;
        case ReelType::BY_VIDEO_CONTENT:
        {
@@ -1837,7 +1871,7 @@ Film::reels () const
                for (auto t: split_points) {
                        if (last && (t - *last) >= DCPTime::from_seconds(1)) {
                                /* Period from *last to t is long enough; use it and start a new one */
-                               p.push_back (DCPTimePeriod(*last, t));
+                               periods.emplace_back(*last, t);
                                last = t;
                        } else if (!last) {
                                /* That was the first time, so start a new period */
@@ -1845,8 +1879,8 @@ Film::reels () const
                        }
                }
 
-               if (!p.empty()) {
-                       p.back().to = split_points.back();
+               if (!periods.empty()) {
+                       periods.back().to = split_points.back();
                }
                break;
        }
@@ -1859,16 +1893,29 @@ Film::reels () const
                Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate()));
                while (current < len) {
                        DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ()));
-                       p.push_back (DCPTimePeriod (current, end));
+                       periods.emplace_back(current, end);
                        current = end;
                }
                break;
        }
+       case ReelType::CUSTOM:
+       {
+               DCPTimePeriod current;
+               for (auto boundary: _custom_reel_boundaries) {
+                       current.to = boundary;
+                       periods.push_back(current);
+                       current.from = boundary;
+               }
+               current.to = len;
+               periods.push_back(current);
+               break;
+       }
        }
 
-       return p;
+       return periods;
 }
 
+
 /** @param period A period within the DCP
  *  @return Name of the content which most contributes to the given period.
  */
index 036bbed7ef3ad8e0a13aa6c24ea5223bbbc52b1b..0a0c5a4e1dcadd716af7197d3335eeb185ca0821 100644 (file)
@@ -186,7 +186,7 @@ public:
                return _playlist;
        }
 
-       std::list<dcpomatic::DCPTimePeriod> reels () const;
+       std::vector<dcpomatic::DCPTimePeriod> reels() const;
        std::list<int> mapped_audio_channels () const;
 
        boost::optional<dcp::LanguageTag> audio_language () const {
@@ -291,6 +291,10 @@ public:
                return _reel_length;
        }
 
+       std::vector<dcpomatic::DCPTime> custom_reel_boundaries() const {
+               return _custom_reel_boundaries;
+       }
+
        std::string context_id () const {
                return _context_id;
        }
@@ -408,6 +412,7 @@ public:
        void set_audio_processor (AudioProcessor const * processor);
        void set_reel_type (ReelType);
        void set_reel_length (int64_t);
+       void set_custom_reel_boundaries(std::vector<dcpomatic::DCPTime> boundaries);
        void set_reencode_j2k (bool);
        void set_marker (dcp::Marker type, dcpomatic::DCPTime time);
        void unset_marker (dcp::Marker type);
@@ -527,8 +532,10 @@ private:
        bool _limit_to_smpte_bv20;
        AudioProcessor const * _audio_processor;
        ReelType _reel_type;
-       /** Desired reel length in bytes, if _reel_type == REELTYPE_BY_LENGTH */
+       /** Desired reel length in bytes, if _reel_type == BY_LENGTH */
        int64_t _reel_length;
+       /** Reel boundaries (excluding those at the start and end, sorted in ascending order) if _reel_type == CUSTOM */
+       std::vector<dcpomatic::DCPTime> _custom_reel_boundaries;
        bool _reencode_j2k;
        /** true if the user has ever explicitly set the video frame rate of this film */
        bool _user_explicit_video_frame_rate;
index c232979657620e09a51a366415afb207d0b9d2e3..0841caa5ca19ea56ab1b3778e36c6cd18b687db3 100644 (file)
@@ -51,6 +51,7 @@ enum class FilmProperty {
        AUDIO_PROCESSOR,
        REEL_TYPE,
        REEL_LENGTH,
+       CUSTOM_REEL_BOUNDARIES,
        REENCODE_J2K,
        MARKERS,
        RATINGS,
index 36059401ec8eb96d73a6921d4715443ab028d9fc..c9c87bae5200b48bdf9bfdaf48d99216400bc110 100644 (file)
@@ -107,7 +107,8 @@ enum class ReelType
 {
        SINGLE,
        BY_VIDEO_CONTENT,
-       BY_LENGTH
+       BY_LENGTH,
+       CUSTOM
 };
 
 
diff --git a/src/wx/colours.h b/src/wx/colours.h
new file mode 100644 (file)
index 0000000..0d33208
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+    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/>.
+
+*/
+
+
+#define VIDEO_CONTENT_COLOUR (wxColour(242, 92, 120, 255))
+#define AUDIO_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
+#define TEXT_CONTENT_COLOUR (wxColour(163, 255, 154, 255))
+#define ATMOS_CONTENT_COLOUR (wxColour(149, 121, 232, 255))
index 4af5a71b708d66f118d336e1412445274883e4a2..9c50d56dab76f49cf2969b44f91de0f59391ce78 100644 (file)
 #include "content_advanced_dialog.h"
 #include "content_menu.h"
 #include "content_properties_dialog.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_video_view.h"
 #include "dir_dialog.h"
 #include "file_dialog.h"
 #include "film_viewer.h"
 #include "id.h"
 #include "repeat_dialog.h"
-#include "timeline_video_content_view.h"
-#include "timeline_audio_content_view.h"
 #include "wx_util.h"
 #include "lib/audio_content.h"
 #include "lib/config.h"
@@ -314,12 +314,12 @@ ContentMenu::remove ()
                                continue;
                        }
 
-                       shared_ptr<TimelineVideoContentView> video;
-                       shared_ptr<TimelineAudioContentView> audio;
+                       shared_ptr<ContentTimelineVideoView> video;
+                       shared_ptr<ContentTimelineAudioView> audio;
 
                        for (auto j: _views) {
-                               auto v = dynamic_pointer_cast<TimelineVideoContentView>(j);
-                               auto a = dynamic_pointer_cast<TimelineAudioContentView>(j);
+                               auto v = dynamic_pointer_cast<ContentTimelineVideoView>(j);
+                               auto a = dynamic_pointer_cast<ContentTimelineAudioView>(j);
                                if (v && v->content() == fc) {
                                        video = v;
                                } else if (a && a->content() == fc) {
index 9e73900fcf012efd41e30502e27509553aae74ae..ed9c15c834368c47b7a808315ab5f6b48a597e9c 100644 (file)
 
 #include "audio_panel.h"
 #include "content_panel.h"
+#include "content_timeline_dialog.h"
 #include "dcpomatic_button.h"
 #include "dir_dialog.h"
 #include "file_dialog.h"
 #include "film_viewer.h"
 #include "image_sequence_dialog.h"
 #include "text_panel.h"
-#include "timeline_dialog.h"
 #include "timing_panel.h"
 #include "video_panel.h"
 #include "wx_util.h"
index ca0d4971946f779336d28da953da681ddfd2b222..38b634a62566895bc12488322aabf115bf89c4ea 100644 (file)
@@ -33,12 +33,12 @@ LIBDCP_ENABLE_WARNINGS
 class AudioPanel;
 class ContentListCtrl;
 class ContentSubPanel;
+class ContentTimelineDialog;
 class Film;
 class FilmEditor;
 class FilmViewer;
 class LimitedContentPanelSplitter;
 class TextPanel;
-class TimelineDialog;
 class TimingPanel;
 class VideoPanel;
 class wxListCtrl;
@@ -132,7 +132,7 @@ private:
        EnumIndexedVector<TextPanel*, TextType> _text_panel;
        TimingPanel* _timing_panel;
        ContentMenu* _menu;
-       wx_ptr<TimelineDialog> _timeline_dialog;
+       wx_ptr<ContentTimelineDialog> _timeline_dialog;
        wxNotebook* _parent;
        wxWindow* _last_selected_tab = nullptr;
 
diff --git a/src/wx/content_timeline.cc b/src/wx/content_timeline.cc
new file mode 100644 (file)
index 0000000..663f930
--- /dev/null
@@ -0,0 +1,1021 @@
+/*
+    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 "content_panel.h"
+#include "content_timeline.h"
+#include "film_editor.h"
+#include "film_viewer.h"
+#include "content_timeline_atmos_view.h"
+#include "content_timeline_audio_view.h"
+#include "content_timeline_text_view.h"
+#include "content_timeline_video_view.h"
+#include "timeline_labels_view.h"
+#include "timeline_reels_view.h"
+#include "timeline_time_axis_view.h"
+#include "wx_util.h"
+#include "lib/atmos_mxf_content.h"
+#include "lib/audio_content.h"
+#include "lib/constants.h"
+#include "lib/film.h"
+#include "lib/image_content.h"
+#include "lib/playlist.h"
+#include "lib/text_content.h"
+#include "lib/timer.h"
+#include "lib/video_content.h"
+#include <dcp/scope_guard.h>
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/graphics.h>
+LIBDCP_ENABLE_WARNINGS
+#include <iterator>
+#include <list>
+
+
+using std::abs;
+using std::dynamic_pointer_cast;
+using std::list;
+using std::make_shared;
+using std::max;
+using std::min;
+using std::shared_ptr;
+using std::weak_ptr;
+using boost::bind;
+using boost::optional;
+using namespace dcpomatic;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
+
+
+int const ContentTimeline::_minimum_pixels_per_track = 16;
+
+
+ContentTimeline::ContentTimeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+       : Timeline(parent)
+       , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
+       , _content_panel (cp)
+       , _film (film)
+       , _viewer (viewer)
+       , _time_axis_view (new TimelineTimeAxisView (*this, 64))
+       , _reels_view (new TimelineReelsView (*this, 32))
+       , _labels_view (new TimelineLabelsView (*this))
+       , _tracks (0)
+       , _left_down (false)
+       , _down_view_position (0)
+       , _first_move (false)
+       , _menu (this, viewer)
+       , _snap (true)
+       , _tool (SELECT)
+       , _x_scroll_rate (16)
+       , _y_scroll_rate (16)
+       , _pixels_per_track (48)
+       , _first_resize (true)
+       , _timer (this)
+{
+#ifndef __WXOSX__
+       _labels_canvas->SetDoubleBuffered (true);
+       _main_canvas->SetDoubleBuffered (true);
+#endif
+
+       auto sizer = new wxBoxSizer (wxHORIZONTAL);
+       sizer->Add (_labels_canvas, 0, wxEXPAND);
+       _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
+       sizer->Add (_main_canvas, 1, wxEXPAND);
+       SetSizer (sizer);
+
+       _labels_canvas->Bind(wxEVT_PAINT,      boost::bind(&ContentTimeline::paint_labels, this));
+       _main_canvas->Bind  (wxEVT_PAINT,      boost::bind(&ContentTimeline::paint_main,   this));
+       _main_canvas->Bind  (wxEVT_LEFT_DOWN,  boost::bind(&ContentTimeline::left_down,    this, _1));
+       _main_canvas->Bind  (wxEVT_LEFT_UP,    boost::bind(&ContentTimeline::left_up,      this, _1));
+       _main_canvas->Bind  (wxEVT_RIGHT_DOWN, boost::bind(&ContentTimeline::right_down,   this, _1));
+       _main_canvas->Bind  (wxEVT_MOTION,     boost::bind(&ContentTimeline::mouse_moved,  this, _1));
+       _main_canvas->Bind  (wxEVT_SIZE,       boost::bind(&ContentTimeline::resized,      this));
+       _main_canvas->Bind  (wxEVT_MOUSEWHEEL, boost::bind(&ContentTimeline::mouse_wheel_turned, this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_TOP,        boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_BOTTOM,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_LINEUP,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_LINEDOWN,   boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_PAGEUP,     boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_PAGEDOWN,   boost::bind(&ContentTimeline::scrolled,     this, _1));
+       _main_canvas->Bind  (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind(&ContentTimeline::scrolled,     this, _1));
+
+       film_change(ChangeType::DONE, FilmProperty::CONTENT);
+
+       SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
+
+       _film_changed_connection = film->Change.connect(bind(&ContentTimeline::film_change, this, _1, _2));
+       _film_content_change_connection = film->ContentChange.connect(bind(&ContentTimeline::film_content_change, this, _1, _3, _4));
+
+       Bind(wxEVT_TIMER, boost::bind(&ContentTimeline::update_playhead, this));
+       _timer.Start (200, wxTIMER_CONTINUOUS);
+
+       setup_scrollbars ();
+       _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
+}
+
+
+void
+ContentTimeline::mouse_wheel_turned(wxMouseEvent& event)
+{
+       auto const rotation = event.GetWheelRotation();
+
+       if (event.ControlDown()) {
+               /* On my mouse one click of the scroll wheel is 120, and it's -ve when
+                * scrolling the wheel towards me.
+                */
+               auto const scale = rotation > 0 ?
+                       (1.0 / (rotation / 90.0)) :
+                       (-rotation / 90.0);
+
+               int before_start_x;
+               int before_start_y;
+               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+
+               auto const before_pps = _pixels_per_second.get_value_or(1);
+               auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
+                       *_last_mouse_wheel_time :
+                       (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
+
+               set_pixels_per_second(before_pps * scale);
+               setup_scrollbars();
+
+               auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
+               _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
+               _labels_canvas->Scroll(0, before_start_y);
+               Refresh();
+
+               if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
+                       _last_mouse_wheel_x = event.GetX();
+                       _last_mouse_wheel_time = before_pos;
+               }
+       } else if (event.ShiftDown()) {
+               int before_start_x;
+               int before_start_y;
+               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
+               auto const width = _main_canvas->GetSize().GetWidth();
+               _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
+       }
+}
+
+
+void
+ContentTimeline::update_playhead()
+{
+       Refresh ();
+}
+
+
+void
+ContentTimeline::paint_labels()
+{
+       wxPaintDC dc (_labels_canvas);
+
+       auto film = _film.lock();
+       if (film->content().empty()) {
+               return;
+       }
+
+       auto gc = wxGraphicsContext::Create (dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       int vsx, vsy;
+       _labels_canvas->GetViewStart (&vsx, &vsy);
+       gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
+
+       _labels_view->paint (gc, {});
+}
+
+
+void
+ContentTimeline::paint_main()
+{
+       wxPaintDC dc (_main_canvas);
+       dc.Clear();
+
+       auto film = _film.lock();
+       if (film->content().empty()) {
+               return;
+       }
+
+       _main_canvas->DoPrepareDC (dc);
+
+       auto gc = wxGraphicsContext::Create (dc);
+       if (!gc) {
+               return;
+       }
+
+       dcp::ScopeGuard sg = [gc]() { delete gc; };
+
+       gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
+
+       for (auto i: _views) {
+
+               auto ic = dynamic_pointer_cast<TimelineContentView> (i);
+
+               /* Find areas of overlap with other content views, so that we can plot them */
+               list<dcpomatic::Rect<int>> overlaps;
+               for (auto j: _views) {
+                       auto jc = dynamic_pointer_cast<TimelineContentView> (j);
+                       /* No overlap with non-content views, views on different tracks, audio views or non-active views */
+                       if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
+                               continue;
+                       }
+
+                       auto r = j->bbox().intersection(i->bbox());
+                       if (r) {
+                               overlaps.push_back (r.get ());
+                       }
+               }
+
+               i->paint (gc, overlaps);
+       }
+
+       if (_zoom_point) {
+               gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
+               gc->SetBrush (*wxTRANSPARENT_BRUSH);
+               gc->DrawRectangle (
+                       min (_down_point.x, _zoom_point->x),
+                       min (_down_point.y, _zoom_point->y),
+                       abs (_down_point.x - _zoom_point->x),
+                       abs (_down_point.y - _zoom_point->y)
+                       );
+       }
+
+       /* Playhead */
+
+       gc->SetPen (*wxRED_PEN);
+       auto path = gc->CreatePath ();
+       double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
+       path.MoveToPoint (ph, 0);
+       path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
+       gc->StrokePath (path);
+}
+
+
+void
+ContentTimeline::film_change(ChangeType type, FilmProperty p)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
+               ensure_ui_thread ();
+               recreate_views ();
+       } else if (p == FilmProperty::CONTENT_ORDER) {
+               Refresh ();
+       }
+}
+
+
+void
+ContentTimeline::recreate_views()
+{
+       auto film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       _views.clear ();
+       _views.push_back (_time_axis_view);
+       _views.push_back (_reels_view);
+
+       for (auto i: film->content ()) {
+               if (i->video) {
+                       _views.push_back(make_shared<ContentTimelineVideoView>(*this, i));
+               }
+
+               if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
+                       _views.push_back(make_shared<ContentTimelineAudioView>(*this, i));
+               }
+
+               for (auto j: i->text) {
+                       _views.push_back(make_shared<ContentTimelineTextView>(*this, i, j));
+               }
+
+               if (i->atmos) {
+                       _views.push_back(make_shared<ContentTimelineAtmosView>(*this, i));
+               }
+       }
+
+       assign_tracks ();
+       setup_scrollbars ();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::film_content_change(ChangeType type, int property, bool frequent)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       ensure_ui_thread ();
+
+       if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
+               recreate_views ();
+       } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
+               _reels_view->force_redraw ();
+       } else if (!frequent) {
+               setup_scrollbars ();
+               Refresh ();
+       }
+}
+
+
+template <class T>
+int
+place(shared_ptr<const Film> film, ContentTimelineViewList& views, int& tracks)
+{
+       int const base = tracks;
+
+       for (auto i: views) {
+               if (!dynamic_pointer_cast<T>(i)) {
+                       continue;
+               }
+
+               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+               DCPOMATIC_ASSERT(cv);
+
+               int t = base;
+
+               auto content = cv->content();
+               DCPTimePeriod const content_period = content->period(film);
+
+               while (true) {
+                       auto j = views.begin();
+                       while (j != views.end()) {
+                               auto test = dynamic_pointer_cast<T> (*j);
+                               if (!test) {
+                                       ++j;
+                                       continue;
+                               }
+
+                               auto test_content = test->content();
+                               if (
+                                       test->track() && test->track().get() == t &&
+                                       content_period.overlap(test_content->period(film))
+                                  ) {
+                                       /* we have an overlap on track `t' */
+                                       ++t;
+                                       break;
+                               }
+
+                               ++j;
+                       }
+
+                       if (j == views.end ()) {
+                               /* no overlap on `t' */
+                               break;
+                       }
+               }
+
+               cv->set_track (t);
+               tracks = max (tracks, t + 1);
+       }
+
+       return tracks - base;
+}
+
+
+/** Compare the mapped output channels of two TimelineViews, so we can into
+ *  order of first mapped DCP channel.
+ */
+struct AudioMappingComparator {
+       bool operator()(shared_ptr<ContentTimelineView> a, shared_ptr<ContentTimelineView> b) {
+               int la = -1;
+               auto cva = dynamic_pointer_cast<ContentTimelineAudioView>(a);
+               if (cva) {
+                       auto oc = cva->content()->audio->mapping().mapped_output_channels();
+                       la = *min_element(boost::begin(oc), boost::end(oc));
+               }
+               int lb = -1;
+               auto cvb = dynamic_pointer_cast<ContentTimelineAudioView>(b);
+               if (cvb) {
+                       auto oc = cvb->content()->audio->mapping().mapped_output_channels();
+                       lb = *min_element(boost::begin(oc), boost::end(oc));
+               }
+               return la < lb;
+       }
+};
+
+
+void
+ContentTimeline::assign_tracks()
+{
+       /* Tracks are:
+          Video 1
+          Video 2
+          Video N
+          Text 1
+          Text 2
+          Text N
+          Atmos
+          Audio 1
+          Audio 2
+          Audio N
+       */
+
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+
+       _tracks = 0;
+
+       for (auto i: _views) {
+               auto c = dynamic_pointer_cast<TimelineContentView>(i);
+               if (c) {
+                       c->unset_track ();
+               }
+       }
+
+       int const video_tracks = place<ContentTimelineVideoView>(film, _views, _tracks);
+       int const text_tracks = place<ContentTimelineTextView>(film, _views, _tracks);
+
+       /* Atmos */
+
+       bool have_atmos = false;
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<ContentTimelineAtmosView>(i);
+               if (cv) {
+                       cv->set_track (_tracks);
+                       have_atmos = true;
+               }
+       }
+
+       if (have_atmos) {
+               ++_tracks;
+       }
+
+       /* Audio.  We're sorting the views so that we get the audio views in order of increasing
+          DCP channel index.
+       */
+
+       auto views = _views;
+       sort(views.begin(), views.end(), AudioMappingComparator());
+       int const audio_tracks = place<ContentTimelineAudioView>(film, views, _tracks);
+
+       _labels_view->set_video_tracks (video_tracks);
+       _labels_view->set_audio_tracks (audio_tracks);
+       _labels_view->set_text_tracks (text_tracks);
+       _labels_view->set_atmos (have_atmos);
+
+       _time_axis_view->set_y (tracks());
+       _reels_view->set_y (8);
+}
+
+
+int
+ContentTimeline::tracks() const
+{
+       return _tracks;
+}
+
+
+void
+ContentTimeline::setup_scrollbars()
+{
+       auto film = _film.lock ();
+       if (!film || !_pixels_per_second) {
+               return;
+       }
+
+       int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
+
+       _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
+       _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+       _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
+       _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
+}
+
+
+shared_ptr<ContentTimelineView>
+ContentTimeline::event_to_view(wxMouseEvent& ev)
+{
+       /* Search backwards through views so that we find the uppermost one first */
+       auto i = _views.rbegin();
+
+       int vsx, vsy;
+       _main_canvas->GetViewStart (&vsx, &vsy);
+       Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
+
+       while (i != _views.rend() && !(*i)->bbox().contains (p)) {
+               ++i;
+       }
+
+       if (i == _views.rend ()) {
+               return {};
+       }
+
+       return *i;
+}
+
+
+void
+ContentTimeline::left_down(wxMouseEvent& ev)
+{
+       _left_down = true;
+       _down_point = ev.GetPosition ();
+
+       switch (_tool) {
+       case SELECT:
+               left_down_select (ev);
+               break;
+       case ZOOM:
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               /* Nothing to do */
+               break;
+       }
+}
+
+
+void
+ContentTimeline::left_down_select(wxMouseEvent& ev)
+{
+       auto view = event_to_view (ev);
+       auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
+
+       _down_view.reset ();
+
+       if (content_view) {
+               _down_view = content_view;
+               _down_view_position = content_view->content()->position ();
+       }
+
+       if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
+               int vsx, vsy;
+               _main_canvas->GetViewStart(&vsx, &vsy);
+               _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
+       }
+
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (!cv) {
+                       continue;
+               }
+
+               if (!ev.ShiftDown ()) {
+                       cv->set_selected (view == i);
+               }
+       }
+
+       if (content_view && ev.ShiftDown ()) {
+               content_view->set_selected (!content_view->selected ());
+       }
+
+       _first_move = false;
+
+       if (_down_view) {
+               /* Pre-compute the points that we might snap to */
+               for (auto i: _views) {
+                       auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+                       if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
+                               continue;
+                       }
+
+                       auto film = _film.lock ();
+                       DCPOMATIC_ASSERT (film);
+
+                       _start_snaps.push_back (cv->content()->position());
+                       _end_snaps.push_back (cv->content()->position());
+                       _start_snaps.push_back (cv->content()->end(film));
+                       _end_snaps.push_back (cv->content()->end(film));
+
+                       for (auto i: cv->content()->reel_split_points(film)) {
+                               _start_snaps.push_back (i);
+                       }
+               }
+
+               /* Tell everyone that things might change frequently during the drag */
+               _down_view->content()->set_change_signals_frequent (true);
+       }
+}
+
+
+void
+ContentTimeline::left_up(wxMouseEvent& ev)
+{
+       _left_down = false;
+
+       switch (_tool) {
+       case SELECT:
+               left_up_select (ev);
+               break;
+       case ZOOM:
+               left_up_zoom (ev);
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::left_up_select(wxMouseEvent& ev)
+{
+       if (_down_view) {
+               _down_view->content()->set_change_signals_frequent (false);
+       }
+
+       _content_panel->set_selection (selected_content ());
+       /* Since we may have just set change signals back to `not-frequent', we have to
+          make sure this position change is signalled, even if the position value has
+          not changed since the last time it was set (with frequent=true).  This is
+          a bit of a hack.
+       */
+       set_position_from_event (ev, true);
+
+       /* Clear up up the stuff we don't do during drag */
+       assign_tracks ();
+       setup_scrollbars ();
+       Refresh ();
+
+       _start_snaps.clear ();
+       _end_snaps.clear ();
+}
+
+
+void
+ContentTimeline::left_up_zoom(wxMouseEvent& ev)
+{
+       _zoom_point = ev.GetPosition ();
+
+       int vsx, vsy;
+       _main_canvas->GetViewStart (&vsx, &vsy);
+
+       wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
+       wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
+
+       if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
+               /* Very small zoom rectangle: we assume it wasn't intentional */
+               _zoom_point = optional<wxPoint> ();
+               Refresh ();
+               return;
+       }
+
+       auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
+       auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
+       set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
+
+       double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
+       double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
+       set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
+
+       setup_scrollbars ();
+       int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
+       _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
+       _labels_canvas->Scroll (0, y);
+
+       _zoom_point = optional<wxPoint> ();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::set_pixels_per_track(int h)
+{
+       _pixels_per_track = max(_minimum_pixels_per_track, h);
+}
+
+
+void
+ContentTimeline::mouse_moved(wxMouseEvent& ev)
+{
+       switch (_tool) {
+       case SELECT:
+               mouse_moved_select (ev);
+               break;
+       case ZOOM:
+               mouse_moved_zoom (ev);
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::mouse_moved_select(wxMouseEvent& ev)
+{
+       if (!_left_down) {
+               return;
+       }
+
+       set_position_from_event (ev);
+}
+
+
+void
+ContentTimeline::mouse_moved_zoom(wxMouseEvent& ev)
+{
+       if (!_left_down) {
+               return;
+       }
+
+       _zoom_point = ev.GetPosition ();
+       setup_scrollbars();
+       Refresh ();
+}
+
+
+void
+ContentTimeline::right_down(wxMouseEvent& ev)
+{
+       switch (_tool) {
+       case SELECT:
+               right_down_select (ev);
+               break;
+       case ZOOM:
+               /* Zoom out */
+               set_pixels_per_second (*_pixels_per_second / 2);
+               set_pixels_per_track (_pixels_per_track / 2);
+               setup_scrollbars ();
+               Refresh ();
+               break;
+       case ZOOM_ALL:
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::right_down_select(wxMouseEvent& ev)
+{
+       auto view = event_to_view (ev);
+       auto cv = dynamic_pointer_cast<TimelineContentView> (view);
+       if (!cv) {
+               return;
+       }
+
+       if (!cv->selected ()) {
+               clear_selection ();
+               cv->set_selected (true);
+       }
+
+       _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
+}
+
+
+void
+ContentTimeline::maybe_snap(DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
+{
+       auto const d = a - b;
+       if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
+               nearest_distance = d;
+       }
+}
+
+
+void
+ContentTimeline::set_position_from_event(wxMouseEvent& ev, bool force_emit)
+{
+       if (!_pixels_per_second) {
+               return;
+       }
+
+       double const pps = _pixels_per_second.get ();
+
+       auto const p = ev.GetPosition();
+
+       if (!_first_move) {
+               /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
+                  before the drag is considered to have started.
+               */
+               int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
+               if (dist < 8) {
+                       return;
+               }
+               _first_move = true;
+       }
+
+       if (!_down_view) {
+               return;
+       }
+
+       auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
+
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+
+       if (_snap) {
+               auto const new_end = new_position + _down_view->content()->length_after_trim(film);
+               /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
+                  positive is right).
+               */
+               optional<DCPTime> nearest_distance;
+
+               /* Find the nearest snap point */
+
+               for (auto i: _start_snaps) {
+                       maybe_snap (i, new_position, nearest_distance);
+               }
+
+               for (auto i: _end_snaps) {
+                       maybe_snap (i, new_end, nearest_distance);
+               }
+
+               if (nearest_distance) {
+                       /* Snap if it's close; `close' means within a proportion of the time on the timeline */
+                       if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / SNAP_SUBDIVISION)) {
+                               new_position += nearest_distance.get ();
+                       }
+               }
+       }
+
+       if (new_position < DCPTime ()) {
+               new_position = DCPTime ();
+       }
+
+       _down_view->content()->set_position (film, new_position, force_emit);
+
+       film->set_sequence (false);
+}
+
+
+void
+ContentTimeline::force_redraw(dcpomatic::Rect<int> const & r)
+{
+       _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
+}
+
+
+shared_ptr<const Film>
+ContentTimeline::film() const
+{
+       return _film.lock ();
+}
+
+
+void
+ContentTimeline::resized()
+{
+       if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
+               zoom_all ();
+               _first_resize = false;
+       }
+       setup_scrollbars ();
+}
+
+
+void
+ContentTimeline::clear_selection()
+{
+       for (auto i: _views) {
+               shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (cv) {
+                       cv->set_selected (false);
+               }
+       }
+}
+
+
+TimelineContentViewList
+ContentTimeline::selected_views() const
+{
+       TimelineContentViewList sel;
+
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
+               if (cv && cv->selected()) {
+                       sel.push_back (cv);
+               }
+       }
+
+       return sel;
+}
+
+
+ContentList
+ContentTimeline::selected_content() const
+{
+       ContentList sel;
+
+       for (auto i: selected_views()) {
+               sel.push_back(i->content());
+       }
+
+       return sel;
+}
+
+
+void
+ContentTimeline::set_selection(ContentList selection)
+{
+       for (auto i: _views) {
+               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
+               if (cv) {
+                       cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
+               }
+       }
+}
+
+
+int
+ContentTimeline::tracks_y_offset() const
+{
+       return _reels_view->bbox().height + 4;
+}
+
+
+int
+ContentTimeline::width() const
+{
+       return _main_canvas->GetVirtualSize().GetWidth();
+}
+
+
+void
+ContentTimeline::scrolled(wxScrollWinEvent& ev)
+{
+       if (ev.GetOrientation() == wxVERTICAL) {
+               int x, y;
+               _main_canvas->GetViewStart (&x, &y);
+               _labels_canvas->Scroll (0, y);
+       }
+       ev.Skip ();
+}
+
+
+void
+ContentTimeline::tool_clicked(Tool t)
+{
+       switch (t) {
+       case ZOOM:
+       case SELECT:
+               _tool = t;
+               break;
+       case ZOOM_ALL:
+               zoom_all ();
+               break;
+       case SNAP:
+       case SEQUENCE:
+               break;
+       }
+}
+
+
+void
+ContentTimeline::zoom_all()
+{
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+       set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
+       set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
+       setup_scrollbars ();
+       _main_canvas->Scroll (0, 0);
+       _labels_canvas->Scroll (0, 0);
+       Refresh ();
+}
+
+
+void
+ContentTimeline::keypress(wxKeyEvent const& event)
+{
+       if (event.GetKeyCode() == WXK_DELETE) {
+               auto film = _film.lock();
+               DCPOMATIC_ASSERT(film);
+               film->remove_content(selected_content());
+       } else {
+               switch (event.GetRawKeyCode()) {
+               case '+':
+                       set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
+                       setup_scrollbars();
+                       break;
+               case '-':
+                       set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
+                       setup_scrollbars();
+                       break;
+               }
+       }
+}
+
diff --git a/src/wx/content_timeline.h b/src/wx/content_timeline.h
new file mode 100644 (file)
index 0000000..10f8801
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+    Copyright (C) 2013-2019 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 "content_menu.h"
+#include "timeline.h"
+#include "timeline_content_view.h"
+#include "lib/film_property.h"
+#include "lib/rect.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+#include <boost/signals2.hpp>
+
+
+class ContentPanel;
+class ContentTimelineView;
+class Film;
+class FilmViewer;
+class TimelineLabelsView;
+class TimelineReelsView;
+class TimelineTimeAxisView;
+
+
+class ContentTimeline : public Timeline
+{
+public:
+       ContentTimeline(wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+
+       std::shared_ptr<const Film> film () const;
+
+       void force_redraw (dcpomatic::Rect<int> const &);
+
+       int width () const;
+
+       int pixels_per_track () const {
+               return _pixels_per_track;
+       }
+
+       int tracks () const;
+
+       void set_snap (bool s) {
+               _snap = s;
+       }
+
+       bool snap () const {
+               return _snap;
+       }
+
+       void set_selection (ContentList selection);
+
+       enum Tool {
+               SELECT,
+               ZOOM,
+               ZOOM_ALL,
+               SNAP,
+               SEQUENCE
+       };
+
+       void tool_clicked (Tool t);
+
+       int tracks_y_offset () const;
+
+       void keypress(wxKeyEvent const &);
+
+private:
+       void paint_labels ();
+       void paint_main ();
+       void left_down (wxMouseEvent &);
+       void left_down_select (wxMouseEvent &);
+       void left_up (wxMouseEvent &);
+       void left_up_select (wxMouseEvent &);
+       void left_up_zoom (wxMouseEvent &);
+       void right_down (wxMouseEvent &);
+       void right_down_select (wxMouseEvent &);
+       void mouse_moved (wxMouseEvent &);
+       void mouse_moved_select (wxMouseEvent &);
+       void mouse_moved_zoom (wxMouseEvent &);
+       void film_change(ChangeType type, FilmProperty);
+       void film_content_change (ChangeType type, int, bool frequent);
+       void resized ();
+       void assign_tracks ();
+       void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
+       void clear_selection ();
+       void recreate_views ();
+       void setup_scrollbars ();
+       void scrolled (wxScrollWinEvent& ev);
+       void set_pixels_per_track (int h);
+       void zoom_all ();
+       void update_playhead ();
+       void mouse_wheel_turned(wxMouseEvent& event);
+
+       std::shared_ptr<ContentTimelineView> event_to_view(wxMouseEvent &);
+       TimelineContentViewList selected_views () const;
+       ContentList selected_content () const;
+       void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
+
+       wxScrolledCanvas* _labels_canvas;
+       wxScrolledCanvas* _main_canvas;
+       ContentPanel* _content_panel;
+       std::weak_ptr<Film> _film;
+       FilmViewer& _viewer;
+       ContentTimelineViewList _views;
+       std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
+       std::shared_ptr<TimelineReelsView> _reels_view;
+       std::shared_ptr<TimelineLabelsView> _labels_view;
+       int _tracks;
+       bool _left_down;
+       wxPoint _down_point;
+       boost::optional<wxPoint> _zoom_point;
+       std::shared_ptr<TimelineContentView> _down_view;
+       dcpomatic::DCPTime _down_view_position;
+       bool _first_move;
+       ContentMenu _menu;
+       bool _snap;
+       std::list<dcpomatic::DCPTime> _start_snaps;
+       std::list<dcpomatic::DCPTime> _end_snaps;
+       Tool _tool;
+       int _x_scroll_rate;
+       int _y_scroll_rate;
+       int _pixels_per_track;
+       bool _first_resize;
+       wxTimer _timer;
+       boost::optional<int> _last_mouse_wheel_x;
+       boost::optional<double> _last_mouse_wheel_time;
+
+       static int const _minimum_pixels_per_track;
+
+       boost::signals2::scoped_connection _film_changed_connection;
+       boost::signals2::scoped_connection _film_content_change_connection;
+};
diff --git a/src/wx/content_timeline_atmos_view.cc b/src/wx/content_timeline_atmos_view.cc
new file mode 100644 (file)
index 0000000..2352ba5
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (C) 2016-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 "colours.h"
+#include "content_timeline_atmos_view.h"
+
+
+using std::shared_ptr;
+
+
+/** @class ContentTimelineContentView
+ *  @brief Content timeline view for AtmosContent.
+ */
+
+ContentTimelineAtmosView::ContentTimelineAtmosView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+
+wxColour
+ContentTimelineAtmosView::background_colour() const
+{
+       return ATMOS_CONTENT_COLOUR;
+}
+
+
+wxColour
+ContentTimelineAtmosView::foreground_colour() const
+{
+       return wxColour (0, 0, 0, 255);
+}
diff --git a/src/wx/content_timeline_atmos_view.h b/src/wx/content_timeline_atmos_view.h
new file mode 100644 (file)
index 0000000..d5fea92
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2016-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 "timeline_content_view.h"
+
+
+/** @class ContentTimelineAtmosContentView
+ *  @brief Content timeline view for AtmosContent.
+ */
+class ContentTimelineAtmosView : public TimelineContentView
+{
+public:
+       ContentTimelineAtmosView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override {
+               return true;
+       }
+
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+};
diff --git a/src/wx/content_timeline_audio_view.cc b/src/wx/content_timeline_audio_view.cc
new file mode 100644 (file)
index 0000000..600e390
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+    Copyright (C) 2013-2018 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 "colours.h"
+#include "content_timeline_audio_view.h"
+#include "wx_util.h"
+#include "lib/audio_content.h"
+#include "lib/util.h"
+
+
+using std::dynamic_pointer_cast;
+using std::list;
+using std::shared_ptr;
+
+
+/** @class ContentTimelineAudioView
+ *  @brief Content timeline view for AudioContent.
+ */
+
+
+ContentTimelineAudioView::ContentTimelineAudioView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+wxColour
+ContentTimelineAudioView::background_colour () const
+{
+       return AUDIO_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineAudioView::foreground_colour () const
+{
+       return wxColour (0, 0, 0, 255);
+}
+
+wxString
+ContentTimelineAudioView::label () const
+{
+       wxString s = TimelineContentView::label ();
+       shared_ptr<AudioContent> ac = content()->audio;
+       DCPOMATIC_ASSERT (ac);
+
+       if (ac->gain() > 0.01) {
+               s += wxString::Format (" +%.1fdB", ac->gain());
+       } else if (ac->gain() < -0.01) {
+               s += wxString::Format (" %.1fdB", ac->gain());
+       }
+
+       if (ac->delay() > 0) {
+               s += wxString::Format (_(" delayed by %dms"), ac->delay());
+       } else if (ac->delay() < 0) {
+               s += wxString::Format (_(" advanced by %dms"), -ac->delay());
+       }
+
+       list<int> mapped = ac->mapping().mapped_output_channels();
+       if (!mapped.empty ()) {
+               s += wxString::FromUTF8(" â†’ ");
+               for (auto i: mapped) {
+                       s += std_to_wx(short_audio_channel_name(i)) + ", ";
+               }
+               s = s.Left(s.Length() - 2);
+       }
+
+       return s;
+}
diff --git a/src/wx/content_timeline_audio_view.h b/src/wx/content_timeline_audio_view.h
new file mode 100644 (file)
index 0000000..719c253
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2013-2015 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 "timeline_content_view.h"
+
+
+/** @class ContentTimelineAudioView
+ *  @brief Content timeline view for AudioContent.
+ */
+class ContentTimelineAudioView : public TimelineContentView
+{
+public:
+       ContentTimelineAudioView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override {
+               return true;
+       }
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+       wxString label () const override;
+};
diff --git a/src/wx/content_timeline_dialog.cc b/src/wx/content_timeline_dialog.cc
new file mode 100644 (file)
index 0000000..697e656
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+    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 "content_panel.h"
+#include "content_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
+
+
+ContentTimelineDialog::ContentTimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
+       : wxDialog (
+               cp->window(),
+               wxID_ANY,
+               _("Timeline"),
+               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
+               )
+       , _film (film)
+       , _timeline (this, cp, film, viewer)
+{
+       auto sizer = new wxBoxSizer (wxVERTICAL);
+
+       wxBitmap select(icon_path("select"), wxBITMAP_TYPE_PNG);
+       wxBitmap zoom(icon_path("zoom"), wxBITMAP_TYPE_PNG);
+       wxBitmap zoom_all(icon_path("zoom_all"), wxBITMAP_TYPE_PNG);
+       wxBitmap snap(icon_path("snap"), wxBITMAP_TYPE_PNG);
+       wxBitmap sequence(icon_path("sequence"), wxBITMAP_TYPE_PNG);
+
+       _toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
+       _toolbar->SetMargins (4, 4);
+       _toolbar->SetToolBitmapSize (wxSize(32, 32));
+       _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::SELECT), _("Select"), select, wxNullBitmap, _("Select and move content"));
+       _toolbar->AddRadioTool(static_cast<int>(ContentTimeline::ZOOM), _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
+       _toolbar->AddTool(static_cast<int>(ContentTimeline::ZOOM_ALL), _("Zoom all"), zoom_all, _("Zoom out to whole film"));
+       _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SNAP), _("Snap"), snap, wxNullBitmap, _("Snap"));
+       _toolbar->AddCheckTool(static_cast<int>(ContentTimeline::SEQUENCE), _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
+       _toolbar->Realize ();
+
+       _toolbar->Bind(wxEVT_TOOL, bind(&ContentTimelineDialog::tool_clicked, this, _1));
+
+       sizer->Add (_toolbar, 0, wxALL, 12);
+       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);
+
+       Bind(wxEVT_CHAR_HOOK, boost::bind(&ContentTimelineDialog::keypress, this, _1));
+
+        _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SNAP), _timeline.snap ());
+       film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
+
+       _film_changed_connection = film->Change.connect(bind(&ContentTimelineDialog::film_change, this, _1, _2));
+}
+
+
+void
+ContentTimelineDialog::film_change(ChangeType type, FilmProperty p)
+{
+       if (type != ChangeType::DONE) {
+               return;
+       }
+
+       auto film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       if (p == FilmProperty::SEQUENCE) {
+               _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SEQUENCE), film->sequence());
+       }
+}
+
+
+void
+ContentTimelineDialog::set_selection(ContentList selection)
+{
+       _timeline.set_selection (selection);
+}
+
+
+void
+ContentTimelineDialog::tool_clicked(wxCommandEvent& ev)
+{
+       auto t = static_cast<ContentTimeline::Tool>(ev.GetId());
+       _timeline.tool_clicked (t);
+       if (t == ContentTimeline::SNAP) {
+               _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
+       } else if (t == ContentTimeline::SEQUENCE) {
+               auto film = _film.lock ();
+               if (film) {
+                       film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
+               }
+       }
+}
+
+
+void
+ContentTimelineDialog::keypress(wxKeyEvent const& event)
+{
+       _timeline.keypress(event);
+}
diff --git a/src/wx/content_timeline_dialog.h b/src/wx/content_timeline_dialog.h
new file mode 100644 (file)
index 0000000..2babf04
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    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 "content_timeline.h"
+#include <dcp/warnings.h>
+LIBDCP_DISABLE_WARNINGS
+#include <wx/wx.h>
+LIBDCP_ENABLE_WARNINGS
+
+
+class Playlist;
+
+
+class ContentTimelineDialog : public wxDialog
+{
+public:
+       ContentTimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
+
+       void set_selection (ContentList selection);
+
+private:
+       void film_change(ChangeType type, FilmProperty);
+       void tool_clicked (wxCommandEvent& id);
+       void keypress(wxKeyEvent const& event);
+
+       std::weak_ptr<Film> _film;
+       ContentTimeline _timeline;
+       wxToolBar* _toolbar;
+       boost::signals2::scoped_connection _film_changed_connection;
+};
diff --git a/src/wx/content_timeline_text_view.cc b/src/wx/content_timeline_text_view.cc
new file mode 100644 (file)
index 0000000..f22f478
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (C) 2013-2018 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 "colours.h"
+#include "content_timeline_text_view.h"
+#include "lib/text_content.h"
+#include "lib/content.h"
+
+
+using std::shared_ptr;
+
+
+ContentTimelineTextView::ContentTimelineTextView(ContentTimeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
+       : TimelineContentView (tl, c)
+       , _caption (caption)
+{
+
+}
+
+wxColour
+ContentTimelineTextView::background_colour() const
+{
+       if (!active ()) {
+               return wxColour (210, 210, 210, 128);
+       }
+
+       return TEXT_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineTextView::foreground_colour() const
+{
+       if (!active ()) {
+               return wxColour (180, 180, 180, 128);
+       }
+
+       return wxColour (0, 0, 0, 255);
+}
+
+bool
+ContentTimelineTextView::active() const
+{
+       return _caption->use();
+}
diff --git a/src/wx/content_timeline_text_view.h b/src/wx/content_timeline_text_view.h
new file mode 100644 (file)
index 0000000..8d37ad4
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    Copyright (C) 2013-2016 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 "timeline_content_view.h"
+
+
+class TextContent;
+
+
+/** @class ContentTimelineTextView
+ *  @brief Content timeline view for TextContent.
+ */
+class ContentTimelineTextView : public TimelineContentView
+{
+public:
+       ContentTimelineTextView(ContentTimeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
+
+private:
+       bool active () const override;
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+
+       std::shared_ptr<TextContent> _caption;
+};
diff --git a/src/wx/content_timeline_video_view.cc b/src/wx/content_timeline_video_view.cc
new file mode 100644 (file)
index 0000000..d907627
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    Copyright (C) 2013-2019 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 "colours.h"
+#include "content_timeline_video_view.h"
+#include "lib/image_content.h"
+#include "lib/video_content.h"
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+
+
+ContentTimelineVideoView::ContentTimelineVideoView(ContentTimeline& tl, shared_ptr<Content> c)
+       : TimelineContentView (tl, c)
+{
+
+}
+
+wxColour
+ContentTimelineVideoView::background_colour() const
+{
+       if (!active()) {
+               return wxColour (210, 210, 210, 128);
+       }
+
+       return VIDEO_CONTENT_COLOUR;
+}
+
+wxColour
+ContentTimelineVideoView::foreground_colour() const
+{
+       if (!active()) {
+               return wxColour (180, 180, 180, 128);
+       }
+
+       return wxColour (0, 0, 0, 255);
+}
+
+bool
+ContentTimelineVideoView::active() const
+{
+       auto c = _content.lock();
+       DCPOMATIC_ASSERT (c);
+       return c->video && c->video->use();
+}
diff --git a/src/wx/content_timeline_video_view.h b/src/wx/content_timeline_video_view.h
new file mode 100644 (file)
index 0000000..940a1a3
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+    Copyright (C) 2013-2015 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 "timeline_content_view.h"
+
+
+/** @class ContentTimelineVideoView
+ *  @brief Content timeline view for VideoContent.
+ */
+class ContentTimelineVideoView : public TimelineContentView
+{
+public:
+       ContentTimelineVideoView(ContentTimeline& tl, std::shared_ptr<Content> c);
+
+private:
+       bool active () const override;
+       wxColour background_colour () const override;
+       wxColour foreground_colour () const override;
+};
diff --git a/src/wx/content_timeline_view.cc b/src/wx/content_timeline_view.cc
new file mode 100644 (file)
index 0000000..127e440
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    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 "content_timeline.h"
+#include "content_timeline_view.h"
+
+
+using std::list;
+using namespace dcpomatic;
+
+
+ContentTimelineView::ContentTimelineView(ContentTimeline& timeline)
+       : TimelineView(timeline)
+{
+
+}
+
+int
+ContentTimelineView::y_pos(int t) const
+{
+       return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
+}
+
+
diff --git a/src/wx/content_timeline_view.h b/src/wx/content_timeline_view.h
new file mode 100644 (file)
index 0000000..450d19d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    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/>.
+
+*/
+
+
+#ifndef DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+#define DCPOMATIC_CONTENT_TIMELINE_VIEW_H
+
+
+#include "timeline_view.h"
+#include "lib/rect.h"
+#include "lib/dcpomatic_time.h"
+
+
+class wxGraphicsContext;
+class ContentTimeline;
+
+
+/** @class ContentTimelineView
+ *  @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
+ */
+class ContentTimelineView : public TimelineView<ContentTimeline>
+{
+public:
+       explicit ContentTimelineView(ContentTimeline& t);
+       virtual ~ContentTimelineView () = default;
+
+       void paint(wxGraphicsContext* gc, std::list<dcpomatic::Rect<int>> overlaps)
+       {
+               _last_paint_bbox = bbox();
+               do_paint(gc, overlaps);
+       }
+
+protected:
+       virtual void do_paint(wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+
+       int y_pos(int t) const;
+};
+
+
+typedef std::vector<std::shared_ptr<ContentTimelineView>> ContentTimelineViewList;
+
+
+#endif
index d81e7bf9522cb9dd812789b7f7459230b3a2a29b..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"
@@ -106,18 +107,12 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
         auto size = dc.GetTextExtent (wxT ("GGGGGGGG..."));
         size.SetHeight (-1);
 
-       _reels_label = create_label (_panel, _("Reels"), true);
-       _reel_type = new Choice(_panel);
-
-       _reel_length_label = create_label (_panel, _("Reel length"), true);
-       _reel_length = new SpinCtrl (_panel, DCPOMATIC_SPIN_CTRL_WIDTH);
-       _reel_length_gb_label = create_label (_panel, _("GB"), false);
-
        _standard_label = create_label (_panel, _("Standard"), true);
        _standard = new Choice(_panel);
 
        _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);
@@ -130,23 +125,15 @@ DCPPanel::DCPPanel(wxNotebook* n, shared_ptr<Film> film, FilmViewer& viewer)
        _copy_isdcf_name_button->Bind(wxEVT_BUTTON,   boost::bind(&DCPPanel::copy_isdcf_name_button_clicked, this));
        _dcp_content_type->Bind      (wxEVT_CHOICE,   boost::bind(&DCPPanel::dcp_content_type_changed, this));
        _encrypted->bind(&DCPPanel::encrypted_toggled, this);
-       _reel_type->Bind             (wxEVT_CHOICE,   boost::bind(&DCPPanel::reel_type_changed, this));
-       _reel_length->Bind           (wxEVT_SPINCTRL, boost::bind(&DCPPanel::reel_length_changed, this));
        _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());
        }
 
-       _reel_type->add(_("Single reel"));
-       _reel_type->add(_("Split by video content"));
-       /// TRANSLATORS: translate the word "Custom" here; do not include the "Reel|" prefix
-       _reel_type->add(S_("Reel|Custom"));
-       _reel_type->SetToolTip(_("How the DCP should be split into parts internally.  If in doubt, choose 'Single reel'"));
-
-       _reel_length->SetRange (1, 64);
-
        add_standards();
        _standard->SetToolTip(_("The standard that the DCP should use.  Interop is older, and SMPTE is the newer (current) standard.  If in doubt, choose 'SMPTE'"));
 
@@ -241,19 +228,6 @@ DCPPanel::add_to_grid ()
        _grid->Add (_encrypted, wxGBPosition(r, 0), wxGBSpan(1, 2));
        ++r;
 
-       add_label_to_sizer (_grid, _reels_label, true, wxGBPosition(r, 0));
-       _grid->Add (_reel_type, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
-       ++r;
-
-       add_label_to_sizer (_grid, _reel_length_label, true, wxGBPosition(r, 0));
-       {
-               auto s = new wxBoxSizer (wxHORIZONTAL);
-               s->Add (_reel_length);
-               add_label_to_sizer (s, _reel_length_gb_label, false, 0, wxLEFT | wxALIGN_CENTER_VERTICAL);
-               _grid->Add (s, wxGBPosition(r, 1));
-       }
-       ++r;
-
        add_label_to_sizer (_grid, _standard_label, true, wxGBPosition(r, 0));
        _grid->Add (_standard, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL);
        ++r;
@@ -261,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;
 }
@@ -373,6 +348,14 @@ DCPPanel::metadata_clicked ()
 }
 
 
+void
+DCPPanel::reels_clicked()
+{
+       _dcp_timeline.reset(_panel, _film);
+       _dcp_timeline->Show();
+}
+
+
 void
 DCPPanel::film_changed(FilmProperty p)
 {
@@ -474,13 +457,6 @@ DCPPanel::film_changed(FilmProperty p)
                setup_audio_channels_choice (_audio_channels, minimum_allowed_audio_channels());
                film_changed (FilmProperty::AUDIO_CHANNELS);
                break;
-       case FilmProperty::REEL_TYPE:
-               checked_set (_reel_type, static_cast<int>(_film->reel_type()));
-               _reel_length->Enable (_film->reel_type() == ReelType::BY_LENGTH);
-               break;
-       case FilmProperty::REEL_LENGTH:
-               checked_set (_reel_length, _film->reel_length() / 1000000000LL);
-               break;
        case FilmProperty::CONTENT:
                setup_dcp_name ();
                setup_sensitivity ();
@@ -666,8 +642,6 @@ DCPPanel::setup_sensitivity ()
        _audio_language->Enable         (_enable_audio_language->GetValue());
        _edit_audio_language->Enable    (_enable_audio_language->GetValue());
        _encrypted->Enable              (_generally_sensitive);
-       _reel_type->Enable              (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->references_dcp_audio());
-       _reel_length->Enable            (_generally_sensitive && _film && _film->reel_type() == ReelType::BY_LENGTH);
        _markers->Enable                (_generally_sensitive && _film && !_film->interop());
        _metadata->Enable               (_generally_sensitive);
        _frame_rate_choice->Enable      (_generally_sensitive && _film && !_film->references_dcp_video() && !_film->contains_atmos_content());
@@ -1032,28 +1006,6 @@ DCPPanel::show_audio_clicked ()
 }
 
 
-void
-DCPPanel::reel_type_changed ()
-{
-       if (!_film || !_reel_type->get()) {
-               return;
-       }
-
-       _film->set_reel_type(static_cast<ReelType>(*_reel_type->get()));
-}
-
-
-void
-DCPPanel::reel_length_changed ()
-{
-       if (!_film) {
-               return;
-       }
-
-       _film->set_reel_length (_reel_length->GetValue() * 1000000000LL);
-}
-
-
 void
 DCPPanel::add_audio_processors ()
 {
index 849fe185cf836bc588428c9308dd928db014226b..6c97a41c31c5bea31783616e163e0c6532a1ed75 100644 (file)
@@ -39,6 +39,7 @@ class wxGridBagSizer;
 
 class AudioDialog;
 class Choice;
+class DCPTimelineDialog;
 class Film;
 class FilmViewer;
 class InteropMetadataDialog;
@@ -82,10 +83,9 @@ private:
        void encrypted_toggled ();
        void audio_processor_changed ();
        void show_audio_clicked ();
-       void reel_type_changed ();
-       void reel_length_changed ();
        void markers_clicked ();
        void metadata_clicked ();
+       void reels_clicked();
        void reencode_j2k_changed ();
        void enable_audio_language_toggled ();
        void edit_audio_language_clicked ();
@@ -153,19 +153,16 @@ private:
        wxStaticText* _standard_label;
        Choice* _standard;
        CheckBox* _encrypted;
-       wxStaticText* _reels_label;
-       Choice* _reel_type;
-       wxStaticText* _reel_length_label;
-       wxStaticText* _reel_length_gb_label;
-       wxSpinCtrl* _reel_length;
        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 1e6a1930d95f6c358015eb643ffa37b471a6d7c8..64fe8719038a62e5415c017572fa0c0e9644c4d6 100644 (file)
@@ -114,7 +114,7 @@ void
 TimecodeBase::changed ()
 {
        if (_set_button && !_ignore_changed) {
-               _set_button->Enable (true);
+               _set_button->Enable(valid());
        }
 }
 
index 6c5d8ae231cd0d6679e1b56cd2e98b0da34b89d6..22899ddc9ba49bd07f40f8d1acb12229b7d47ea1 100644 (file)
@@ -50,6 +50,7 @@ public:
 protected:
        void changed ();
        void set_clicked ();
+       virtual bool valid() const = 0;
 
        wxSizer* _sizer;
        wxPanel* _editable;
@@ -96,6 +97,11 @@ public:
                _frames->SetHint (std_to_wx(dcp::raw_convert<std::string>(hmsf.f)));
        }
 
+       void set_maximum(dcpomatic::HMSF maximum)
+       {
+               _maximum = std::move(maximum);
+       }
+
        dcpomatic::HMSF get () const
        {
                auto value_or_hint = [](wxTextCtrl const * t) {
@@ -116,6 +122,13 @@ public:
        {
                return T(get(), fps);
        }
+
+private:
+       bool valid() const override {
+               return !_maximum || get() <= *_maximum;
+       }
+
+       boost::optional<dcpomatic::HMSF> _maximum;
 };
 
 #endif
index 38e9de4ee46318ca73bfff98a29023c64bac5047..329f4ef005ce7d3936c448bf459b6f44c5c1c51a 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 */
 
-#include "content_panel.h"
-#include "film_editor.h"
-#include "film_viewer.h"
-#include "timeline.h"
-#include "timeline_atmos_content_view.h"
-#include "timeline_audio_content_view.h"
-#include "timeline_labels_view.h"
-#include "timeline_reels_view.h"
-#include "timeline_text_content_view.h"
-#include "timeline_time_axis_view.h"
-#include "timeline_video_content_view.h"
-#include "wx_util.h"
-#include "lib/atmos_mxf_content.h"
-#include "lib/audio_content.h"
-#include "lib/film.h"
-#include "lib/image_content.h"
-#include "lib/playlist.h"
-#include "lib/text_content.h"
-#include "lib/timer.h"
-#include "lib/video_content.h"
-#include <dcp/scope_guard.h>
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/graphics.h>
-LIBDCP_ENABLE_WARNINGS
-#include <iterator>
-#include <list>
-
 
-using std::abs;
-using std::dynamic_pointer_cast;
-using std::list;
-using std::make_shared;
-using std::max;
-using std::min;
-using std::shared_ptr;
-using std::weak_ptr;
-using boost::bind;
-using boost::optional;
-using namespace dcpomatic;
-#if BOOST_VERSION >= 106100
-using namespace boost::placeholders;
-#endif
+#include "timeline.h"
 
 
 /* 3 hours in 640 pixels */
-double const Timeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
-int const Timeline::_minimum_pixels_per_track = 16;
-
-
-Timeline::Timeline(wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
-       : wxPanel (parent, wxID_ANY)
-       , _labels_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
-       , _main_canvas (new wxScrolledCanvas (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE))
-       , _content_panel (cp)
-       , _film (film)
-       , _viewer (viewer)
-       , _time_axis_view (new TimelineTimeAxisView (*this, 64))
-       , _reels_view (new TimelineReelsView (*this, 32))
-       , _labels_view (new TimelineLabelsView (*this))
-       , _tracks (0)
-       , _left_down (false)
-       , _down_view_position (0)
-       , _first_move (false)
-       , _menu (this, viewer)
-       , _snap (true)
-       , _tool (SELECT)
-       , _x_scroll_rate (16)
-       , _y_scroll_rate (16)
-       , _pixels_per_track (48)
-       , _first_resize (true)
-       , _timer (this)
-{
-#ifndef __WXOSX__
-       _labels_canvas->SetDoubleBuffered (true);
-       _main_canvas->SetDoubleBuffered (true);
-#endif
-
-       auto sizer = new wxBoxSizer (wxHORIZONTAL);
-       sizer->Add (_labels_canvas, 0, wxEXPAND);
-       _labels_canvas->SetMinSize (wxSize (_labels_view->bbox().width, -1));
-       sizer->Add (_main_canvas, 1, wxEXPAND);
-       SetSizer (sizer);
-
-       _labels_canvas->Bind (wxEVT_PAINT,      boost::bind (&Timeline::paint_labels, this));
-       _main_canvas->Bind   (wxEVT_PAINT,      boost::bind (&Timeline::paint_main,   this));
-       _main_canvas->Bind   (wxEVT_LEFT_DOWN,  boost::bind (&Timeline::left_down,    this, _1));
-       _main_canvas->Bind   (wxEVT_LEFT_UP,    boost::bind (&Timeline::left_up,      this, _1));
-       _main_canvas->Bind   (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down,   this, _1));
-       _main_canvas->Bind   (wxEVT_MOTION,     boost::bind (&Timeline::mouse_moved,  this, _1));
-       _main_canvas->Bind   (wxEVT_SIZE,       boost::bind (&Timeline::resized,      this));
-       _main_canvas->Bind   (wxEVT_MOUSEWHEEL, boost::bind(&Timeline::mouse_wheel_turned, this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_TOP,        boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_BOTTOM,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEUP,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_LINEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEUP,     boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_PAGEDOWN,   boost::bind (&Timeline::scrolled,     this, _1));
-       _main_canvas->Bind   (wxEVT_SCROLLWIN_THUMBTRACK, boost::bind (&Timeline::scrolled,     this, _1));
-
-       film_change(ChangeType::DONE, FilmProperty::CONTENT);
-
-       SetMinSize (wxSize (640, 4 * pixels_per_track() + 96));
-
-       _film_changed_connection = film->Change.connect (bind (&Timeline::film_change, this, _1, _2));
-       _film_content_change_connection = film->ContentChange.connect (bind (&Timeline::film_content_change, this, _1, _3, _4));
-
-       Bind (wxEVT_TIMER, boost::bind(&Timeline::update_playhead, this));
-       _timer.Start (200, wxTIMER_CONTINUOUS);
-
-       setup_scrollbars ();
-       _labels_canvas->ShowScrollbars (wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
-}
-
-
-void
-Timeline::mouse_wheel_turned(wxMouseEvent& event)
-{
-       auto const rotation = event.GetWheelRotation();
-
-       if (event.ControlDown()) {
-               /* On my mouse one click of the scroll wheel is 120, and it's -ve when
-                * scrolling the wheel towards me.
-                */
-               auto const scale = rotation > 0 ?
-                       (1.0 / (rotation / 90.0)) :
-                       (-rotation / 90.0);
-
-               int before_start_x;
-               int before_start_y;
-               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
-
-               auto const before_pps = _pixels_per_second.get_value_or(1);
-               auto const before_pos = _last_mouse_wheel_x && *_last_mouse_wheel_x == event.GetX() ?
-                       *_last_mouse_wheel_time :
-                       (before_start_x * _x_scroll_rate + event.GetX()) / before_pps;
-
-               set_pixels_per_second(before_pps * scale);
-               setup_scrollbars();
-
-               auto after_left = std::max(0.0, before_pos * _pixels_per_second.get_value_or(1) - event.GetX());
-               _main_canvas->Scroll(after_left / _x_scroll_rate, before_start_y);
-               _labels_canvas->Scroll(0, before_start_y);
-               Refresh();
-
-               if (!_last_mouse_wheel_x || *_last_mouse_wheel_x != event.GetX()) {
-                       _last_mouse_wheel_x = event.GetX();
-                       _last_mouse_wheel_time = before_pos;
-               }
-       } else if (event.ShiftDown()) {
-               int before_start_x;
-               int before_start_y;
-               _main_canvas->GetViewStart(&before_start_x, &before_start_y);
-               auto const width = _main_canvas->GetSize().GetWidth();
-               _main_canvas->Scroll(std::max(0.0, before_start_x - rotation * 100.0 / width), before_start_y);
-       }
-}
-
-
-void
-Timeline::update_playhead ()
-{
-       Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_second (double pps)
-{
-       _pixels_per_second = max (_minimum_pixels_per_second, pps);
-}
-
-
-void
-Timeline::paint_labels ()
-{
-       wxPaintDC dc (_labels_canvas);
-
-       auto film = _film.lock();
-       if (film->content().empty()) {
-               return;
-       }
-
-       auto gc = wxGraphicsContext::Create (dc);
-       if (!gc) {
-               return;
-       }
-
-       dcp::ScopeGuard sg = [gc]() { delete gc; };
-
-       int vsx, vsy;
-       _labels_canvas->GetViewStart (&vsx, &vsy);
-       gc->Translate (-vsx * _x_scroll_rate, -vsy * _y_scroll_rate + tracks_y_offset());
-
-       _labels_view->paint (gc, {});
-}
-
-
-void
-Timeline::paint_main ()
-{
-       wxPaintDC dc (_main_canvas);
-       dc.Clear();
-
-       auto film = _film.lock();
-       if (film->content().empty()) {
-               return;
-       }
-
-       _main_canvas->DoPrepareDC (dc);
-
-       auto gc = wxGraphicsContext::Create (dc);
-       if (!gc) {
-               return;
-       }
-
-       dcp::ScopeGuard sg = [gc]() { delete gc; };
-
-       gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
-
-       for (auto i: _views) {
-
-               auto ic = dynamic_pointer_cast<TimelineContentView> (i);
-
-               /* Find areas of overlap with other content views, so that we can plot them */
-               list<dcpomatic::Rect<int>> overlaps;
-               for (auto j: _views) {
-                       auto jc = dynamic_pointer_cast<TimelineContentView> (j);
-                       /* No overlap with non-content views, views on different tracks, audio views or non-active views */
-                       if (!ic || !jc || i == j || ic->track() != jc->track() || ic->track().get_value_or(2) >= 2 || !ic->active() || !jc->active()) {
-                               continue;
-                       }
-
-                       auto r = j->bbox().intersection(i->bbox());
-                       if (r) {
-                               overlaps.push_back (r.get ());
-                       }
-               }
-
-               i->paint (gc, overlaps);
-       }
-
-       if (_zoom_point) {
-               gc->SetPen(gui_is_dark() ? *wxWHITE_PEN : *wxBLACK_PEN);
-               gc->SetBrush (*wxTRANSPARENT_BRUSH);
-               gc->DrawRectangle (
-                       min (_down_point.x, _zoom_point->x),
-                       min (_down_point.y, _zoom_point->y),
-                       abs (_down_point.x - _zoom_point->x),
-                       abs (_down_point.y - _zoom_point->y)
-                       );
-       }
-
-       /* Playhead */
-
-       gc->SetPen (*wxRED_PEN);
-       auto path = gc->CreatePath ();
-       double const ph = _viewer.position().seconds() * pixels_per_second().get_value_or(0);
-       path.MoveToPoint (ph, 0);
-       path.AddLineToPoint (ph, pixels_per_track() * _tracks + 32);
-       gc->StrokePath (path);
-}
-
-
-void
-Timeline::film_change(ChangeType type, FilmProperty p)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       if (p == FilmProperty::CONTENT || p == FilmProperty::REEL_TYPE || p == FilmProperty::REEL_LENGTH) {
-               ensure_ui_thread ();
-               recreate_views ();
-       } else if (p == FilmProperty::CONTENT_ORDER) {
-               Refresh ();
-       }
-}
-
-
-void
-Timeline::recreate_views ()
-{
-       auto film = _film.lock ();
-       if (!film) {
-               return;
-       }
-
-       _views.clear ();
-       _views.push_back (_time_axis_view);
-       _views.push_back (_reels_view);
-
-       for (auto i: film->content ()) {
-               if (i->video) {
-                       _views.push_back (make_shared<TimelineVideoContentView>(*this, i));
-               }
-
-               if (i->audio && !i->audio->mapping().mapped_output_channels().empty ()) {
-                       _views.push_back (make_shared<TimelineAudioContentView>(*this, i));
-               }
-
-               for (auto j: i->text) {
-                       _views.push_back (make_shared<TimelineTextContentView>(*this, i, j));
-               }
-
-               if (i->atmos) {
-                       _views.push_back (make_shared<TimelineAtmosContentView>(*this, i));
-               }
-       }
-
-       assign_tracks ();
-       setup_scrollbars ();
-       Refresh ();
-}
-
-
-void
-Timeline::film_content_change (ChangeType type, int property, bool frequent)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       ensure_ui_thread ();
-
-       if (property == AudioContentProperty::STREAMS || property == VideoContentProperty::FRAME_TYPE) {
-               recreate_views ();
-       } else if (property == ContentProperty::POSITION || property == ContentProperty::LENGTH) {
-               _reels_view->force_redraw ();
-       } else if (!frequent) {
-               setup_scrollbars ();
-               Refresh ();
-       }
-}
-
-
-template <class T>
-int
-place (shared_ptr<const Film> film, TimelineViewList& views, int& tracks)
-{
-       int const base = tracks;
-
-       for (auto i: views) {
-               if (!dynamic_pointer_cast<T>(i)) {
-                       continue;
-               }
-
-               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
-               DCPOMATIC_ASSERT(cv);
-
-               int t = base;
-
-               auto content = cv->content();
-               DCPTimePeriod const content_period = content->period(film);
-
-               while (true) {
-                       auto j = views.begin();
-                       while (j != views.end()) {
-                               auto test = dynamic_pointer_cast<T> (*j);
-                               if (!test) {
-                                       ++j;
-                                       continue;
-                               }
-
-                               auto test_content = test->content();
-                               if (
-                                       test->track() && test->track().get() == t &&
-                                       content_period.overlap(test_content->period(film))
-                                  ) {
-                                       /* we have an overlap on track `t' */
-                                       ++t;
-                                       break;
-                               }
-
-                               ++j;
-                       }
-
-                       if (j == views.end ()) {
-                               /* no overlap on `t' */
-                               break;
-                       }
-               }
-
-               cv->set_track (t);
-               tracks = max (tracks, t + 1);
-       }
-
-       return tracks - base;
-}
-
-
-/** Compare the mapped output channels of two TimelineViews, so we can into
- *  order of first mapped DCP channel.
- */
-struct AudioMappingComparator {
-       bool operator()(shared_ptr<TimelineView> a, shared_ptr<TimelineView> b) {
-               int la = -1;
-               auto cva = dynamic_pointer_cast<TimelineAudioContentView>(a);
-               if (cva) {
-                       auto oc = cva->content()->audio->mapping().mapped_output_channels();
-                       la = *min_element(boost::begin(oc), boost::end(oc));
-               }
-               int lb = -1;
-               auto cvb = dynamic_pointer_cast<TimelineAudioContentView>(b);
-               if (cvb) {
-                       auto oc = cvb->content()->audio->mapping().mapped_output_channels();
-                       lb = *min_element(boost::begin(oc), boost::end(oc));
-               }
-               return la < lb;
-       }
-};
-
-
-void
-Timeline::assign_tracks ()
-{
-       /* Tracks are:
-          Video 1
-          Video 2
-          Video N
-          Text 1
-          Text 2
-          Text N
-          Atmos
-          Audio 1
-          Audio 2
-          Audio N
-       */
-
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-
-       _tracks = 0;
-
-       for (auto i: _views) {
-               auto c = dynamic_pointer_cast<TimelineContentView>(i);
-               if (c) {
-                       c->unset_track ();
-               }
-       }
-
-       int const video_tracks = place<TimelineVideoContentView> (film, _views, _tracks);
-       int const text_tracks = place<TimelineTextContentView> (film, _views, _tracks);
-
-       /* Atmos */
-
-       bool have_atmos = false;
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineAtmosContentView>(i);
-               if (cv) {
-                       cv->set_track (_tracks);
-                       have_atmos = true;
-               }
-       }
-
-       if (have_atmos) {
-               ++_tracks;
-       }
-
-       /* Audio.  We're sorting the views so that we get the audio views in order of increasing
-          DCP channel index.
-       */
-
-       auto views = _views;
-       sort(views.begin(), views.end(), AudioMappingComparator());
-       int const audio_tracks = place<TimelineAudioContentView> (film, views, _tracks);
-
-       _labels_view->set_video_tracks (video_tracks);
-       _labels_view->set_audio_tracks (audio_tracks);
-       _labels_view->set_text_tracks (text_tracks);
-       _labels_view->set_atmos (have_atmos);
-
-       _time_axis_view->set_y (tracks());
-       _reels_view->set_y (8);
-}
-
-
-int
-Timeline::tracks () const
-{
-       return _tracks;
-}
-
-
-void
-Timeline::setup_scrollbars ()
-{
-       auto film = _film.lock ();
-       if (!film || !_pixels_per_second) {
-               return;
-       }
-
-       int const h = tracks() * pixels_per_track() + tracks_y_offset() + _time_axis_view->bbox().height;
-
-       _labels_canvas->SetVirtualSize (_labels_view->bbox().width, h);
-       _labels_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
-       _main_canvas->SetVirtualSize (*_pixels_per_second * film->length().seconds(), h);
-       _main_canvas->SetScrollRate (_x_scroll_rate, _y_scroll_rate);
-}
-
-
-shared_ptr<TimelineView>
-Timeline::event_to_view (wxMouseEvent& ev)
-{
-       /* Search backwards through views so that we find the uppermost one first */
-       auto i = _views.rbegin();
-
-       int vsx, vsy;
-       _main_canvas->GetViewStart (&vsx, &vsy);
-       Position<int> const p (ev.GetX() + vsx * _x_scroll_rate, ev.GetY() + vsy * _y_scroll_rate);
-
-       while (i != _views.rend() && !(*i)->bbox().contains (p)) {
-               ++i;
-       }
-
-       if (i == _views.rend ()) {
-               return {};
-       }
-
-       return *i;
-}
-
-
-void
-Timeline::left_down (wxMouseEvent& ev)
-{
-       _left_down = true;
-       _down_point = ev.GetPosition ();
-
-       switch (_tool) {
-       case SELECT:
-               left_down_select (ev);
-               break;
-       case ZOOM:
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               /* Nothing to do */
-               break;
-       }
-}
-
-
-void
-Timeline::left_down_select (wxMouseEvent& ev)
-{
-       auto view = event_to_view (ev);
-       auto content_view = dynamic_pointer_cast<TimelineContentView>(view);
-
-       _down_view.reset ();
+double constexpr minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
 
-       if (content_view) {
-               _down_view = content_view;
-               _down_view_position = content_view->content()->position ();
-       }
 
-       if (dynamic_pointer_cast<TimelineTimeAxisView>(view)) {
-               int vsx, vsy;
-               _main_canvas->GetViewStart(&vsx, &vsy);
-               _viewer.seek(DCPTime::from_seconds((ev.GetPosition().x + vsx * _x_scroll_rate) / _pixels_per_second.get_value_or(1)), true);
-       }
-
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (!cv) {
-                       continue;
-               }
-
-               if (!ev.ShiftDown ()) {
-                       cv->set_selected (view == i);
-               }
-       }
-
-       if (content_view && ev.ShiftDown ()) {
-               content_view->set_selected (!content_view->selected ());
-       }
-
-       _first_move = false;
-
-       if (_down_view) {
-               /* Pre-compute the points that we might snap to */
-               for (auto i: _views) {
-                       auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-                       if (!cv || cv == _down_view || cv->content() == _down_view->content()) {
-                               continue;
-                       }
-
-                       auto film = _film.lock ();
-                       DCPOMATIC_ASSERT (film);
-
-                       _start_snaps.push_back (cv->content()->position());
-                       _end_snaps.push_back (cv->content()->position());
-                       _start_snaps.push_back (cv->content()->end(film));
-                       _end_snaps.push_back (cv->content()->end(film));
-
-                       for (auto i: cv->content()->reel_split_points(film)) {
-                               _start_snaps.push_back (i);
-                       }
-               }
-
-               /* Tell everyone that things might change frequently during the drag */
-               _down_view->content()->set_change_signals_frequent (true);
-       }
-}
-
-
-void
-Timeline::left_up (wxMouseEvent& ev)
-{
-       _left_down = false;
-
-       switch (_tool) {
-       case SELECT:
-               left_up_select (ev);
-               break;
-       case ZOOM:
-               left_up_zoom (ev);
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::left_up_select (wxMouseEvent& ev)
+Timeline::Timeline(wxWindow* parent)
+       : wxPanel(parent, wxID_ANY)
 {
-       if (_down_view) {
-               _down_view->content()->set_change_signals_frequent (false);
-       }
 
-       _content_panel->set_selection (selected_content ());
-       /* Since we may have just set change signals back to `not-frequent', we have to
-          make sure this position change is signalled, even if the position value has
-          not changed since the last time it was set (with frequent=true).  This is
-          a bit of a hack.
-       */
-       set_position_from_event (ev, true);
-
-       /* Clear up up the stuff we don't do during drag */
-       assign_tracks ();
-       setup_scrollbars ();
-       Refresh ();
-
-       _start_snaps.clear ();
-       _end_snaps.clear ();
-}
-
-
-void
-Timeline::left_up_zoom (wxMouseEvent& ev)
-{
-       _zoom_point = ev.GetPosition ();
-
-       int vsx, vsy;
-       _main_canvas->GetViewStart (&vsx, &vsy);
-
-       wxPoint top_left(min(_down_point.x, _zoom_point->x), min(_down_point.y, _zoom_point->y));
-       wxPoint bottom_right(max(_down_point.x, _zoom_point->x), max(_down_point.y, _zoom_point->y));
-
-       if ((bottom_right.x - top_left.x) < 8 || (bottom_right.y - top_left.y) < 8) {
-               /* Very small zoom rectangle: we assume it wasn't intentional */
-               _zoom_point = optional<wxPoint> ();
-               Refresh ();
-               return;
-       }
-
-       auto const time_left = DCPTime::from_seconds((top_left.x + vsx) / *_pixels_per_second);
-       auto const time_right = DCPTime::from_seconds((bottom_right.x + vsx) / *_pixels_per_second);
-       set_pixels_per_second (double(GetSize().GetWidth()) / (time_right.seconds() - time_left.seconds()));
-
-       double const tracks_top = double(top_left.y - tracks_y_offset()) / _pixels_per_track;
-       double const tracks_bottom = double(bottom_right.y - tracks_y_offset()) / _pixels_per_track;
-       set_pixels_per_track (lrint(GetSize().GetHeight() / (tracks_bottom - tracks_top)));
-
-       setup_scrollbars ();
-       int const y = (tracks_top * _pixels_per_track + tracks_y_offset()) / _y_scroll_rate;
-       _main_canvas->Scroll (time_left.seconds() * *_pixels_per_second / _x_scroll_rate, y);
-       _labels_canvas->Scroll (0, y);
-
-       _zoom_point = optional<wxPoint> ();
-       Refresh ();
-}
-
-
-void
-Timeline::set_pixels_per_track (int h)
-{
-       _pixels_per_track = max(_minimum_pixels_per_track, h);
 }
 
 
 void
-Timeline::mouse_moved (wxMouseEvent& ev)
+Timeline::set_pixels_per_second(double pps)
 {
-       switch (_tool) {
-       case SELECT:
-               mouse_moved_select (ev);
-               break;
-       case ZOOM:
-               mouse_moved_zoom (ev);
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
+       _pixels_per_second = std::max(minimum_pixels_per_second, pps);
 }
 
 
-void
-Timeline::mouse_moved_select (wxMouseEvent& ev)
-{
-       if (!_left_down) {
-               return;
-       }
-
-       set_position_from_event (ev);
-}
-
-
-void
-Timeline::mouse_moved_zoom (wxMouseEvent& ev)
-{
-       if (!_left_down) {
-               return;
-       }
-
-       _zoom_point = ev.GetPosition ();
-       setup_scrollbars();
-       Refresh ();
-}
-
-
-void
-Timeline::right_down (wxMouseEvent& ev)
-{
-       switch (_tool) {
-       case SELECT:
-               right_down_select (ev);
-               break;
-       case ZOOM:
-               /* Zoom out */
-               set_pixels_per_second (*_pixels_per_second / 2);
-               set_pixels_per_track (_pixels_per_track / 2);
-               setup_scrollbars ();
-               Refresh ();
-               break;
-       case ZOOM_ALL:
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::right_down_select (wxMouseEvent& ev)
-{
-       auto view = event_to_view (ev);
-       auto cv = dynamic_pointer_cast<TimelineContentView> (view);
-       if (!cv) {
-               return;
-       }
-
-       if (!cv->selected ()) {
-               clear_selection ();
-               cv->set_selected (true);
-       }
-
-       _menu.popup (_film, selected_content (), selected_views (), ev.GetPosition ());
-}
-
-
-void
-Timeline::maybe_snap (DCPTime a, DCPTime b, optional<DCPTime>& nearest_distance) const
-{
-       auto const d = a - b;
-       if (!nearest_distance || d.abs() < nearest_distance.get().abs()) {
-               nearest_distance = d;
-       }
-}
-
-
-void
-Timeline::set_position_from_event (wxMouseEvent& ev, bool force_emit)
-{
-       if (!_pixels_per_second) {
-               return;
-       }
-
-       double const pps = _pixels_per_second.get ();
-
-       auto const p = ev.GetPosition();
-
-       if (!_first_move) {
-               /* We haven't moved yet; in that case, we must move the mouse some reasonable distance
-                  before the drag is considered to have started.
-               */
-               int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2));
-               if (dist < 8) {
-                       return;
-               }
-               _first_move = true;
-       }
-
-       if (!_down_view) {
-               return;
-       }
-
-       auto new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
-
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-
-       if (_snap) {
-               auto const new_end = new_position + _down_view->content()->length_after_trim(film);
-               /* Signed `distance' to nearest thing (i.e. negative is left on the timeline,
-                  positive is right).
-               */
-               optional<DCPTime> nearest_distance;
-
-               /* Find the nearest snap point */
-
-               for (auto i: _start_snaps) {
-                       maybe_snap (i, new_position, nearest_distance);
-               }
-
-               for (auto i: _end_snaps) {
-                       maybe_snap (i, new_end, nearest_distance);
-               }
-
-               if (nearest_distance) {
-                       /* Snap if it's close; `close' means within a proportion of the time on the timeline */
-                       if (nearest_distance.get().abs() < DCPTime::from_seconds ((width() / pps) / 64)) {
-                               new_position += nearest_distance.get ();
-                       }
-               }
-       }
-
-       if (new_position < DCPTime ()) {
-               new_position = DCPTime ();
-       }
-
-       _down_view->content()->set_position (film, new_position, force_emit);
-
-       film->set_sequence (false);
-}
-
-
-void
-Timeline::force_redraw (dcpomatic::Rect<int> const & r)
-{
-       _main_canvas->RefreshRect (wxRect (r.x, r.y, r.width, r.height), false);
-}
-
-
-shared_ptr<const Film>
-Timeline::film () const
-{
-       return _film.lock ();
-}
-
-
-void
-Timeline::resized ()
-{
-       if (_main_canvas->GetSize().GetWidth() > 0 && _first_resize) {
-               zoom_all ();
-               _first_resize = false;
-       }
-       setup_scrollbars ();
-}
-
-
-void
-Timeline::clear_selection ()
-{
-       for (auto i: _views) {
-               shared_ptr<TimelineContentView> cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (cv) {
-                       cv->set_selected (false);
-               }
-       }
-}
-
-
-TimelineContentViewList
-Timeline::selected_views () const
-{
-       TimelineContentViewList sel;
-
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView>(i);
-               if (cv && cv->selected()) {
-                       sel.push_back (cv);
-               }
-       }
-
-       return sel;
-}
-
-
-ContentList
-Timeline::selected_content () const
-{
-       ContentList sel;
-
-       for (auto i: selected_views()) {
-               sel.push_back(i->content());
-       }
-
-       return sel;
-}
-
-
-void
-Timeline::set_selection (ContentList selection)
-{
-       for (auto i: _views) {
-               auto cv = dynamic_pointer_cast<TimelineContentView> (i);
-               if (cv) {
-                       cv->set_selected (find (selection.begin(), selection.end(), cv->content ()) != selection.end ());
-               }
-       }
-}
-
-
-int
-Timeline::tracks_y_offset () const
-{
-       return _reels_view->bbox().height + 4;
-}
-
-
-int
-Timeline::width () const
-{
-       return _main_canvas->GetVirtualSize().GetWidth();
-}
-
-
-void
-Timeline::scrolled (wxScrollWinEvent& ev)
-{
-       if (ev.GetOrientation() == wxVERTICAL) {
-               int x, y;
-               _main_canvas->GetViewStart (&x, &y);
-               _labels_canvas->Scroll (0, y);
-       }
-       ev.Skip ();
-}
-
-
-void
-Timeline::tool_clicked (Tool t)
-{
-       switch (t) {
-       case ZOOM:
-       case SELECT:
-               _tool = t;
-               break;
-       case ZOOM_ALL:
-               zoom_all ();
-               break;
-       case SNAP:
-       case SEQUENCE:
-               break;
-       }
-}
-
-
-void
-Timeline::zoom_all ()
-{
-       auto film = _film.lock ();
-       DCPOMATIC_ASSERT (film);
-       set_pixels_per_second((_main_canvas->GetSize().GetWidth() - 32) / std::max(1.0, film->length().seconds()));
-       set_pixels_per_track((_main_canvas->GetSize().GetHeight() - tracks_y_offset() - _time_axis_view->bbox().height - 32) / std::max(1, _tracks));
-       setup_scrollbars ();
-       _main_canvas->Scroll (0, 0);
-       _labels_canvas->Scroll (0, 0);
-       Refresh ();
-}
-
-
-void
-Timeline::keypress(wxKeyEvent const& event)
-{
-       if (event.GetKeyCode() == WXK_DELETE) {
-               auto film = _film.lock();
-               DCPOMATIC_ASSERT(film);
-               film->remove_content(selected_content());
-       } else {
-               switch (event.GetRawKeyCode()) {
-               case '+':
-                       set_pixels_per_second(_pixels_per_second.get_value_or(1) * 2);
-                       setup_scrollbars();
-                       break;
-               case '-':
-                       set_pixels_per_second(_pixels_per_second.get_value_or(1) / 2);
-                       setup_scrollbars();
-                       break;
-               }
-       }
-}
-
index 621609fa7e66b94a2ae1f8cd73f2d548ce609368..cc35913b98052cf4462f73f8735e45cf02f25545 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 */
 
 
-#include "content_menu.h"
-#include "timeline_content_view.h"
-#include "lib/film_property.h"
-#include "lib/rect.h"
+#ifndef DCPOMATIC_TIMELINE_H
+#define DCPOMATIC_TIMELINE_H
+
+
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/wx.h>
 LIBDCP_ENABLE_WARNINGS
-#include <boost/signals2.hpp>
-
-
-class ContentPanel;
-class Film;
-class FilmViewer;
-class TimelineLabelsView;
-class TimelineReelsView;
-class TimelineTimeAxisView;
-class TimelineView;
+#include <boost/optional.hpp>
 
 
 class Timeline : public wxPanel
 {
 public:
-       Timeline (wxWindow *, ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
-
-       std::shared_ptr<const Film> film () const;
-
-       void force_redraw (dcpomatic::Rect<int> const &);
-
-       int width () const;
+       explicit Timeline(wxWindow* parent);
 
-       int pixels_per_track () const {
-               return _pixels_per_track;
-       }
-
-       boost::optional<double> pixels_per_second () const {
+       boost::optional<double> pixels_per_second() const {
                return _pixels_per_second;
        }
 
-       int tracks () const;
 
-       void set_snap (bool s) {
-               _snap = s;
-       }
+protected:
+       void set_pixels_per_second(double pps);
 
-       bool snap () const {
-               return _snap;
-       }
-
-       void set_selection (ContentList selection);
-
-       enum Tool {
-               SELECT,
-               ZOOM,
-               ZOOM_ALL,
-               SNAP,
-               SEQUENCE
-       };
-
-       void tool_clicked (Tool t);
-
-       int tracks_y_offset () const;
-
-       void keypress(wxKeyEvent const &);
-
-private:
-       void paint_labels ();
-       void paint_main ();
-       void left_down (wxMouseEvent &);
-       void left_down_select (wxMouseEvent &);
-       void left_up (wxMouseEvent &);
-       void left_up_select (wxMouseEvent &);
-       void left_up_zoom (wxMouseEvent &);
-       void right_down (wxMouseEvent &);
-       void right_down_select (wxMouseEvent &);
-       void mouse_moved (wxMouseEvent &);
-       void mouse_moved_select (wxMouseEvent &);
-       void mouse_moved_zoom (wxMouseEvent &);
-       void film_change(ChangeType type, FilmProperty);
-       void film_content_change (ChangeType type, int, bool frequent);
-       void resized ();
-       void assign_tracks ();
-       void set_position_from_event (wxMouseEvent& ev, bool force_emit = false);
-       void clear_selection ();
-       void recreate_views ();
-       void setup_scrollbars ();
-       void scrolled (wxScrollWinEvent& ev);
-       void set_pixels_per_second (double pps);
-       void set_pixels_per_track (int h);
-       void zoom_all ();
-       void update_playhead ();
-       void mouse_wheel_turned(wxMouseEvent& event);
-
-       std::shared_ptr<TimelineView> event_to_view (wxMouseEvent &);
-       TimelineContentViewList selected_views () const;
-       ContentList selected_content () const;
-       void maybe_snap (dcpomatic::DCPTime a, dcpomatic::DCPTime b, boost::optional<dcpomatic::DCPTime>& nearest_distance) const;
-
-       wxScrolledCanvas* _labels_canvas;
-       wxScrolledCanvas* _main_canvas;
-       ContentPanel* _content_panel;
-       std::weak_ptr<Film> _film;
-       FilmViewer& _viewer;
-       TimelineViewList _views;
-       std::shared_ptr<TimelineTimeAxisView> _time_axis_view;
-       std::shared_ptr<TimelineReelsView> _reels_view;
-       std::shared_ptr<TimelineLabelsView> _labels_view;
-       int _tracks;
        boost::optional<double> _pixels_per_second;
-       bool _left_down;
-       wxPoint _down_point;
-       boost::optional<wxPoint> _zoom_point;
-       std::shared_ptr<TimelineContentView> _down_view;
-       dcpomatic::DCPTime _down_view_position;
-       bool _first_move;
-       ContentMenu _menu;
-       bool _snap;
-       std::list<dcpomatic::DCPTime> _start_snaps;
-       std::list<dcpomatic::DCPTime> _end_snaps;
-       Tool _tool;
-       int _x_scroll_rate;
-       int _y_scroll_rate;
-       int _pixels_per_track;
-       bool _first_resize;
-       wxTimer _timer;
-       boost::optional<int> _last_mouse_wheel_x;
-       boost::optional<double> _last_mouse_wheel_time;
-
-       static double const _minimum_pixels_per_second;
-       static int const _minimum_pixels_per_track;
-
-       boost::signals2::scoped_connection _film_changed_connection;
-       boost::signals2::scoped_connection _film_content_change_connection;
 };
+
+
+#endif
diff --git a/src/wx/timeline_atmos_content_view.cc b/src/wx/timeline_atmos_content_view.cc
deleted file mode 100644 (file)
index ec0f06e..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-    Copyright (C) 2016-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 "timeline_atmos_content_view.h"
-
-
-using std::shared_ptr;
-
-
-/** @class TimelineAtmosContentView
- *  @brief Timeline view for AtmosContent.
- */
-
-TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-
-wxColour
-TimelineAtmosContentView::background_colour () const
-{
-       return wxColour (149, 121, 232, 255);
-}
-
-
-wxColour
-TimelineAtmosContentView::foreground_colour () const
-{
-       return wxColour (0, 0, 0, 255);
-}
diff --git a/src/wx/timeline_atmos_content_view.h b/src/wx/timeline_atmos_content_view.h
deleted file mode 100644 (file)
index 15da14e..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2016-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 "timeline_content_view.h"
-
-
-/** @class TimelineAtmosContentView
- *  @brief Timeline view for AtmosContent.
- */
-class TimelineAtmosContentView : public TimelineContentView
-{
-public:
-       TimelineAtmosContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override {
-               return true;
-       }
-
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-};
diff --git a/src/wx/timeline_audio_content_view.cc b/src/wx/timeline_audio_content_view.cc
deleted file mode 100644 (file)
index 057ebb9..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
-    Copyright (C) 2013-2018 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 "timeline_audio_content_view.h"
-#include "wx_util.h"
-#include "lib/audio_content.h"
-#include "lib/util.h"
-
-using std::list;
-using std::shared_ptr;
-using std::dynamic_pointer_cast;
-
-/** @class TimelineAudioContentView
- *  @brief Timeline view for AudioContent.
- */
-
-TimelineAudioContentView::TimelineAudioContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-wxColour
-TimelineAudioContentView::background_colour () const
-{
-       return wxColour (149, 121, 232, 255);
-}
-
-wxColour
-TimelineAudioContentView::foreground_colour () const
-{
-       return wxColour (0, 0, 0, 255);
-}
-
-wxString
-TimelineAudioContentView::label () const
-{
-       wxString s = TimelineContentView::label ();
-       shared_ptr<AudioContent> ac = content()->audio;
-       DCPOMATIC_ASSERT (ac);
-
-       if (ac->gain() > 0.01) {
-               s += wxString::Format (" +%.1fdB", ac->gain());
-       } else if (ac->gain() < -0.01) {
-               s += wxString::Format (" %.1fdB", ac->gain());
-       }
-
-       if (ac->delay() > 0) {
-               s += wxString::Format (_(" delayed by %dms"), ac->delay());
-       } else if (ac->delay() < 0) {
-               s += wxString::Format (_(" advanced by %dms"), -ac->delay());
-       }
-
-       list<int> mapped = ac->mapping().mapped_output_channels();
-       if (!mapped.empty ()) {
-               s += wxString::FromUTF8(" â†’ ");
-               for (auto i: mapped) {
-                       s += std_to_wx(short_audio_channel_name(i)) + ", ";
-               }
-               s = s.Left(s.Length() - 2);
-       }
-
-       return s;
-}
diff --git a/src/wx/timeline_audio_content_view.h b/src/wx/timeline_audio_content_view.h
deleted file mode 100644 (file)
index 5b8d6cd..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2013-2015 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 "timeline_content_view.h"
-
-
-/** @class TimelineAudioContentView
- *  @brief Timeline view for AudioContent.
- */
-class TimelineAudioContentView : public TimelineContentView
-{
-public:
-       TimelineAudioContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override {
-               return true;
-       }
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-       wxString label () const override;
-};
index 5d039d0d3f0a2e82d252da234f63519b8eb4864e..cb0d102405ea34cad4e6b8bc65a4c56820b75557 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_content_view.h"
 #include "wx_util.h"
 #include "lib/content.h"
@@ -37,8 +37,8 @@ using namespace boost::placeholders;
 #endif
 
 
-TimelineContentView::TimelineContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineView (tl)
+TimelineContentView::TimelineContentView(ContentTimeline& tl, shared_ptr<Content> c)
+       : ContentTimelineView(tl)
        , _content (c)
 {
        _content_connection = c->Change.connect (bind (&TimelineContentView::content_change, this, _1, _3));
index 7794120cdc96762880e944bb14baf1b58b12503e..7b206d30f324f8fefb0cc5580997e12ab29cab9c 100644 (file)
@@ -23,7 +23,7 @@
 #define DCPOMATIC_TIMELINE_CONTENT_VIEW_H
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 #include "lib/change_signaller.h"
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
@@ -37,10 +37,10 @@ class Content;
 /** @class TimelineContentView
  *  @brief Parent class for views of pieces of content.
  */
-class TimelineContentView : public TimelineView
+class TimelineContentView : public ContentTimelineView
 {
 public:
-       TimelineContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
        dcpomatic::Rect<int> bbox () const override;
 
diff --git a/src/wx/timeline_dialog.cc b/src/wx/timeline_dialog.cc
deleted file mode 100644 (file)
index e0e1689..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-    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 "content_panel.h"
-#include "film_editor.h"
-#include "timeline_dialog.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
-
-
-TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmViewer& viewer)
-       : wxDialog (
-               cp->window(),
-               wxID_ANY,
-               _("Timeline"),
-               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
-               )
-       , _film (film)
-       , _timeline (this, cp, film, viewer)
-{
-       auto sizer = new wxBoxSizer (wxVERTICAL);
-
-       wxBitmap select(icon_path("select"), wxBITMAP_TYPE_PNG);
-       wxBitmap zoom(icon_path("zoom"), wxBITMAP_TYPE_PNG);
-       wxBitmap zoom_all(icon_path("zoom_all"), wxBITMAP_TYPE_PNG);
-       wxBitmap snap(icon_path("snap"), wxBITMAP_TYPE_PNG);
-       wxBitmap sequence(icon_path("sequence"), wxBITMAP_TYPE_PNG);
-
-       _toolbar = new wxToolBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
-       _toolbar->SetMargins (4, 4);
-       _toolbar->SetToolBitmapSize (wxSize(32, 32));
-       _toolbar->AddRadioTool ((int) Timeline::SELECT, _("Select"), select, wxNullBitmap, _("Select and move content"));
-       _toolbar->AddRadioTool ((int) Timeline::ZOOM, _("Zoom"), zoom, wxNullBitmap, _("Zoom in / out"));
-       _toolbar->AddTool ((int) Timeline::ZOOM_ALL, _("Zoom all"), zoom_all, _("Zoom out to whole film"));
-       _toolbar->AddCheckTool ((int) Timeline::SNAP, _("Snap"), snap, wxNullBitmap, _("Snap"));
-       _toolbar->AddCheckTool ((int) Timeline::SEQUENCE, _("Sequence"), sequence, wxNullBitmap, _("Keep video and subtitles in sequence"));
-       _toolbar->Realize ();
-
-       _toolbar->Bind (wxEVT_TOOL, bind (&TimelineDialog::tool_clicked, this, _1));
-
-       sizer->Add (_toolbar, 0, wxALL, 12);
-       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);
-
-       Bind(wxEVT_CHAR_HOOK, boost::bind(&TimelineDialog::keypress, this, _1));
-
-        _toolbar->ToggleTool ((int) Timeline::SNAP, _timeline.snap ());
-       film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
-
-       _film_changed_connection = film->Change.connect (bind (&TimelineDialog::film_change, this, _1, _2));
-}
-
-
-void
-TimelineDialog::film_change(ChangeType type, FilmProperty p)
-{
-       if (type != ChangeType::DONE) {
-               return;
-       }
-
-       auto film = _film.lock ();
-       if (!film) {
-               return;
-       }
-
-       if (p == FilmProperty::SEQUENCE) {
-               _toolbar->ToggleTool ((int) Timeline::SEQUENCE, film->sequence ());
-       }
-}
-
-
-void
-TimelineDialog::set_selection (ContentList selection)
-{
-       _timeline.set_selection (selection);
-}
-
-
-void
-TimelineDialog::tool_clicked (wxCommandEvent& ev)
-{
-       Timeline::Tool t = static_cast<Timeline::Tool>(ev.GetId());
-       _timeline.tool_clicked (t);
-       if (t == Timeline::SNAP) {
-               _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
-       } else if (t == Timeline::SEQUENCE) {
-               auto film = _film.lock ();
-               if (film) {
-                       film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
-               }
-       }
-}
-
-
-void
-TimelineDialog::keypress(wxKeyEvent const& event)
-{
-       _timeline.keypress(event);
-}
diff --git a/src/wx/timeline_dialog.h b/src/wx/timeline_dialog.h
deleted file mode 100644 (file)
index 8134aa6..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-    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 "timeline.h"
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <wx/wx.h>
-LIBDCP_ENABLE_WARNINGS
-
-
-class Playlist;
-
-
-class TimelineDialog : public wxDialog
-{
-public:
-       TimelineDialog(ContentPanel *, std::shared_ptr<Film>, FilmViewer& viewer);
-
-       void set_selection (ContentList selection);
-
-private:
-       void film_change(ChangeType type, FilmProperty);
-       void tool_clicked (wxCommandEvent& id);
-       void keypress(wxKeyEvent const& event);
-
-       std::weak_ptr<Film> _film;
-       Timeline _timeline;
-       wxToolBar* _toolbar;
-       boost::signals2::scoped_connection _film_changed_connection;
-};
index 181adc5ca411b07a8ada58617309e62782768010..c869d7ec524a63d85287e42dfe765a0674aa8dd6 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_labels_view.h"
 #include "wx_util.h"
 #include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::max;
 using std::min;
 
 
-TimelineLabelsView::TimelineLabelsView (Timeline& tl)
-       : TimelineView (tl)
+TimelineLabelsView::TimelineLabelsView(ContentTimeline& tl)
+       : ContentTimelineView(tl)
 {
        wxString labels[] = {
                _("Video"),
index fb80b1bf3311ec9533fdd6e280620750516c188d..324531c397c715b810b0ba431ea017d0c70e21fe 100644 (file)
 */
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 
 
 class wxWindow;
 
 
-class TimelineLabelsView : public TimelineView
+class TimelineLabelsView : public ContentTimelineView
 {
 public:
-       explicit TimelineLabelsView (Timeline& tl);
+       explicit TimelineLabelsView(ContentTimeline& tl);
 
        dcpomatic::Rect<int> bbox () const override;
 
index 0601a1196a1ee77a7aa1d52281182200d11af25b..5f2a7807954dd047a0019966e444518196a0711c 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_reels_view.h"
 #include "wx_util.h"
 #include "lib/film.h"
@@ -35,8 +35,8 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineReelsView::TimelineReelsView (Timeline& tl, int y)
-       : TimelineView (tl)
+TimelineReelsView::TimelineReelsView(ContentTimeline& tl, int y)
+       : ContentTimelineView(tl)
        , _y (y)
 {
 
index 357fe2ce47f80daa69ad3c6892f15f5796f8a9f0..7dbc34308289721e32424831df359540d2b31da3 100644 (file)
 */
 
 
-#include "timeline_view.h"
+#include "content_timeline_view.h"
 
 
-class TimelineReelsView : public TimelineView
+class TimelineReelsView : public ContentTimelineView
 {
 public:
-       TimelineReelsView (Timeline& tl, int y);
+       TimelineReelsView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
diff --git a/src/wx/timeline_text_content_view.cc b/src/wx/timeline_text_content_view.cc
deleted file mode 100644 (file)
index a573985..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-    Copyright (C) 2013-2018 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 "timeline_text_content_view.h"
-#include "lib/text_content.h"
-#include "lib/content.h"
-
-
-using std::shared_ptr;
-
-
-TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
-       : TimelineContentView (tl, c)
-       , _caption (caption)
-{
-
-}
-
-wxColour
-TimelineTextContentView::background_colour () const
-{
-       if (!active ()) {
-               return wxColour (210, 210, 210, 128);
-       }
-
-       return wxColour (163, 255, 154, 255);
-}
-
-wxColour
-TimelineTextContentView::foreground_colour () const
-{
-       if (!active ()) {
-               return wxColour (180, 180, 180, 128);
-       }
-
-       return wxColour (0, 0, 0, 255);
-}
-
-bool
-TimelineTextContentView::active () const
-{
-       return _caption->use();
-}
diff --git a/src/wx/timeline_text_content_view.h b/src/wx/timeline_text_content_view.h
deleted file mode 100644 (file)
index 046f5b3..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-    Copyright (C) 2013-2016 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 "timeline_content_view.h"
-
-class TextContent;
-class TextContent;
-
-/** @class TimelineTextContentView
- *  @brief Timeline view for TextContent.
- */
-class TimelineTextContentView : public TimelineContentView
-{
-public:
-       TimelineTextContentView (Timeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
-
-private:
-       bool active () const override;
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-
-       std::shared_ptr<TextContent> _caption;
-};
index d055bda7d54bfb310e338f1fcd2b2112cc64ad51..d9b7710c62589ff61f4ceae5ac37d6a13ef14f4c 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include "timeline_time_axis_view.h"
 #include "wx_util.h"
 #include <dcp/warnings.h>
@@ -34,8 +34,8 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineTimeAxisView::TimelineTimeAxisView (Timeline& tl, int y)
-       : TimelineView (tl)
+TimelineTimeAxisView::TimelineTimeAxisView(ContentTimeline& tl, int y)
+       : ContentTimelineView(tl)
        , _y (y)
 {
 
index 4c8e090fe7f5850a376e3eabb9646f61053f661d..f89121776dcb774fa853cf358e9ec44313d0df60 100644 (file)
 
 */
 
-#include "timeline_view.h"
 
-class TimelineTimeAxisView : public TimelineView
+#include "content_timeline_view.h"
+
+
+class TimelineTimeAxisView : public ContentTimelineView
 {
 public:
-       TimelineTimeAxisView (Timeline& tl, int y);
+       TimelineTimeAxisView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
diff --git a/src/wx/timeline_video_content_view.cc b/src/wx/timeline_video_content_view.cc
deleted file mode 100644 (file)
index b0f4b4f..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-    Copyright (C) 2013-2019 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 "lib/image_content.h"
-#include "lib/video_content.h"
-#include "timeline_video_content_view.h"
-
-using std::dynamic_pointer_cast;
-using std::shared_ptr;
-
-TimelineVideoContentView::TimelineVideoContentView (Timeline& tl, shared_ptr<Content> c)
-       : TimelineContentView (tl, c)
-{
-
-}
-
-wxColour
-TimelineVideoContentView::background_colour () const
-{
-       if (!active()) {
-               return wxColour (210, 210, 210, 128);
-       }
-
-       return wxColour (242, 92, 120, 255);
-}
-
-wxColour
-TimelineVideoContentView::foreground_colour () const
-{
-       if (!active()) {
-               return wxColour (180, 180, 180, 128);
-       }
-
-       return wxColour (0, 0, 0, 255);
-}
-
-bool
-TimelineVideoContentView::active () const
-{
-       shared_ptr<Content> c = _content.lock ();
-       DCPOMATIC_ASSERT (c);
-       return c->video && c->video->use();
-}
diff --git a/src/wx/timeline_video_content_view.h b/src/wx/timeline_video_content_view.h
deleted file mode 100644 (file)
index fa8ddf5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-    Copyright (C) 2013-2015 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 "timeline_content_view.h"
-
-
-/** @class TimelineVideoContentView
- *  @brief Timeline view for VideoContent.
- */
-class TimelineVideoContentView : public TimelineContentView
-{
-public:
-       TimelineVideoContentView (Timeline& tl, std::shared_ptr<Content> c);
-
-private:
-       bool active () const override;
-       wxColour background_colour () const override;
-       wxColour foreground_colour () const override;
-};
diff --git a/src/wx/timeline_view.cc b/src/wx/timeline_view.cc
deleted file mode 100644 (file)
index 2897c98..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-    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 "timeline_view.h"
-#include "timeline.h"
-
-
-using std::list;
-using namespace dcpomatic;
-
-
-/** @class TimelineView
- *  @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
- */
-TimelineView::TimelineView (Timeline& t)
-       : _timeline (t)
-{
-
-}
-
-
-void
-TimelineView::paint (wxGraphicsContext* g, list<dcpomatic::Rect<int>> overlaps)
-{
-       _last_paint_bbox = bbox ();
-       do_paint (g, overlaps);
-}
-
-
-void
-TimelineView::force_redraw ()
-{
-       _timeline.force_redraw (_last_paint_bbox.extended(4));
-       _timeline.force_redraw (bbox().extended(4));
-}
-
-
-int
-TimelineView::time_x (DCPTime t) const
-{
-       return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
-}
-
-
-int
-TimelineView::y_pos(int t) const
-{
-       return t * _timeline.pixels_per_track() + _timeline.tracks_y_offset();
-}
-
-
index 166a1121a80df30432a08979dc11c07be043e647..32eedde09e47765d45bb186c18e6e9f390b0f16b 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
 
 class wxGraphicsContext;
-class Timeline;
 
 
-/** @class TimelineView
- *  @brief Parent class for components of the timeline (e.g. a piece of content or an axis).
+/** @class ContentTimelineView
+ *  @brief Parent class for components of the content timeline (e.g. a piece of content or an axis).
  */
+template <class Timeline>
 class TimelineView
 {
 public:
-       explicit TimelineView (Timeline& t);
-       virtual ~TimelineView () {}
+       explicit TimelineView(Timeline& timeline)
+               : _timeline(timeline)
+       {}
 
-       TimelineView (TimelineView const&) = delete;
-       TimelineView& operator= (TimelineView const&) = delete;
+       virtual ~TimelineView () = default;
 
-       void paint (wxGraphicsContext* g, std::list<dcpomatic::Rect<int>> overlaps);
-       void force_redraw ();
+       TimelineView(TimelineView const&) = delete;
+       TimelineView& operator=(TimelineView const&) = delete;
 
-       virtual dcpomatic::Rect<int> bbox () const = 0;
+       void force_redraw()
+       {
+               _timeline.force_redraw(_last_paint_bbox.extended(4));
+               _timeline.force_redraw(bbox().extended(4));
+       }
 
-protected:
-       virtual void do_paint (wxGraphicsContext *, std::list<dcpomatic::Rect<int>> overlaps) = 0;
+       virtual dcpomatic::Rect<int> bbox() const = 0;
 
-       int time_x (dcpomatic::DCPTime t) const;
-       int y_pos(int t) const;
+protected:
+       int time_x(dcpomatic::DCPTime t) const
+       {
+               return t.seconds() * _timeline.pixels_per_second().get_value_or(0);
+       }
 
        Timeline& _timeline;
-
-private:
        dcpomatic::Rect<int> _last_paint_bbox;
 };
 
 
-typedef std::vector<std::shared_ptr<TimelineView>> TimelineViewList;
-
-
 #endif
+
index 7dbd214c6ebc305833137fa91deed7f727717eff..cf05dfe7dbab731083361435c9e2d64e4f0251be 100644 (file)
@@ -48,6 +48,13 @@ sources = """
           content_panel.cc
           content_properties_dialog.cc
           content_sub_panel.cc
+          content_timeline.cc
+          content_timeline_atmos_view.cc
+          content_timeline_audio_view.cc
+          content_timeline_dialog.cc
+          content_timeline_text_view.cc
+          content_timeline_video_view.cc
+          content_timeline_view.cc
           content_version_dialog.cc
           content_view.cc
           controls.cc
@@ -55,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
@@ -161,16 +171,10 @@ sources = """
           timer_display.cc
           timecode.cc
           timeline.cc
-          timeline_atmos_content_view.cc
           timeline_content_view.cc
-          timeline_dialog.cc
-          timeline_audio_content_view.cc
           timeline_labels_view.cc
-          timeline_text_content_view.cc
           timeline_reels_view.cc
           timeline_time_axis_view.cc
-          timeline_video_content_view.cc
-          timeline_view.cc
           timing_panel.cc
           try_unmount_dialog.cc
           update_dialog.cc
index dcf5bbc1871d9cf1f16883821ab3a0dda7f88c34..d8935daa195d1523f70a3f98ae6c3b49d6dd9fa7 100644 (file)
@@ -56,7 +56,7 @@ class PasswordEntry;
 #define DCPOMATIC_SIZER_GAP 8
 #define DCPOMATIC_DIALOG_BORDER 12
 #ifdef __WXGTK3__
-#define DCPOMATIC_SPIN_CTRL_WIDTH 118
+#define DCPOMATIC_SPIN_CTRL_WIDTH 124
 #else
 #define DCPOMATIC_SPIN_CTRL_WIDTH 56
 #endif