Initial commit
[fillmore] / src / marina / ui_clip.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 GapView : Gtk.DrawingArea {
10     public Model.Gap gap;
11     Gdk.Color fill_color;
12
13     public GapView(int64 start, int64 length, int width, int height) {
14
15         gap = new Model.Gap(start, start + length);
16
17         Gdk.Color.parse("#777", out fill_color);
18
19         set_flags(Gtk.WidgetFlags.NO_WINDOW);
20
21         set_size_request(width, height);
22     }
23
24     public signal void removed(GapView gap_view);
25     public signal void unselected(GapView gap_view);
26
27     public void remove() {
28         removed(this);
29     }
30
31     public void unselect() {
32         unselected(this);
33     }
34
35     public override bool expose_event(Gdk.EventExpose e) {
36         draw_rounded_rectangle(window, fill_color, true, allocation.x, allocation.y, 
37                                 allocation.width - 1, allocation.height - 1);
38         return true;
39     }
40 }
41
42 public class ClipView : Gtk.DrawingArea {
43     enum MotionMode {
44         NONE,
45         DRAGGING,
46         LEFT_TRIM,
47         RIGHT_TRIM
48     }
49
50     public Model.Clip clip;
51     public int64 initial_time;
52     weak Model.TimeSystem time_provider;
53     public bool is_selected;
54     public int height; // TODO: We request size of height, but we aren't allocated this height.
55                        // We should be using the allocated height, not the requested height. 
56     public static Gtk.Menu context_menu;
57     TransportDelegate transport_delegate;
58     Gdk.Color color_black;
59     Gdk.Color color_normal;
60     Gdk.Color color_selected;
61     int drag_point;
62     int snap_amount;
63     bool snapped;
64     MotionMode motion_mode = MotionMode.NONE;
65     bool button_down = false;
66     bool pending_selection;
67     const int MIN_DRAG = 5;
68     const int TRIM_WIDTH = 10;
69     public const int SNAP_DELTA = 10;
70
71     static Gdk.Cursor left_trim_cursor = new Gdk.Cursor(Gdk.CursorType.LEFT_SIDE);
72     static Gdk.Cursor right_trim_cursor = new Gdk.Cursor(Gdk.CursorType.RIGHT_SIDE);
73     static Gdk.Cursor hand_cursor = new Gdk.Cursor.from_name(Gdk.Display.get_default(), "dnd-none");
74     // will be used for drag
75     static Gdk.Cursor plus_cursor = new Gdk.Cursor.from_name(Gdk.Display.get_default(), "dnd-copy");
76
77     public signal void clip_deleted(Model.Clip clip);
78     public signal void clip_moved(ClipView clip);
79     public signal void selection_request(ClipView clip_view, bool extend_selection);
80     public signal void move_request(ClipView clip_view, int64 delta);
81     public signal void move_commit(ClipView clip_view, int64 delta);
82     public signal void move_begin(ClipView clip_view, bool copy);
83     public signal void trim_begin(ClipView clip_view, Gdk.WindowEdge edge);
84     public signal void trim_commit(ClipView clip_view, Gdk.WindowEdge edge);
85
86     public ClipView(TransportDelegate transport_delegate, Model.Clip clip, 
87             Model.TimeSystem time_provider, int height) {
88         this.transport_delegate = transport_delegate;
89         this.clip = clip;
90         this.time_provider = time_provider;
91         this.height = height;
92         is_selected = false;
93
94         clip.moved.connect(on_clip_moved);
95         clip.updated.connect(on_clip_updated);
96
97         Gdk.Color.parse("000", out color_black);
98         get_clip_colors();
99
100         set_flags(Gtk.WidgetFlags.NO_WINDOW);
101
102         adjust_size(height);
103     }
104
105     void get_clip_colors() {
106         if (clip.clipfile.is_online()) {
107             Gdk.Color.parse(clip.type == Model.MediaType.VIDEO ? "#d82" : "#84a", 
108                 out color_selected);
109             Gdk.Color.parse(clip.type == Model.MediaType.VIDEO ? "#da5" : "#b9d", 
110                 out color_normal);
111         } else {
112             Gdk.Color.parse("red", out color_selected);
113             Gdk.Color.parse("#AA0000", out color_normal);
114         }
115     }
116
117     void on_clip_updated() {
118         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_updated");
119         get_clip_colors();
120         queue_draw();
121     }
122
123     // Note that a view's size may vary slightly (by a single pixel) depending on its
124     // starting position.  This is because the clip's length may not be an integer number of
125     // pixels, and may get rounded either up or down depending on the clip position.
126     public void adjust_size(int height) {
127         int width = time_provider.time_to_xpos(clip.start + clip.duration) -
128                     time_provider.time_to_xpos(clip.start);
129         set_size_request(width + 1, height);
130     }
131
132     public void on_clip_moved(Model.Clip clip) {
133         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_moved");
134         adjust_size(height);
135         clip_moved(this);
136     }
137
138     public void delete_clip() {
139         clip_deleted(clip);
140     }
141
142     public void draw() {
143         weak Gdk.Color fill = is_selected ? color_selected : color_normal;
144
145         bool left_trimmed = clip.media_start != 0 && !clip.is_recording;
146
147         bool right_trimmed = clip.clipfile.is_online() ? 
148                               (clip.media_start + clip.duration != clip.clipfile.length) : false;
149
150         if (!left_trimmed && !right_trimmed) {
151             draw_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
152                                    allocation.width - 2, allocation.height - 2);
153             draw_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
154                                    allocation.width - 1, allocation.height - 1);
155
156         } else if (!left_trimmed && right_trimmed) {
157             draw_left_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
158                                         allocation.width - 2, allocation.height - 2);
159             draw_left_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
160                                    allocation.width - 1, allocation.height - 1);
161
162         } else if (left_trimmed && !right_trimmed) {
163             draw_right_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
164                                          allocation.width - 2, allocation.height - 2);
165             draw_right_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
166                                          allocation.width - 1, allocation.height - 1);
167
168         } else {
169             draw_square_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
170                                   allocation.width - 2, allocation.height - 2);
171             draw_square_rectangle(window, color_black, false, allocation.x, allocation.y,
172                                   allocation.width - 1, allocation.height - 1);
173         }
174
175         Gdk.GC gc = new Gdk.GC(window);
176         Gdk.Rectangle r = { 0, 0, 0, 0 };
177
178         // Due to a Vala compiler bug, we have to do this initialization here...
179         r.x = allocation.x;
180         r.y = allocation.y;
181         r.width = allocation.width;
182         r.height = allocation.height;
183
184         gc.set_clip_rectangle(r);
185
186         Pango.Layout layout;
187         if (clip.is_recording) {
188             layout = create_pango_layout("Recording");
189         } else if (!clip.clipfile.is_online()) {
190             layout = create_pango_layout("%s (Offline)".printf(clip.name));
191         }
192         else {
193             layout = create_pango_layout("%s".printf(clip.name));
194         }
195         int width, height;
196         layout.get_pixel_size(out width, out height);
197         Gdk.draw_layout(window, gc, allocation.x + 10, allocation.y + height, layout);
198     }
199
200     public override bool expose_event(Gdk.EventExpose event) {
201         draw();
202         return true;
203     }
204
205     public override bool button_press_event(Gdk.EventButton event) {
206         if (!transport_delegate.is_stopped()) {
207             return true;
208         }
209
210         event.x -= allocation.x;
211         bool primary_press = event.button == 1;
212         if (primary_press) {
213             button_down = true;
214             drag_point = (int)event.x;
215             snap_amount = 0;
216             snapped = false;
217         }
218
219         bool extend_selection = (event.state & Gdk.ModifierType.CONTROL_MASK) != 0;
220         // The clip is not responsible for changing the selection state.
221         // It may depend upon knowledge of multiple clips.  Let anyone who is interested
222         // update our state.
223         if (is_left_trim(event.x, event.y)) {
224             selection_request(this, false);
225             if (primary_press) {
226                 trim_begin(this, Gdk.WindowEdge.WEST);
227                 motion_mode = MotionMode.LEFT_TRIM;
228             }
229         } else if (is_right_trim(event.x, event.y)){
230             selection_request(this, false);
231             if (primary_press) {
232                 trim_begin(this, Gdk.WindowEdge.EAST);
233                 motion_mode = MotionMode.RIGHT_TRIM;
234             }
235         } else {
236             if (!is_selected) {
237                 pending_selection = false;
238                 selection_request(this, extend_selection);
239             } else {
240                 pending_selection = true;
241             }
242         }
243
244         if (event.button == 3) {
245             context_menu.select_first(true);
246             context_menu.popup(null, null, null, event.button, event.time);
247         } else {
248             context_menu.popdown();
249         }
250
251         return true;
252     }
253
254     public override bool button_release_event(Gdk.EventButton event) {
255         if (!transport_delegate.is_stopped()) {
256             return true;
257         }
258
259         event.x -= allocation.x;
260         button_down = false;
261         if (event.button == 1) {
262             switch (motion_mode) {
263                 case MotionMode.NONE: {
264                     if (pending_selection) {
265                         selection_request(this, true);
266                     }
267                 }
268                 break;
269                 case MotionMode.DRAGGING: {
270                     int64 delta = time_provider.xsize_to_time((int) event.x - drag_point);
271                     if (motion_mode == MotionMode.DRAGGING) {
272                         move_commit(this, delta);
273                     }
274                 }
275                 break;
276                 case MotionMode.LEFT_TRIM:
277                     trim_commit(this, Gdk.WindowEdge.WEST);
278                 break;
279                 case MotionMode.RIGHT_TRIM:
280                     trim_commit(this, Gdk.WindowEdge.EAST);
281                 break;
282             }
283         }
284         motion_mode = MotionMode.NONE;
285         return true;
286     }
287
288     public override bool motion_notify_event(Gdk.EventMotion event) {
289         if (!transport_delegate.is_stopped()) {
290             return true;
291         }
292
293         event.x -= allocation.x;
294         int delta_pixels = (int)(event.x - drag_point) - snap_amount;
295         if (snapped) {
296             snap_amount += delta_pixels;
297             if (snap_amount.abs() < SNAP_DELTA) {
298                 return true;
299             }
300             delta_pixels += snap_amount;
301             snap_amount = 0;
302             snapped = false;
303         }
304
305         int64 delta_time = time_provider.xsize_to_time(delta_pixels);
306
307         switch (motion_mode) {
308             case MotionMode.NONE:
309                 if (!button_down && is_left_trim(event.x, event.y)) {
310                     window.set_cursor(left_trim_cursor);
311                 } else if (!button_down && is_right_trim(event.x, event.y)) {
312                     window.set_cursor(right_trim_cursor);
313                 } else if (is_selected && button_down) {
314                     if (delta_pixels.abs() > MIN_DRAG) {
315                         bool do_copy = (event.state & Gdk.ModifierType.CONTROL_MASK) != 0;
316                         if (do_copy) {
317                             window.set_cursor(plus_cursor);
318                         } else {
319                             window.set_cursor(hand_cursor);
320                         }
321                         motion_mode = MotionMode.DRAGGING;
322                         move_begin(this, do_copy);
323                     }
324                 } else {
325                     window.set_cursor(null);
326                 }
327             break;
328             case MotionMode.RIGHT_TRIM:
329             case MotionMode.LEFT_TRIM:
330                 if (button_down) {
331                     if (motion_mode == MotionMode.LEFT_TRIM) {
332                         clip.trim(delta_time, Gdk.WindowEdge.WEST);
333                     } else {
334                         int64 duration = clip.duration;
335                         clip.trim(delta_time, Gdk.WindowEdge.EAST);
336                         if (duration != clip.duration) {
337                             drag_point += (int)delta_pixels;
338                         }
339                     }
340                 }
341                 return true;
342             case MotionMode.DRAGGING:
343                 move_request(this, delta_time);
344                 return true;
345         }
346         return false;
347     }
348
349     bool is_trim_height(double y) {
350         return y - allocation.y > allocation.height / 2;
351     }
352
353     bool is_left_trim(double x, double y) {
354         return is_trim_height(y) && x > 0 && x < TRIM_WIDTH;
355     }
356
357     bool is_right_trim(double x, double y) {
358         return is_trim_height(y) && x > allocation.width - TRIM_WIDTH && 
359             x < allocation.width;
360     }
361
362     public void select() {
363         if (!is_selected) {
364             selection_request(this, true);
365         }
366     }
367     
368     public void snap(int64 amount) {
369         snap_amount = time_provider.time_to_xsize(amount);
370         snapped = true;
371     }
372 }