Rename Timeline -> ContentTimeline.
authorCarl Hetherington <cth@carlh.net>
Tue, 12 Dec 2023 00:32:04 +0000 (01:32 +0100)
committerCarl Hetherington <cth@carlh.net>
Mon, 11 Mar 2024 23:43:51 +0000 (00:43 +0100)
25 files changed:
src/wx/content_timeline.cc [new file with mode: 0644]
src/wx/content_timeline.h [new file with mode: 0644]
src/wx/timeline.cc [deleted file]
src/wx/timeline.h [deleted file]
src/wx/timeline_atmos_content_view.cc
src/wx/timeline_atmos_content_view.h
src/wx/timeline_audio_content_view.cc
src/wx/timeline_audio_content_view.h
src/wx/timeline_content_view.cc
src/wx/timeline_content_view.h
src/wx/timeline_dialog.cc
src/wx/timeline_dialog.h
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
src/wx/timeline_text_content_view.h
src/wx/timeline_time_axis_view.cc
src/wx/timeline_time_axis_view.h
src/wx/timeline_video_content_view.cc
src/wx/timeline_video_content_view.h
src/wx/timeline_view.cc
src/wx/timeline_view.h
src/wx/wscript

diff --git a/src/wx/content_timeline.cc b/src/wx/content_timeline.cc
new file mode 100644 (file)
index 0000000..b650f8e
--- /dev/null
@@ -0,0 +1,1029 @@
+/*
+    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 "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
+
+
+/* 3 hours in 640 pixels */
+double const ContentTimeline::_minimum_pixels_per_second = 640.0 / (60 * 60 * 3);
+int const ContentTimeline::_minimum_pixels_per_track = 16;
+
+
+ContentTimeline::ContentTimeline(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(&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::set_pixels_per_second(double pps)
+{
+       _pixels_per_second = max (_minimum_pixels_per_second, pps);
+}
+
+
+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<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
+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, 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
+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<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
+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<TimelineView>
+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) / 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
+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..65b2ff9
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+    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_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 Film;
+class FilmViewer;
+class TimelineLabelsView;
+class TimelineReelsView;
+class TimelineTimeAxisView;
+class TimelineView;
+
+
+class ContentTimeline : public wxPanel
+{
+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;
+       }
+
+       boost::optional<double> pixels_per_second () const {
+               return _pixels_per_second;
+       }
+
+       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_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;
+};
diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc
deleted file mode 100644 (file)
index 38e9de4..0000000
+++ /dev/null
@@ -1,1029 +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 "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
-
-
-/* 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 ();
-
-       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)
-{
-       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)
-{
-       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
-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;
-               }
-       }
-}
-
diff --git a/src/wx/timeline.h b/src/wx/timeline.h
deleted file mode 100644 (file)
index 621609f..0000000
+++ /dev/null
@@ -1,155 +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 "content_menu.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 Film;
-class FilmViewer;
-class TimelineLabelsView;
-class TimelineReelsView;
-class TimelineTimeAxisView;
-class TimelineView;
-
-
-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;
-
-       int pixels_per_track () const {
-               return _pixels_per_track;
-       }
-
-       boost::optional<double> pixels_per_second () const {
-               return _pixels_per_second;
-       }
-
-       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_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;
-};
index ec0f06ec170cd2d374aa5b72f282388633bdd4b4..d40ede137d024271e87ddc34836b63d142ce84f6 100644 (file)
@@ -29,7 +29,7 @@ using std::shared_ptr;
  *  @brief Timeline view for AtmosContent.
  */
 
