1 /* Copyright 2009-2010 Yorba Foundation
3 * This software is licensed under the GNU Lesser General Public License
4 * (version 2.1 or later). See the COPYING file in this distribution.
9 public class TrackClipPair {
10 public TrackClipPair(Model.Track track, Model.Clip clip) {
14 public Model.Track track;
15 public Model.Clip clip;
18 public class Clipboard {
19 public Gee.ArrayList<TrackClipPair> clips = new Gee.ArrayList<TrackClipPair>();
20 int64 minimum_time = -1;
22 public void select(Gee.ArrayList<ClipView> selected_clips) {
25 foreach(ClipView clip_view in selected_clips) {
26 TrackView track_view = clip_view.parent as TrackView;
27 if (minimum_time < 0 || clip_view.clip.start < minimum_time) {
28 minimum_time = clip_view.clip.start;
30 TrackClipPair track_clip_pair = new TrackClipPair(track_view.get_track(), clip_view.clip);
31 clips.add(track_clip_pair);
35 public void paste(Model.Track selected_track, int64 time) {
36 if (clips.size != 1) {
37 foreach (TrackClipPair pair in clips) {
38 pair.track.do_clip_paste(pair.clip.copy(), time + pair.clip.start - minimum_time);
41 selected_track.do_clip_paste(clips[0].clip.copy(), time);
46 public class TimeLine : Gtk.EventBox {
47 public Model.Project project;
48 public weak Model.TimeSystem provider;
49 public View.Ruler ruler;
50 Gtk.Widget drag_widget = null;
52 public Gee.ArrayList<TrackView> tracks = new Gee.ArrayList<TrackView>();
55 public Gee.ArrayList<ClipView> selected_clips = new Gee.ArrayList<ClipView>();
56 public Clipboard clipboard = new Clipboard();
58 public const int BAR_HEIGHT = 32;
59 public const int BORDER = 4;
61 public signal void selection_changed(bool selected);
62 public signal void track_changed();
63 public signal void trackview_added(TrackView trackview);
64 public signal void trackview_removed(TrackView trackview);
67 float pixel_min = 0.1f;
68 float pixel_max = 4505.0f;
71 public const int RULER_HEIGHT = 32;
72 // GapView will re-emerge after 0.1 release
73 // public GapView gap_view;
75 public TimeLine(Model.Project p, Model.TimeSystem provider, Gdk.DragAction actions) {
76 add_events(Gdk.EventMask.POINTER_MOTION_MASK);
80 this.provider = provider;
81 provider.geometry_changed.connect(on_geometry_changed);
83 vbox = new Gtk.VBox(false, 0);
84 ruler = new View.Ruler(provider, RULER_HEIGHT);
85 ruler.position_changed.connect(on_ruler_position_changed);
86 vbox.pack_start(ruler, false, false, 0);
88 project.track_added.connect(on_track_added);
89 project.track_removed.connect(on_track_removed);
90 project.media_engine.position_changed.connect(on_position_changed);
93 modify_bg(Gtk.StateType.NORMAL, parse_color("#444"));
94 modify_fg(Gtk.StateType.NORMAL, parse_color("#f00"));
96 pixel_div = pixel_max / pixel_min;
97 provider.calculate_pixel_step (0.5f, pixel_min, pixel_div);
98 Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, drag_target_entries, actions);
101 public void zoom_to_project(double width) {
102 if (project.get_length() == 0)
105 // The 12.0 is just a magic number to completely get rid of the scrollbar on this operation
108 double numerator = GLib.Math.log(
109 (width * Gst.SECOND) / ((double) project.get_length() * (double) pixel_min));
110 double denominator = GLib.Math.log((double) pixel_div);
112 zoom((float) (numerator / denominator) - provider.get_pixel_percentage());
115 public void zoom(float inc) {
116 provider.calculate_pixel_step(inc, pixel_min, pixel_div);
117 foreach (TrackView track in tracks) {
120 project.media_engine.position_changed(project.transport_get_position());
124 void on_geometry_changed() {
125 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_geometry_changed");
126 provider.calculate_pixel_step(0, pixel_min, pixel_div);
130 void on_position_changed() {
131 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_position_changed");
135 void on_track_added(Model.Track track) {
136 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_track_added");
137 TrackView track_view = ClassFactory.get_class_factory().get_track_view(track, this);
138 track_view.clip_view_added.connect(on_clip_view_added);
139 tracks.add(track_view);
140 vbox.pack_start(track_view, false, false, 0);
141 trackview_added(track_view);
142 if (track.media_type() == Model.MediaType.VIDEO) {
143 vbox.reorder_child(track_view, 1);
148 void on_track_removed(Model.Track track) {
149 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_track_removed");
150 foreach (TrackView track_view in tracks) {
151 if (track_view.get_track() == track) {
152 trackview_removed(track_view);
153 vbox.remove(track_view);
154 tracks.remove(track_view);
160 public void on_clip_view_added(ClipView clip_view) {
161 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_clip_view_added");
162 clip_view.selection_request.connect(on_clip_view_selection_request);
163 clip_view.move_request.connect(on_clip_view_move_request);
164 clip_view.move_commit.connect(on_clip_view_move_commit);
165 clip_view.move_begin.connect(on_clip_view_move_begin);
166 clip_view.trim_begin.connect(on_clip_view_trim_begin);
167 clip_view.trim_commit.connect(on_clip_view_trim_commit);
170 public void deselect_all_clips() {
171 foreach(ClipView selected_clip_view in selected_clips) {
172 selected_clip_view.is_selected = false;
174 selected_clips.clear();
177 void on_clip_view_move_begin(ClipView clip_view, bool copy) {
178 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_begin");
181 project.undo_manager.start_transaction("Copy Clip");
183 ClipView max_clip = null;
184 //The first pass removes the clips from the track model and makes any copies
185 foreach (ClipView selected_clip in selected_clips) {
186 if (max_clip == null) {
187 max_clip = selected_clip;
188 } else if (max_clip.clip.end < selected_clip.clip.end) {
189 max_clip = selected_clip;
191 selected_clip.initial_time = selected_clip.clip.start;
192 selected_clip.clip.gnonlin_disconnect();
193 TrackView track_view = selected_clip.get_parent() as TrackView;
194 if (track_view != null) {
195 track_view.get_track().remove_clip_from_array(selected_clip.clip);
198 // TODO: When adding in linking/groups, this should be moved into track_view
199 // We'll want to call move_begin for each clip that is linked, or in a group
200 // or selected and not iterate over them in this fashion in the timeline.
201 Model.Clip clip = selected_clip.clip.copy();
202 track_view.get_track().append_at_time(clip, selected_clip.clip.start, false);
206 high_water = new Gtk.Label(null);
207 Gtk.Fixed the_parent = clip_view.get_parent() as Gtk.Fixed;
208 the_parent.put(high_water,
209 max_clip.allocation.x + max_clip.allocation.width, max_clip.allocation.y);
211 //The second pass moves the selected clips to the top. We can't do this in one pass
212 //because creating a copy inserts the new copy in the z-order at the top.
213 foreach (ClipView selected_clip in selected_clips) {
214 TrackView track_view = selected_clip.get_parent() as TrackView;
215 track_view.move_to_top(selected_clip);
219 void on_clip_view_trim_begin(ClipView clip, Gdk.WindowEdge edge) {
220 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_trim_begin");
222 case Gdk.WindowEdge.WEST:
223 clip.initial_time = clip.clip.start;
225 case Gdk.WindowEdge.EAST:
226 clip.initial_time = clip.clip.duration;
229 assert(false); // We only support trimming east and west;
234 void on_clip_view_selection_request(ClipView clip_view, bool extend) {
235 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_selection_request");
237 if (gap_view != null) {
241 bool in_selected_clips = selected_clips.contains(clip_view);
243 if (!in_selected_clips) {
244 deselect_all_clips();
245 clip_view.is_selected = true;
246 selected_clips.add(clip_view);
249 if (selected_clips.size > 1) {
250 if (in_selected_clips && clip_view.is_selected) {
251 clip_view.is_selected = false;
252 // just deselected with multiple clips, so moving is not allowed
254 selected_clips.remove(clip_view);
257 if (!in_selected_clips) {
258 clip_view.is_selected = true;
259 selected_clips.add(clip_view);
263 selection_changed(is_clip_selected());
267 void on_clip_view_move_commit(ClipView clip_view, int64 delta) {
268 window.set_cursor(null);
269 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_request");
270 Gtk.Fixed fixed = high_water.get_parent() as Gtk.Fixed;
271 fixed.remove(high_water);
274 project.undo_manager.start_transaction("Move Clip");
275 foreach (ClipView selected_clip_view in selected_clips) {
276 TrackView track_view = selected_clip_view.get_parent() as TrackView;
277 selected_clip_view.clip.gnonlin_connect();
278 track_view.get_track().move(selected_clip_view.clip,
279 selected_clip_view.clip.start, selected_clip_view.initial_time);
281 project.undo_manager.end_transaction("Move Clip");
284 project.undo_manager.end_transaction("Copy Clip");
288 void on_clip_view_trim_commit(ClipView clip_view, Gdk.WindowEdge edge) {
289 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_commit");
290 window.set_cursor(null);
291 TrackView track_view = clip_view.get_parent() as TrackView;
294 case Gdk.WindowEdge.WEST:
295 delta = clip_view.clip.start - clip_view.initial_time;
297 case Gdk.WindowEdge.EAST:
298 delta = clip_view.clip.duration - clip_view.initial_time;
301 assert(false); // We only handle WEST and EAST
304 //restore back to pre-trim state
305 project.undo_manager.start_transaction("Trim Clip");
306 clip_view.clip.trim(-delta, edge);
307 clip_view.clip.gnonlin_connect();
308 track_view.get_track().trim(clip_view.clip, delta, edge);
309 project.undo_manager.end_transaction("Trim Clip");
312 void constrain_move(ClipView clip_view, ref int64 delta) {
313 int min_delta = clip_view.SNAP_DELTA;
314 int delta_xsize = provider.time_to_xsize(delta);
315 TrackView track_view = (TrackView) clip_view.parent as TrackView;
316 Model.Track track = track_view.get_track();
317 if (delta_xsize.abs() < min_delta) {
318 int64 range = provider.xsize_to_time(min_delta);
320 if (track.clip_is_near(clip_view.clip, range, out adjustment)) {
322 clip_view.snap(provider.time_to_xsize(adjustment));
327 void on_clip_view_move_request(ClipView clip_view, int64 delta) {
328 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_view_move_request");
329 if (project.snap_to_clip) {
330 constrain_move(clip_view, ref delta);
332 if (move_allowed(ref delta)) {
333 move_the_clips(delta);
337 bool move_allowed(ref int64 move_distance) {
338 if (drag_widget == null) {
342 ClipView max_clip = null;
344 foreach(ClipView clip_view in selected_clips) {
345 if (max_clip == null) {
346 max_clip = clip_view;
347 } else if (clip_view.clip.end > max_clip.clip.end){
348 max_clip = clip_view;
350 int position = provider.time_to_xpos(clip_view.clip.start + move_distance);
351 if (position < BORDER) {
358 void move_the_clips(int64 move_distance) {
359 foreach (ClipView clip_view in selected_clips) {
360 do_clip_move(clip_view, move_distance);
364 public void do_clip_move(ClipView clip_view, int64 delta) {
365 clip_view.clip.start += delta;
368 public void on_ruler_position_changed(int x) {
369 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_ruler_position_changed");
370 if (!project.transport_is_recording()) {
375 public bool is_clip_selected() {
376 return selected_clips.size > 0;
379 public bool gap_selected() {
381 // return gap_view != null;
384 public void delete_selection() {
385 project.undo_manager.start_transaction("Delete Clips From Timeline");
387 if (is_clip_selected()) {
388 while (selected_clips.size > 0) {
389 selected_clips[0].delete_clip();
390 selected_clips.remove_at(0);
395 if (gap_view != null) {
396 if (!project.can_delete_gap(gap_view.gap)) {
397 if (DialogUtils.delete_cancel("Really delete single-track gap?") ==
398 Gtk.ResponseType.YES) {
402 project.delete_gap(gap_view.gap);
409 project.undo_manager.end_transaction("Delete Clips From Timeline");
412 public void do_cut() {
413 clipboard.select(selected_clips);
417 public void do_copy() {
418 clipboard.select(selected_clips);
419 selection_changed(true);
422 public void paste() {
423 do_paste(project.transport_get_position());
426 public void do_paste(int64 pos) {
427 TrackView? view = null;
428 foreach (TrackView track_view in tracks) {
429 if (track_view.get_track().get_is_selected()) {
433 // TODO: Lombard doesn't use selected state. The following check should be removed
436 view = clipboard.clips[0].clip.type == Model.MediaType.VIDEO ?
437 find_video_track_view() : find_audio_track_view();
439 project.undo_manager.start_transaction("Paste");
440 clipboard.paste(view.get_track(), pos);
441 project.undo_manager.end_transaction("Paste");
445 public void select_all() {
446 foreach (TrackView track in tracks) {
451 public override bool expose_event(Gdk.EventExpose event) {
452 base.expose_event(event);
454 int xpos = provider.time_to_xpos(project.transport_get_position());
455 Gdk.draw_line(window, style.fg_gc[(int) Gtk.StateType.NORMAL],
457 xpos, allocation.height);
462 public override void drag_data_received(Gdk.DragContext context, int x, int y,
463 Gtk.SelectionData selection_data, uint drag_info,
465 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_data_received");
466 string[] a = selection_data.get_uris();
467 Gtk.drag_finish(context, true, false, time);
469 Model.Track? track = null;
470 TrackView? track_view = find_child(x, y) as TrackView;
472 if (track_view == null) {
476 bool timeline_add = true;
479 if (Gtk.drag_get_source_widget(context) != null) {
480 DialogUtils.warning("Cannot add files",
481 "Files must be dropped onto the timeline individually.");
485 if (DialogUtils.add_cancel(
486 "Files must be dropped onto the timeline individually.\n" +
487 "Do you wish to add these files to the library?") != Gtk.ResponseType.YES) {
490 timeline_add = false;
492 track = track_view.get_track();
495 project.create_clip_importer(track, timeline_add, provider.xpos_to_time(x),
496 context.action == Gdk.DragAction.COPY, (Gtk.Window) get_toplevel(), a.length);
498 foreach (string s in a) {
501 filename = GLib.Filename.from_uri(s);
502 } catch (GLib.ConvertError e) { continue; }
503 project.importer.add_file(filename);
505 project.importer.start();
507 project.error_occurred("Error importing", e.message);
511 public void update_pos(int event_x) {
512 int64 time = provider.xpos_to_time(event_x);
513 if (project.snap_to_clip) {
514 project.snap_coord(out time, provider.get_pixel_snap_time());
516 project.media_engine.go(time);
519 public Gtk.Widget? find_child(double x, double y) {
520 foreach (Gtk.Widget w in vbox.get_children()) {
521 if (w.allocation.y <= y && y < w.allocation.y + w.allocation.height)
527 void deselect_all() {
528 foreach (ClipView clip_view in selected_clips) {
529 clip_view.is_selected = false;
531 selected_clips.clear();
532 selection_changed(false);
535 public override bool button_press_event(Gdk.EventButton event) {
537 if (gap_view != null)
541 Gtk.Widget? child = find_child(event.x, event.y);
543 if (child is View.Ruler) {
544 View.Ruler ruler = child as View.Ruler;
545 ruler.button_press_event(event);
547 } else if (child is TrackView) {
548 TrackView track_view = child as TrackView;
550 drag_widget = track_view.find_child(event.x, event.y);
551 if (drag_widget != null) {
552 drag_widget.button_press_event(event);
555 // want to select the track_views track as selected
556 track_view.get_track().set_selected(true);
566 public override bool button_release_event(Gdk.EventButton event) {
567 if (drag_widget != null) {
568 drag_widget.button_release_event(event);
574 public override bool motion_notify_event(Gdk.EventMotion event) {
575 if (drag_widget != null) {
576 drag_widget.motion_notify_event(event);
578 Gtk.Widget widget = find_child(event.x, event.y);
579 if (widget is TrackView) {
580 TrackView? track_view = widget as TrackView;
581 if (track_view != null) {
582 ClipView? clip_view = track_view.find_child(event.x, event.y) as ClipView;
583 if (clip_view != null) {
584 clip_view.motion_notify_event(event);
586 window.set_cursor(null);
589 } else if (widget is View.Ruler) {
590 widget.motion_notify_event(event);
592 window.set_cursor(null);
598 TrackView? find_video_track_view() {
599 foreach (TrackView track in tracks) {
600 if (track.get_track().media_type() == Model.MediaType.VIDEO) {
608 TrackView? find_audio_track_view() {
609 foreach (TrackView track in tracks) {
610 if (track.get_track().media_type() == Model.MediaType.AUDIO) {