Initial commit
[fillmore] / src / marina / timeline.vala
diff --git a/src/marina/timeline.vala b/src/marina/timeline.vala
new file mode 100644 (file)
index 0000000..cf8bdd5
--- /dev/null
@@ -0,0 +1,617 @@
+/* Copyright 2009-2010 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution. 
+ */
+
+using Logging;
+
+public class TrackClipPair {
+    public TrackClipPair(Model.Track track, Model.Clip clip) {
+        this.track = track;
+        this.clip = clip;
+    }
+    public Model.Track track;
+    public Model.Clip clip;
+}
+
+public class Clipboard {
+    public Gee.ArrayList<TrackClipPair> clips = new Gee.ArrayList<TrackClipPair>();
+    int64 minimum_time = -1;
+
+    public void select(Gee.ArrayList<ClipView> selected_clips) {
+        clips.clear();
+        minimum_time = -1;
+        foreach(ClipView clip_view in selected_clips) {
+            TrackView track_view = clip_view.parent as TrackView;
+            if (minimum_time < 0 || clip_view.clip.start < minimum_time) {
+                minimum_time = clip_view.clip.start;
+            }
+            TrackClipPair track_clip_pair = new TrackClipPair(track_view.get_track(), clip_view.clip);
+            clips.add(track_clip_pair);
+        }
+    }
+
+    public void paste(Model.Track selected_track, int64 time) {
+        if (clips.size != 1) {
+            foreach (TrackClipPair pair in clips) {
+                pair.track.do_clip_paste(pair.clip.copy(), time + pair.clip.start - minimum_time);
+            }
+        } else {
+            selected_track.do_clip_paste(clips[0].clip.copy(), time);
+        }
+    }
+}
+
+public class TimeLine : Gtk.EventBox {
+    public Model.Project project;
+    public weak Model.TimeSystem provider;
+    public View.Ruler ruler;
+    Gtk.Widget drag_widget = null;
+    bool copying;
+    public Gee.ArrayList<TrackView> tracks = new Gee.ArrayList<TrackView>();
+    Gtk.VBox vbox;
+
+    public Gee.ArrayList<ClipView> selected_clips = new Gee.ArrayList<ClipView>();
+    public Clipboard clipboard = new Clipboard();
+
+    public const int BAR_HEIGHT = 32;
+    public const int BORDER = 4;
+
+    public signal void selection_changed(bool selected);
+    public signal void track_changed();
+    public signal void trackview_added(TrackView trackview);
+    public signal void trackview_removed(TrackView trackview);
+
+    float pixel_div;
+    float pixel_min = 0.1f;
+    float pixel_max = 4505.0f;
+    Gtk.Label high_water;
+
+    public const int RULER_HEIGHT = 32;
+    // GapView will re-emerge after 0.1 release
+    // public GapView gap_view;
+
+    public TimeLine(Model.Project p, Model.TimeSystem provider, Gdk.DragAction actions) {
+        add_events(Gdk.EventMask.POINTER_MOTION_MASK);
+        drag_widget = null;
+        can_focus = true;
+        project = p;
+        this.provider = provider;
+        provider.geometry_changed.connect(on_geometry_changed);
+
+        vbox = new Gtk.VBox(false, 0);
+        ruler = new View.Ruler(provider, RULER_HEIGHT);
+        ruler.position_changed.connect(on_ruler_position_changed);
+        vbox.pack_start(ruler, false, false, 0);
+
+        project.track_added.connect(on_track_added);
+        project.track_removed.connect(on_track_removed);
+        project.media_engine.position_changed.connect(on_position_changed);
+        add(vbox);
+
+        modify_bg(Gtk.StateType.NORMAL, parse_color("#444"));
+        modify_fg(Gtk.StateType.NORMAL, parse_color("#f00"));
+
+        pixel_div = pixel_max / pixel_min;
+        provider.calculate_pixel_step (0.5f, pixel_min, pixel_div);
+        Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, drag_target_entries, actions);
+    }
+
+    public void zoom_to_project(double width) {
+        if (project.get_length() == 0)
+            return;
+            
+        // The 12.0 is just a magic number to completely get rid of the scrollbar on this operation
+        width -= 12.0;
+            
+        double numerator = GLib.Math.log(
+                    (width * Gst.SECOND) / ((double) project.get_length() * (double) pixel_min));
+        double denominator = GLib.Math.log((double) pixel_div);
+
+        zoom((float) (numerator / denominator) - provider.get_pixel_percentage());
+    }
+
+    public void zoom(float inc) {
+        provider.calculate_pixel_step(inc, pixel_min, pixel_div);
+        foreach (TrackView track in tracks) {
+            track.resize();
+        }
+        project.media_engine.position_changed(project.transport_get_position());
+        queue_draw();
+    }
+
+    void on_geometry_changed() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_geometry_changed");
+        provider.calculate_pixel_step(0, pixel_min, pixel_div);
+        ruler.queue_draw();
+    }
+
+    void on_position_changed() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_position_changed");
+        queue_draw();
+    }
+
+    void on_track_added(Model.Track track) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_track_added");
+        TrackView track_view = ClassFactory.get_class_factory().get_track_view(track, this);
+        track_view.clip_view_added.connect(on_clip_view_added);
+        tracks.add(track_view);
+        vbox.pack_start(track_view, false, false, 0);
+        trackview_added(track_view);
+        if (track.media_type() == Model.MediaType.VIDEO) {
+            vbox.reorder_child(track_view, 1);
+        }
+        vbox.show_all();
+    }
+
+    void on_track_removed(Model.Track track) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_track_removed");
+        foreach (TrackView track_view in tracks) {
+            if (track_view.get_track() == track) {
+                trackview_removed(track_view);
+                vbox.remove(track_view);
+                tracks.remove(track_view);
+                break;
+            }
+        }
+    }
+
+    public void on_clip_view_added(ClipView clip_view) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_clip_view_added");
+        clip_view.selection_request.connect(on_clip_view_selection_request);
+        clip_view.move_request.connect(on_clip_view_move_request);
+        clip_view.move_commit.connect(on_clip_view_move_commit);
+        clip_view.move_begin.connect(on_clip_view_move_begin);
+        clip_view.trim_begin.connect(on_clip_view_trim_begin);
+        clip_view.trim_commit.connect(on_clip_view_trim_commit);
+    }
+
+    public void deselect_all_clips() {
+        foreach(ClipView selected_clip_view in selected_clips) {
+            selected_clip_view.is_selected = false;
+        }
+        selected_clips.clear();
+    }
+
+    void on_clip_view_move_begin(ClipView clip_view, bool copy) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_begin");
+        copying = copy;
+        if (copy) {
+            project.undo_manager.start_transaction("Copy Clip");
+        }
+        ClipView max_clip = null;
+        //The first pass removes the clips from the track model and makes any copies
+        foreach (ClipView selected_clip in selected_clips) {
+            if (max_clip == null) {
+                max_clip = selected_clip;
+            } else if (max_clip.clip.end < selected_clip.clip.end) {
+                max_clip = selected_clip;
+            }
+            selected_clip.initial_time = selected_clip.clip.start;
+            selected_clip.clip.gnonlin_disconnect();
+            TrackView track_view = selected_clip.get_parent() as TrackView;
+            if (track_view != null) {
+                track_view.get_track().remove_clip_from_array(selected_clip.clip);
+            }
+            if (copy) {
+                // TODO: When adding in linking/groups, this should be moved into track_view
+                // We'll want to call move_begin for each clip that is linked, or in a group 
+                // or selected and not iterate over them in this fashion in the timeline.
+                Model.Clip clip = selected_clip.clip.copy();
+                track_view.get_track().append_at_time(clip, selected_clip.clip.start, false);
+            }
+        }
+
+        high_water = new Gtk.Label(null);
+        Gtk.Fixed the_parent = clip_view.get_parent() as Gtk.Fixed;
+        the_parent.put(high_water,
+            max_clip.allocation.x + max_clip.allocation.width, max_clip.allocation.y);
+
+        //The second pass moves the selected clips to the top.  We can't do this in one pass
+        //because creating a copy inserts the new copy in the z-order at the top.
+        foreach (ClipView selected_clip in selected_clips) {
+            TrackView track_view = selected_clip.get_parent() as TrackView;
+            track_view.move_to_top(selected_clip);
+        }
+    }
+
+    void on_clip_view_trim_begin(ClipView clip, Gdk.WindowEdge edge) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_trim_begin");
+        switch (edge) {
+            case Gdk.WindowEdge.WEST:
+                clip.initial_time = clip.clip.start;
+                break;
+            case Gdk.WindowEdge.EAST:
+                clip.initial_time = clip.clip.duration;
+                break;
+            default:
+                assert(false); // We only support trimming east and west;
+                break;
+        }
+    }
+
+    void on_clip_view_selection_request(ClipView clip_view, bool extend) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_selection_request");
+/*
+        if (gap_view != null) {
+            gap_view.unselect();
+        }
+*/
+        bool in_selected_clips = selected_clips.contains(clip_view);
+        if (!extend) {
+            if (!in_selected_clips) {
+                deselect_all_clips();
+                clip_view.is_selected = true;
+                selected_clips.add(clip_view);
+            }
+        } else {
+            if (selected_clips.size > 1) {
+                if (in_selected_clips && clip_view.is_selected) {
+                    clip_view.is_selected = false;
+                    // just deselected with multiple clips, so moving is not allowed
+                    drag_widget = null;
+                    selected_clips.remove(clip_view);
+                }
+            }
+            if (!in_selected_clips) {
+                clip_view.is_selected = true;
+                selected_clips.add(clip_view);
+            }
+        }
+        track_changed();
+        selection_changed(is_clip_selected());
+        queue_draw();
+    }
+
+    void on_clip_view_move_commit(ClipView clip_view, int64 delta) {
+        window.set_cursor(null);
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_request");
+        Gtk.Fixed fixed = high_water.get_parent() as Gtk.Fixed;
+        fixed.remove(high_water);
+        high_water = null;
+
+        project.undo_manager.start_transaction("Move Clip");
+        foreach (ClipView selected_clip_view in selected_clips) {
+            TrackView track_view = selected_clip_view.get_parent() as TrackView;
+            selected_clip_view.clip.gnonlin_connect();
+            track_view.get_track().move(selected_clip_view.clip, 
+                 selected_clip_view.clip.start, selected_clip_view.initial_time);
+        }
+        project.undo_manager.end_transaction("Move Clip");
+        if (copying) {
+            copying = false;
+            project.undo_manager.end_transaction("Copy Clip");
+        }
+    }
+
+    void on_clip_view_trim_commit(ClipView clip_view, Gdk.WindowEdge edge) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_commit");
+        window.set_cursor(null);
+        TrackView track_view = clip_view.get_parent() as TrackView;
+        int64 delta = 0;
+        switch (edge) {
+            case Gdk.WindowEdge.WEST:
+                delta = clip_view.clip.start - clip_view.initial_time;
+                break;
+            case Gdk.WindowEdge.EAST:
+                delta = clip_view.clip.duration - clip_view.initial_time;
+                break;
+            default:
+                assert(false);  // We only handle WEST and EAST
+                break;
+        }
+        //restore back to pre-trim state
+        project.undo_manager.start_transaction("Trim Clip");
+        clip_view.clip.trim(-delta, edge);
+        clip_view.clip.gnonlin_connect();
+        track_view.get_track().trim(clip_view.clip, delta, edge);
+        project.undo_manager.end_transaction("Trim Clip");
+    }
+
+    void constrain_move(ClipView clip_view, ref int64 delta) {
+        int min_delta = clip_view.SNAP_DELTA;
+        int delta_xsize = provider.time_to_xsize(delta);
+        TrackView track_view = (TrackView) clip_view.parent as TrackView;
+        Model.Track track = track_view.get_track();
+        if (delta_xsize.abs() < min_delta) {
+            int64 range = provider.xsize_to_time(min_delta);
+            int64 adjustment;
+            if (track.clip_is_near(clip_view.clip, range, out adjustment)) {
+                delta = adjustment;
+                clip_view.snap(provider.time_to_xsize(adjustment));
+            }
+        }
+    }
+
+    void on_clip_view_move_request(ClipView clip_view, int64 delta) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_request");
+        if (project.snap_to_clip) {
+            constrain_move(clip_view, ref delta);
+        }
+        if (move_allowed(ref delta)) {
+            move_the_clips(delta);
+        }
+    }
+
+    bool move_allowed(ref int64 move_distance) {
+        if (drag_widget == null) {
+            return false;
+        }
+
+        ClipView max_clip = null;
+
+        foreach(ClipView clip_view in selected_clips) {
+            if (max_clip == null) {
+                max_clip = clip_view;
+            } else if (clip_view.clip.end  > max_clip.clip.end){
+                max_clip = clip_view;
+            }
+            int position = provider.time_to_xpos(clip_view.clip.start + move_distance);
+            if (position < BORDER) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    void move_the_clips(int64 move_distance) {
+        foreach (ClipView clip_view in selected_clips) {
+            do_clip_move(clip_view, move_distance);
+        }
+    }
+
+    public void do_clip_move(ClipView clip_view, int64 delta) {
+        clip_view.clip.start += delta;
+    }
+
+    public void on_ruler_position_changed(int x) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_ruler_position_changed");
+        if (!project.transport_is_recording()) {
+            update_pos(x);
+        }
+    }
+
+    public bool is_clip_selected() {
+        return selected_clips.size > 0;
+    }
+    
+    public bool gap_selected() {
+        return false;
+//        return gap_view != null;
+    }
+
+    public void delete_selection() {
+        project.undo_manager.start_transaction("Delete Clips From Timeline");
+        drag_widget = null;
+        if (is_clip_selected()) {
+            while (selected_clips.size > 0) {
+                selected_clips[0].delete_clip();
+                selected_clips.remove_at(0);
+            }
+            track_changed();
+        } else {
+/*
+            if (gap_view != null) {
+                if (!project.can_delete_gap(gap_view.gap)) {
+                    if (DialogUtils.delete_cancel("Really delete single-track gap?") ==
+                           Gtk.ResponseType.YES) {
+                        gap_view.remove();
+                    }
+                } else {
+                    project.delete_gap(gap_view.gap);
+                }
+                
+                gap_view.unselect();
+            }
+*/
+        }
+        project.undo_manager.end_transaction("Delete Clips From Timeline");
+    }
+
+    public void do_cut() {
+        clipboard.select(selected_clips);
+        delete_selection();
+    }
+
+    public void do_copy() {
+        clipboard.select(selected_clips);
+        selection_changed(true);
+    }
+
+    public void paste() {
+        do_paste(project.transport_get_position());
+    }
+
+    public void do_paste(int64 pos) {
+        TrackView? view = null;
+        foreach (TrackView track_view in tracks) {
+            if (track_view.get_track().get_is_selected()) {
+                view = track_view;
+            }
+        }
+        // TODO: Lombard doesn't use selected state.  The following check should be removed
+        // when it does
+        if (view == null) {
+            view = clipboard.clips[0].clip.type == Model.MediaType.VIDEO ?
+                            find_video_track_view() : find_audio_track_view();
+        }
+        project.undo_manager.start_transaction("Paste");
+        clipboard.paste(view.get_track(), pos);
+        project.undo_manager.end_transaction("Paste");
+        queue_draw();
+    }
+
+    public void select_all() {
+        foreach (TrackView track in tracks) {
+            track.select_all();
+        }
+    }
+
+    public override bool expose_event(Gdk.EventExpose event) {
+        base.expose_event(event);
+
+        int xpos = provider.time_to_xpos(project.transport_get_position());
+        Gdk.draw_line(window, style.fg_gc[(int) Gtk.StateType.NORMAL],
+                      xpos, 0,
+                      xpos, allocation.height);
+
+        return true;
+    }
+
+    public override void drag_data_received(Gdk.DragContext context, int x, int y,
+                                            Gtk.SelectionData selection_data, uint drag_info,
+                                            uint time) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_data_received");
+        string[] a = selection_data.get_uris();
+        Gtk.drag_finish(context, true, false, time);
+
+        Model.Track? track = null;
+        TrackView? track_view = find_child(x, y) as TrackView;
+
+        if (track_view == null) {
+            return;
+        }
+
+        bool timeline_add = true;
+
+        if (a.length > 1) {
+            if (Gtk.drag_get_source_widget(context) != null) {
+                DialogUtils.warning("Cannot add files",
+                    "Files must be dropped onto the timeline individually.");
+                return;
+            }
+
+            if (DialogUtils.add_cancel(
+                "Files must be dropped onto the timeline individually.\n" +
+                    "Do you wish to add these files to the library?") != Gtk.ResponseType.YES) {
+                        return;
+                    }
+            timeline_add = false;
+        } else {
+            track = track_view.get_track();
+        }
+
+        project.create_clip_importer(track, timeline_add, provider.xpos_to_time(x),
+            context.action == Gdk.DragAction.COPY, (Gtk.Window) get_toplevel(), a.length);
+        try {
+            foreach (string s in a) {
+                string filename;
+                try {
+                    filename = GLib.Filename.from_uri(s);
+                } catch (GLib.ConvertError e) { continue; }
+                project.importer.add_file(filename);
+            }
+            project.importer.start();
+        } catch (Error e) {
+            project.error_occurred("Error importing", e.message);
+        }
+    }
+
+    public void update_pos(int event_x) {
+        int64 time = provider.xpos_to_time(event_x);
+        if (project.snap_to_clip) {
+            project.snap_coord(out time, provider.get_pixel_snap_time());
+        }
+        project.media_engine.go(time);
+    }
+
+    public Gtk.Widget? find_child(double x, double y) {
+        foreach (Gtk.Widget w in vbox.get_children()) {
+            if (w.allocation.y <= y && y < w.allocation.y + w.allocation.height)
+                return w;
+        }
+        return null;
+    }
+
+    void deselect_all() {
+        foreach (ClipView clip_view in selected_clips) {
+            clip_view.is_selected = false;
+        }
+        selected_clips.clear();
+        selection_changed(false);
+    }
+
+    public override bool button_press_event(Gdk.EventButton event) {
+/*
+        if (gap_view != null)
+            gap_view.unselect();
+*/      
+        drag_widget = null;
+        Gtk.Widget? child = find_child(event.x, event.y);
+
+        if (child is View.Ruler) {
+            View.Ruler ruler = child as View.Ruler;
+            ruler.button_press_event(event);
+            drag_widget = child;
+        } else if (child is TrackView) {
+            TrackView track_view = child as TrackView;
+
+            drag_widget = track_view.find_child(event.x, event.y);
+            if (drag_widget != null) {
+                drag_widget.button_press_event(event);
+            } else {
+                deselect_all();
+                // want to select the track_views track as selected
+                track_view.get_track().set_selected(true);
+            }
+        } else {
+            deselect_all();
+        }
+        queue_draw();
+
+        return true;
+    }
+
+    public override bool button_release_event(Gdk.EventButton event) {
+        if (drag_widget != null) {
+            drag_widget.button_release_event(event);
+            drag_widget = null;
+        }
+        return true;
+    }
+
+    public override bool motion_notify_event(Gdk.EventMotion event) {
+        if (drag_widget != null) {
+            drag_widget.motion_notify_event(event);
+        } else {
+            Gtk.Widget widget = find_child(event.x, event.y);
+            if (widget is TrackView) {
+                TrackView? track_view = widget as TrackView;
+                if (track_view != null) {
+                    ClipView? clip_view = track_view.find_child(event.x, event.y) as ClipView;
+                    if (clip_view != null) {
+                        clip_view.motion_notify_event(event);
+                    } else {
+                        window.set_cursor(null);
+                    }
+                }
+            } else if (widget is View.Ruler) {
+                widget.motion_notify_event(event);
+            } else {
+                window.set_cursor(null);
+            }
+        }
+        return true;
+    }
+
+    TrackView? find_video_track_view() {
+        foreach (TrackView track in tracks) {
+            if (track.get_track().media_type() == Model.MediaType.VIDEO) {
+                return track;
+            }
+        }
+
+        return null;
+    }
+
+    TrackView? find_audio_track_view() {
+        foreach (TrackView track in tracks) {
+            if (track.get_track().media_type() == Model.MediaType.AUDIO) {
+                return track;
+            }
+        }
+
+        return null;
+    }
+}