-TimelineAtmosContentView::TimelineAtmosContentView (Timeline& tl, shared_ptr<Content> c)
+TimelineAtmosContentView::TimelineAtmosContentView(ContentTimeline& tl, shared_ptr<Content> c)
        : TimelineContentView (tl, c)
 {
 
index 15da14edc8be424aee2e436764d3634cc4342fcf..9536837e04476a97bbe9b61647d1ab2937f68fb5 100644 (file)
@@ -28,7 +28,7 @@
 class TimelineAtmosContentView : public TimelineContentView
 {
 public:
-       TimelineAtmosContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineAtmosContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
 private:
        bool active () const override {
index 057ebb944b8b8d632b53c36ba93410fe3a4cbb85..2920bf4fdb81c3eb424426f0769ec5b659c878a0 100644 (file)
 
 */
 
+
 #include "timeline_audio_content_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;
-using std::dynamic_pointer_cast;
+
 
 /** @class TimelineAudioContentView
  *  @brief Timeline view for AudioContent.
  */
 
-TimelineAudioContentView::TimelineAudioContentView (Timeline& tl, shared_ptr<Content> c)
+
+TimelineAudioContentView::TimelineAudioContentView(ContentTimeline& tl, shared_ptr<Content> c)
        : TimelineContentView (tl, c)
 {
 
index 5b8d6cdc22e46b8202c997945f3eb08287bd7c1e..29849adb292c07cc682eef74beeed11d1489dc80 100644 (file)
@@ -28,7 +28,7 @@
 class TimelineAudioContentView : public TimelineContentView
 {
 public:
-       TimelineAudioContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineAudioContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
 private:
        bool active () const override {
index 5d039d0d3f0a2e82d252da234f63519b8eb4864e..05ee6617b4148ffdce8c0cc93a116c3f8e2fdefd 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,7 +37,7 @@ using namespace boost::placeholders;
 #endif
 
 
-TimelineContentView::TimelineContentView (Timeline& tl, shared_ptr<Content> c)
+TimelineContentView::TimelineContentView(ContentTimeline& tl, shared_ptr<Content> c)
        : TimelineView (tl)
        , _content (c)
 {
index 7794120cdc96762880e944bb14baf1b58b12503e..ac84214f04d24795b36f6cadc9f9911aaaee3891 100644 (file)
@@ -40,7 +40,7 @@ class Content;
 class TimelineContentView : public TimelineView
 {
 public:
-       TimelineContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
        dcpomatic::Rect<int> bbox () const override;
 
index e0e1689a8acfcf227e89ab5ff78ff822dbb601c6..f0216a9cb75e8ba70b0f9e68cfa0117ad7d607ef 100644 (file)
@@ -73,11 +73,11 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView
        _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->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 (&TimelineDialog::tool_clicked, this, _1));
@@ -98,7 +98,7 @@ TimelineDialog::TimelineDialog(ContentPanel* cp, shared_ptr<Film> film, FilmView
 
        Bind(wxEVT_CHAR_HOOK, boost::bind(&TimelineDialog::keypress, this, _1));
 
-        _toolbar->ToggleTool ((int) Timeline::SNAP, _timeline.snap ());
+        _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SNAP), _timeline.snap ());
        film_change(ChangeType::DONE, FilmProperty::SEQUENCE);
 
        _film_changed_connection = film->Change.connect (bind (&TimelineDialog::film_change, this, _1, _2));
@@ -118,7 +118,7 @@ TimelineDialog::film_change(ChangeType type, FilmProperty p)
        }
 
        if (p == FilmProperty::SEQUENCE) {
-               _toolbar->ToggleTool ((int) Timeline::SEQUENCE, film->sequence ());
+               _toolbar->ToggleTool(static_cast<int>(ContentTimeline::SEQUENCE), film->sequence());
        }
 }
 
@@ -133,11 +133,11 @@ TimelineDialog::set_selection (ContentList selection)
 void
 TimelineDialog::tool_clicked (wxCommandEvent& ev)
 {
-       Timeline::Tool t = static_cast<Timeline::Tool>(ev.GetId());
+       auto t = static_cast<ContentTimeline::Tool>(ev.GetId());
        _timeline.tool_clicked (t);
-       if (t == Timeline::SNAP) {
+       if (t == ContentTimeline::SNAP) {
                _timeline.set_snap (_toolbar->GetToolState(static_cast<int>(t)));
-       } else if (t == Timeline::SEQUENCE) {
+       } else if (t == ContentTimeline::SEQUENCE) {
                auto film = _film.lock ();
                if (film) {
                        film->set_sequence (_toolbar->GetToolState(static_cast<int>(t)));
index 8134aa6dbae15c34503237eb5e16d0d1fda9c155..d2821c20c08b0b2aec3d6a809c89e67374cc9947 100644 (file)
@@ -19,7 +19,7 @@
 */
 
 
-#include "timeline.h"
+#include "content_timeline.h"
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/wx.h>
@@ -42,7 +42,7 @@ private:
        void keypress(wxKeyEvent const& event);
 
        std::weak_ptr<Film> _film;
-       Timeline _timeline;
+       ContentTimeline _timeline;
        wxToolBar* _toolbar;
        boost::signals2::scoped_connection _film_changed_connection;
 };
index 181adc5ca411b07a8ada58617309e62782768010..48c879d13dd616d4a7159b54fc253e186968394e 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,7 +34,7 @@ using std::max;
 using std::min;
 
 
-TimelineLabelsView::TimelineLabelsView (Timeline& tl)
+TimelineLabelsView::TimelineLabelsView(ContentTimeline& tl)
        : TimelineView (tl)
 {
        wxString labels[] = {
index fb80b1bf3311ec9533fdd6e280620750516c188d..1fa5b60b1ee1397604b9dd0403f398d01ed8f377 100644 (file)
@@ -28,7 +28,7 @@ class wxWindow;
 class TimelineLabelsView : public TimelineView
 {
 public:
-       explicit TimelineLabelsView (Timeline& tl);
+       explicit TimelineLabelsView(ContentTimeline& tl);
 
        dcpomatic::Rect<int> bbox () const override;
 
index 0601a1196a1ee77a7aa1d52281182200d11af25b..34ce96a4ee3bc12028a26fd0300fe3d7cb709403 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,7 +35,7 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineReelsView::TimelineReelsView (Timeline& tl, int y)
+TimelineReelsView::TimelineReelsView(ContentTimeline& tl, int y)
        : TimelineView (tl)
        , _y (y)
 {
index 357fe2ce47f80daa69ad3c6892f15f5796f8a9f0..670df7c9dd169d79f4554a4e43122c6069c90d48 100644 (file)
@@ -25,7 +25,7 @@
 class TimelineReelsView : public TimelineView
 {
 public:
-       TimelineReelsView (Timeline& tl, int y);
+       TimelineReelsView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
index a57398599d4392d00296d40deeccd479aa7f7531..bc099d4d6efc8aacb0d51d379002ab0139a87e85 100644 (file)
@@ -27,7 +27,7 @@
 using std::shared_ptr;
 
 
-TimelineTextContentView::TimelineTextContentView (Timeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
+TimelineTextContentView::TimelineTextContentView(ContentTimeline& tl, shared_ptr<Content> c, shared_ptr<TextContent> caption)
        : TimelineContentView (tl, c)
        , _caption (caption)
 {
index 046f5b3e66ca9d5a288a53369bb81a0e49c28068..c33c2662eb08c2f18b43cfa3289c7e1789942f62 100644 (file)
 
 */
 
+
 #include "timeline_content_view.h"
 
+
 class TextContent;
-class TextContent;
+
 
 /** @class TimelineTextContentView
  *  @brief Timeline view for TextContent.
@@ -29,7 +31,7 @@ class TextContent;
 class TimelineTextContentView : public TimelineContentView
 {
 public:
-       TimelineTextContentView (Timeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
+       TimelineTextContentView(ContentTimeline& tl, std::shared_ptr<Content>, std::shared_ptr<TextContent>);
 
 private:
        bool active () const override;
index d055bda7d54bfb310e338f1fcd2b2112cc64ad51..b17b274b2af5540acf6da2dee824d3bdc97e75d0 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,7 +34,7 @@ using std::list;
 using namespace dcpomatic;
 
 
-TimelineTimeAxisView::TimelineTimeAxisView (Timeline& tl, int y)
+TimelineTimeAxisView::TimelineTimeAxisView(ContentTimeline& tl, int y)
        : TimelineView (tl)
        , _y (y)
 {
index 4c8e090fe7f5850a376e3eabb9646f61053f661d..6ffb9e5dc27a3c19719216c1cd7b0a8585ab144c 100644 (file)
@@ -23,7 +23,7 @@
 class TimelineTimeAxisView : public TimelineView
 {
 public:
-       TimelineTimeAxisView (Timeline& tl, int y);
+       TimelineTimeAxisView(ContentTimeline& tl, int y);
 
        dcpomatic::Rect<int> bbox () const override;
        void set_y (int y);
index b0f4b4f5d34dd255a8a5083b7ca883f96b5d160d..d72b1e1491880b95c3f4488645f75489a43a5ef9 100644 (file)
 
 */
 
+
 #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)
+
+TimelineVideoContentView::TimelineVideoContentView(ContentTimeline& tl, shared_ptr<Content> c)
        : TimelineContentView (tl, c)
 {
 
index fa8ddf54c736ceebeb099eddefdb68079850fa7b..507f3caed1e9a71c5d3eb9226bc71cd8096fe9a9 100644 (file)
@@ -28,7 +28,7 @@
 class TimelineVideoContentView : public TimelineContentView
 {
 public:
-       TimelineVideoContentView (Timeline& tl, std::shared_ptr<Content> c);
+       TimelineVideoContentView(ContentTimeline& tl, std::shared_ptr<Content> c);
 
 private:
        bool active () const override;
index 2897c98e35398a27d83c13c875d6d016b90cc3c3..f81ddd413ebce28676a4edb0890f4cda520bd34b 100644 (file)
@@ -19,8 +19,8 @@
 */
 
 
+#include "content_timeline.h"
 #include "timeline_view.h"
-#include "timeline.h"
 
 
 using std::list;
@@ -30,7 +30,7 @@ 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)
+TimelineView::TimelineView(ContentTimeline& t)
        : _timeline (t)
 {
 
index 166a1121a80df30432a08979dc11c07be043e647..e753255e3b7c9fbea2bfbd2e3b58d57d9db5f213 100644 (file)
@@ -28,7 +28,7 @@
 
 
 class wxGraphicsContext;
-class Timeline;
+class ContentTimeline;
 
 
 /** @class TimelineView
@@ -37,7 +37,7 @@ class Timeline;
 class TimelineView
 {
 public:
-       explicit TimelineView (Timeline& t);
+       explicit TimelineView(ContentTimeline& t);
        virtual ~TimelineView () {}
 
        TimelineView (TimelineView const&) = delete;
@@ -54,7 +54,7 @@ protected:
        int time_x (dcpomatic::DCPTime t) const;
        int y_pos(int t) const;
 
-       Timeline& _timeline;
+       ContentTimeline& _timeline;
 
 private:
        dcpomatic::Rect<int> _last_paint_bbox;
index 7dbd214c6ebc305833137fa91deed7f727717eff..abf260ad6a1d2be1fb848d89598e833297ee7001 100644 (file)
@@ -48,6 +48,7 @@ sources = """
           content_panel.cc
           content_properties_dialog.cc
           content_sub_panel.cc
+          content_timeline.cc
           content_version_dialog.cc
           content_view.cc
           controls.cc
@@ -160,7 +161,6 @@ sources = """
           time_picker.cc
           timer_display.cc
           timecode.cc
-          timeline.cc
           timeline_atmos_content_view.cc
           timeline_content_view.cc
           timeline_dialog.cc