+ if (i == _views.end ()) {
+ return shared_ptr<View> ();
+ }
+
+ return *i;
+}
+
+void
+Timeline::left_down (wxMouseEvent& ev)
+{
+ shared_ptr<View> view = event_to_view (ev);
+ shared_ptr<ContentView> content_view = dynamic_pointer_cast<ContentView> (view);
+
+ _down_view.reset ();
+
+ if (content_view) {
+ _down_view = content_view;
+ _down_view_position = content_view->content()->position ();
+ }
+
+ for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
+ shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
+ if (!cv) {
+ continue;
+ }
+
+ if (!ev.ShiftDown ()) {
+ cv->set_selected (view == *i);
+ }
+
+ if (view == *i) {
+ _content_panel->set_selection (cv->content ());
+ }
+ }
+
+ if (content_view && ev.ShiftDown ()) {
+ content_view->set_selected (!content_view->selected ());
+ }
+
+ _left_down = true;
+ _down_point = ev.GetPosition ();
+ _first_move = false;
+
+ if (_down_view) {
+ _down_view->content()->set_change_signals_frequent (true);
+ }
+}
+
+void
+Timeline::left_up (wxMouseEvent& ev)
+{
+ _left_down = false;
+
+ if (_down_view) {
+ _down_view->content()->set_change_signals_frequent (false);
+ }
+
+ set_position_from_event (ev);
+}
+
+void
+Timeline::mouse_moved (wxMouseEvent& ev)
+{
+ if (!_left_down) {
+ return;
+ }
+
+ set_position_from_event (ev);
+}
+
+void
+Timeline::right_down (wxMouseEvent& ev)
+{
+ shared_ptr<View> view = event_to_view (ev);
+ shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (view);
+ if (!cv) {
+ return;
+ }
+
+ if (!cv->selected ()) {
+ clear_selection ();
+ cv->set_selected (true);
+ }
+
+ _menu.popup (_film, selected_content (), ev.GetPosition ());
+}
+
+void
+Timeline::set_position_from_event (wxMouseEvent& ev)
+{
+ if (!_pixels_per_second) {
+ return;
+ }
+
+ double const pps = _pixels_per_second.get ();
+
+ wxPoint 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;
+ }
+
+ DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps);
+
+ if (_snap) {
+
+ bool first = true;
+ DCPTime nearest_distance = DCPTime::max ();
+ DCPTime nearest_new_position = DCPTime::max ();
+
+ /* Find the nearest content edge; this is inefficient */
+ for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
+ shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
+ if (!cv || cv == _down_view) {
+ continue;
+ }
+
+ {
+ /* Snap starts to ends */
+ DCPTime const d = DCPTime (cv->content()->end() - new_position).abs ();
+ if (first || d < nearest_distance) {
+ nearest_distance = d;
+ nearest_new_position = cv->content()->end() + DCPTime::delta ();
+ }
+ }
+
+ {
+ /* Snap ends to starts */
+ DCPTime const d = DCPTime (
+ cv->content()->position() - (new_position + _down_view->content()->length_after_trim())
+ ).abs ();
+
+ if (d < nearest_distance) {
+ nearest_distance = d;
+ nearest_new_position = cv->content()->position() - _down_view->content()->length_after_trim () - DCPTime::delta();
+ }
+ }
+
+ first = false;
+ }
+
+ if (!first) {
+ /* Snap if it's close; `close' means within a proportion of the time on the timeline */
+ if (nearest_distance < DCPTime::from_seconds ((width() / pps) / 32)) {
+ new_position = nearest_new_position;
+ }