/* 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 clips = new Gee.ArrayList(); int64 minimum_time = -1; public void select(Gee.ArrayList 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 tracks = new Gee.ArrayList(); Gtk.VBox vbox; public Gee.ArrayList selected_clips = new Gee.ArrayList(); 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; } }