Initial commit
[fillmore] / src / marina / timeline.vala
1 /* Copyright 2009-2010 Yorba Foundation
2  *
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. 
5  */
6
7 using Logging;
8
9 public class TrackClipPair {
10     public TrackClipPair(Model.Track track, Model.Clip clip) {
11         this.track = track;
12         this.clip = clip;
13     }
14     public Model.Track track;
15     public Model.Clip clip;
16 }
17
18 public class Clipboard {
19     public Gee.ArrayList<TrackClipPair> clips = new Gee.ArrayList<TrackClipPair>();
20     int64 minimum_time = -1;
21
22     public void select(Gee.ArrayList<ClipView> selected_clips) {
23         clips.clear();
24         minimum_time = -1;
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;
29             }
30             TrackClipPair track_clip_pair = new TrackClipPair(track_view.get_track(), clip_view.clip);
31             clips.add(track_clip_pair);
32         }
33     }
34
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);
39             }
40         } else {
41             selected_track.do_clip_paste(clips[0].clip.copy(), time);
42         }
43     }
44 }
45
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;
51     bool copying;
52     public Gee.ArrayList<TrackView> tracks = new Gee.ArrayList<TrackView>();
53     Gtk.VBox vbox;
54
55     public Gee.ArrayList<ClipView> selected_clips = new Gee.ArrayList<ClipView>();
56     public Clipboard clipboard = new Clipboard();
57
58     public const int BAR_HEIGHT = 32;
59     public const int BORDER = 4;
60
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);
65
66     float pixel_div;
67     float pixel_min = 0.1f;
68     float pixel_max = 4505.0f;
69     Gtk.Label high_water;
70
71     public const int RULER_HEIGHT = 32;
72     // GapView will re-emerge after 0.1 release
73     // public GapView gap_view;
74
75     public TimeLine(Model.Project p, Model.TimeSystem provider, Gdk.DragAction actions) {
76         add_events(Gdk.EventMask.POINTER_MOTION_MASK);
77         drag_widget = null;
78         can_focus = true;
79         project = p;
80         this.provider = provider;
81         provider.geometry_changed.connect(on_geometry_changed);
82
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);
87
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);
91         add(vbox);
92
93         modify_bg(Gtk.StateType.NORMAL, parse_color("#444"));
94         modify_fg(Gtk.StateType.NORMAL, parse_color("#f00"));
95
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);
99     }
100
101     public void zoom_to_project(double width) {
102         if (project.get_length() == 0)
103             return;
104             
105         // The 12.0 is just a magic number to completely get rid of the scrollbar on this operation
106         width -= 12.0;
107             
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);
111
112         zoom((float) (numerator / denominator) - provider.get_pixel_percentage());
113     }
114
115     public void zoom(float inc) {
116         provider.calculate_pixel_step(inc, pixel_min, pixel_div);
117         foreach (TrackView track in tracks) {
118             track.resize();
119         }
120         project.media_engine.position_changed(project.transport_get_position());
121         queue_draw();
122     }
123
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);
127         ruler.queue_draw();
128     }
129
130     void on_position_changed() {
131         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_position_changed");
132         queue_draw();
133     }
134
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);
144         }
145         vbox.show_all();
146     }
147
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);
155                 break;
156             }
157         }
158     }
159
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);
168     }
169
170     public void deselect_all_clips() {
171         foreach(ClipView selected_clip_view in selected_clips) {
172             selected_clip_view.is_selected = false;
173         }
174         selected_clips.clear();
175     }
176
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");
179         copying = copy;
180         if (copy) {
181             project.undo_manager.start_transaction("Copy Clip");
182         }
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;
190             }
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);
196             }
197             if (copy) {
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);
203             }
204         }
205
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);
210
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);
216         }
217     }
218
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");
221         switch (edge) {
222             case Gdk.WindowEdge.WEST:
223                 clip.initial_time = clip.clip.start;
224                 break;
225             case Gdk.WindowEdge.EAST:
226                 clip.initial_time = clip.clip.duration;
227                 break;
228             default:
229                 assert(false); // We only support trimming east and west;
230                 break;
231         }
232     }
233
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");
236 /*
237         if (gap_view != null) {
238             gap_view.unselect();
239         }
240 */
241         bool in_selected_clips = selected_clips.contains(clip_view);
242         if (!extend) {
243             if (!in_selected_clips) {
244                 deselect_all_clips();
245                 clip_view.is_selected = true;
246                 selected_clips.add(clip_view);
247             }
248         } else {
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
253                     drag_widget = null;
254                     selected_clips.remove(clip_view);
255                 }
256             }
257             if (!in_selected_clips) {
258                 clip_view.is_selected = true;
259                 selected_clips.add(clip_view);
260             }
261         }
262         track_changed();
263         selection_changed(is_clip_selected());
264         queue_draw();
265     }
266
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);
272         high_water = null;
273
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);
280         }
281         project.undo_manager.end_transaction("Move Clip");
282         if (copying) {
283             copying = false;
284             project.undo_manager.end_transaction("Copy Clip");
285         }
286     }
287
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;
292         int64 delta = 0;
293         switch (edge) {
294             case Gdk.WindowEdge.WEST:
295                 delta = clip_view.clip.start - clip_view.initial_time;
296                 break;
297             case Gdk.WindowEdge.EAST:
298                 delta = clip_view.clip.duration - clip_view.initial_time;
299                 break;
300             default:
301                 assert(false);  // We only handle WEST and EAST
302                 break;
303         }
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");
310     }
311
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);
319             int64 adjustment;
320             if (track.clip_is_near(clip_view.clip, range, out adjustment)) {
321                 delta = adjustment;
322                 clip_view.snap(provider.time_to_xsize(adjustment));
323             }
324         }
325     }
326
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);
331         }
332         if (move_allowed(ref delta)) {
333             move_the_clips(delta);
334         }
335     }
336
337     bool move_allowed(ref int64 move_distance) {
338         if (drag_widget == null) {
339             return false;
340         }
341
342         ClipView max_clip = null;
343
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;
349             }
350             int position = provider.time_to_xpos(clip_view.clip.start + move_distance);
351             if (position < BORDER) {
352                 return false;
353             }
354         }
355         return true;
356     }
357
358     void move_the_clips(int64 move_distance) {
359         foreach (ClipView clip_view in selected_clips) {
360             do_clip_move(clip_view, move_distance);
361         }
362     }
363
364     public void do_clip_move(ClipView clip_view, int64 delta) {
365         clip_view.clip.start += delta;
366     }
367
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()) {
371             update_pos(x);
372         }
373     }
374
375     public bool is_clip_selected() {
376         return selected_clips.size > 0;
377     }
378     
379     public bool gap_selected() {
380         return false;
381 //        return gap_view != null;
382     }
383
384     public void delete_selection() {
385         project.undo_manager.start_transaction("Delete Clips From Timeline");
386         drag_widget = null;
387         if (is_clip_selected()) {
388             while (selected_clips.size > 0) {
389                 selected_clips[0].delete_clip();
390                 selected_clips.remove_at(0);
391             }
392             track_changed();
393         } else {
394 /*
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) {
399                         gap_view.remove();
400                     }
401                 } else {
402                     project.delete_gap(gap_view.gap);
403                 }
404                 
405                 gap_view.unselect();
406             }
407 */
408         }
409         project.undo_manager.end_transaction("Delete Clips From Timeline");
410     }
411
412     public void do_cut() {
413         clipboard.select(selected_clips);
414         delete_selection();
415     }
416
417     public void do_copy() {
418         clipboard.select(selected_clips);
419         selection_changed(true);
420     }
421
422     public void paste() {
423         do_paste(project.transport_get_position());
424     }
425
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()) {
430                 view = track_view;
431             }
432         }
433         // TODO: Lombard doesn't use selected state.  The following check should be removed
434         // when it does
435         if (view == null) {
436             view = clipboard.clips[0].clip.type == Model.MediaType.VIDEO ?
437                             find_video_track_view() : find_audio_track_view();
438         }
439         project.undo_manager.start_transaction("Paste");
440         clipboard.paste(view.get_track(), pos);
441         project.undo_manager.end_transaction("Paste");
442         queue_draw();
443     }
444
445     public void select_all() {
446         foreach (TrackView track in tracks) {
447             track.select_all();
448         }
449     }
450
451     public override bool expose_event(Gdk.EventExpose event) {
452         base.expose_event(event);
453
454         int xpos = provider.time_to_xpos(project.transport_get_position());
455         Gdk.draw_line(window, style.fg_gc[(int) Gtk.StateType.NORMAL],
456                       xpos, 0,
457                       xpos, allocation.height);
458
459         return true;
460     }
461
462     public override void drag_data_received(Gdk.DragContext context, int x, int y,
463                                             Gtk.SelectionData selection_data, uint drag_info,
464                                             uint time) {
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);
468
469         Model.Track? track = null;
470         TrackView? track_view = find_child(x, y) as TrackView;
471
472         if (track_view == null) {
473             return;
474         }
475
476         bool timeline_add = true;
477
478         if (a.length > 1) {
479             if (Gtk.drag_get_source_widget(context) != null) {
480                 DialogUtils.warning("Cannot add files",
481                     "Files must be dropped onto the timeline individually.");
482                 return;
483             }
484
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) {
488                         return;
489                     }
490             timeline_add = false;
491         } else {
492             track = track_view.get_track();
493         }
494
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);
497         try {
498             foreach (string s in a) {
499                 string filename;
500                 try {
501                     filename = GLib.Filename.from_uri(s);
502                 } catch (GLib.ConvertError e) { continue; }
503                 project.importer.add_file(filename);
504             }
505             project.importer.start();
506         } catch (Error e) {
507             project.error_occurred("Error importing", e.message);
508         }
509     }
510
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());
515         }
516         project.media_engine.go(time);
517     }
518
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)
522                 return w;
523         }
524         return null;
525     }
526
527     void deselect_all() {
528         foreach (ClipView clip_view in selected_clips) {
529             clip_view.is_selected = false;
530         }
531         selected_clips.clear();
532         selection_changed(false);
533     }
534
535     public override bool button_press_event(Gdk.EventButton event) {
536 /*
537         if (gap_view != null)
538             gap_view.unselect();
539 */      
540         drag_widget = null;
541         Gtk.Widget? child = find_child(event.x, event.y);
542
543         if (child is View.Ruler) {
544             View.Ruler ruler = child as View.Ruler;
545             ruler.button_press_event(event);
546             drag_widget = child;
547         } else if (child is TrackView) {
548             TrackView track_view = child as TrackView;
549
550             drag_widget = track_view.find_child(event.x, event.y);
551             if (drag_widget != null) {
552                 drag_widget.button_press_event(event);
553             } else {
554                 deselect_all();
555                 // want to select the track_views track as selected
556                 track_view.get_track().set_selected(true);
557             }
558         } else {
559             deselect_all();
560         }
561         queue_draw();
562
563         return true;
564     }
565
566     public override bool button_release_event(Gdk.EventButton event) {
567         if (drag_widget != null) {
568             drag_widget.button_release_event(event);
569             drag_widget = null;
570         }
571         return true;
572     }
573
574     public override bool motion_notify_event(Gdk.EventMotion event) {
575         if (drag_widget != null) {
576             drag_widget.motion_notify_event(event);
577         } else {
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);
585                     } else {
586                         window.set_cursor(null);
587                     }
588                 }
589             } else if (widget is View.Ruler) {
590                 widget.motion_notify_event(event);
591             } else {
592                 window.set_cursor(null);
593             }
594         }
595         return true;
596     }
597
598     TrackView? find_video_track_view() {
599         foreach (TrackView track in tracks) {
600             if (track.get_track().media_type() == Model.MediaType.VIDEO) {
601                 return track;
602             }
603         }
604
605         return null;
606     }
607
608     TrackView? find_audio_track_view() {
609         foreach (TrackView track in tracks) {
610             if (track.get_track().media_type() == Model.MediaType.AUDIO) {
611                 return track;
612             }
613         }
614
615         return null;
616     }
617 }