Initial commit
[fillmore] / src / marina / ui_clip.vala
diff --git a/src/marina/ui_clip.vala b/src/marina/ui_clip.vala
new file mode 100644 (file)
index 0000000..9a17244
--- /dev/null
@@ -0,0 +1,372 @@
+/* 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 GapView : Gtk.DrawingArea {
+    public Model.Gap gap;
+    Gdk.Color fill_color;
+
+    public GapView(int64 start, int64 length, int width, int height) {
+
+        gap = new Model.Gap(start, start + length);
+
+        Gdk.Color.parse("#777", out fill_color);
+
+        set_flags(Gtk.WidgetFlags.NO_WINDOW);
+
+        set_size_request(width, height);
+    }
+
+    public signal void removed(GapView gap_view);
+    public signal void unselected(GapView gap_view);
+
+    public void remove() {
+        removed(this);
+    }
+
+    public void unselect() {
+        unselected(this);
+    }
+
+    public override bool expose_event(Gdk.EventExpose e) {
+        draw_rounded_rectangle(window, fill_color, true, allocation.x, allocation.y, 
+                                allocation.width - 1, allocation.height - 1);
+        return true;
+    }
+}
+
+public class ClipView : Gtk.DrawingArea {
+    enum MotionMode {
+        NONE,
+        DRAGGING,
+        LEFT_TRIM,
+        RIGHT_TRIM
+    }
+
+    public Model.Clip clip;
+    public int64 initial_time;
+    weak Model.TimeSystem time_provider;
+    public bool is_selected;
+    public int height; // TODO: We request size of height, but we aren't allocated this height.
+                       // We should be using the allocated height, not the requested height. 
+    public static Gtk.Menu context_menu;
+    TransportDelegate transport_delegate;
+    Gdk.Color color_black;
+    Gdk.Color color_normal;
+    Gdk.Color color_selected;
+    int drag_point;
+    int snap_amount;
+    bool snapped;
+    MotionMode motion_mode = MotionMode.NONE;
+    bool button_down = false;
+    bool pending_selection;
+    const int MIN_DRAG = 5;
+    const int TRIM_WIDTH = 10;
+    public const int SNAP_DELTA = 10;
+
+    static Gdk.Cursor left_trim_cursor = new Gdk.Cursor(Gdk.CursorType.LEFT_SIDE);
+    static Gdk.Cursor right_trim_cursor = new Gdk.Cursor(Gdk.CursorType.RIGHT_SIDE);
+    static Gdk.Cursor hand_cursor = new Gdk.Cursor.from_name(Gdk.Display.get_default(), "dnd-none");
+    // will be used for drag
+    static Gdk.Cursor plus_cursor = new Gdk.Cursor.from_name(Gdk.Display.get_default(), "dnd-copy");
+
+    public signal void clip_deleted(Model.Clip clip);
+    public signal void clip_moved(ClipView clip);
+    public signal void selection_request(ClipView clip_view, bool extend_selection);
+    public signal void move_request(ClipView clip_view, int64 delta);
+    public signal void move_commit(ClipView clip_view, int64 delta);
+    public signal void move_begin(ClipView clip_view, bool copy);
+    public signal void trim_begin(ClipView clip_view, Gdk.WindowEdge edge);
+    public signal void trim_commit(ClipView clip_view, Gdk.WindowEdge edge);
+
+    public ClipView(TransportDelegate transport_delegate, Model.Clip clip, 
+            Model.TimeSystem time_provider, int height) {
+        this.transport_delegate = transport_delegate;
+        this.clip = clip;
+        this.time_provider = time_provider;
+        this.height = height;
+        is_selected = false;
+
+        clip.moved.connect(on_clip_moved);
+        clip.updated.connect(on_clip_updated);
+
+        Gdk.Color.parse("000", out color_black);
+        get_clip_colors();
+
+        set_flags(Gtk.WidgetFlags.NO_WINDOW);
+
+        adjust_size(height);
+    }
+
+    void get_clip_colors() {
+        if (clip.clipfile.is_online()) {
+            Gdk.Color.parse(clip.type == Model.MediaType.VIDEO ? "#d82" : "#84a", 
+                out color_selected);
+            Gdk.Color.parse(clip.type == Model.MediaType.VIDEO ? "#da5" : "#b9d", 
+                out color_normal);
+        } else {
+            Gdk.Color.parse("red", out color_selected);
+            Gdk.Color.parse("#AA0000", out color_normal);
+        }
+    }
+
+    void on_clip_updated() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_updated");
+        get_clip_colors();
+        queue_draw();
+    }
+
+    // Note that a view's size may vary slightly (by a single pixel) depending on its
+    // starting position.  This is because the clip's length may not be an integer number of
+    // pixels, and may get rounded either up or down depending on the clip position.
+    public void adjust_size(int height) {
+        int width = time_provider.time_to_xpos(clip.start + clip.duration) -
+                    time_provider.time_to_xpos(clip.start);
+        set_size_request(width + 1, height);
+    }
+
+    public void on_clip_moved(Model.Clip clip) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_moved");
+        adjust_size(height);
+        clip_moved(this);
+    }
+
+    public void delete_clip() {
+        clip_deleted(clip);
+    }
+
+    public void draw() {
+        weak Gdk.Color fill = is_selected ? color_selected : color_normal;
+
+        bool left_trimmed = clip.media_start != 0 && !clip.is_recording;
+
+        bool right_trimmed = clip.clipfile.is_online() ? 
+                              (clip.media_start + clip.duration != clip.clipfile.length) : false;
+
+        if (!left_trimmed && !right_trimmed) {
+            draw_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
+                                   allocation.width - 2, allocation.height - 2);
+            draw_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
+                                   allocation.width - 1, allocation.height - 1);
+
+        } else if (!left_trimmed && right_trimmed) {
+            draw_left_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
+                                        allocation.width - 2, allocation.height - 2);
+            draw_left_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
+                                   allocation.width - 1, allocation.height - 1);
+
+        } else if (left_trimmed && !right_trimmed) {
+            draw_right_rounded_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
+                                         allocation.width - 2, allocation.height - 2);
+            draw_right_rounded_rectangle(window, color_black, false, allocation.x, allocation.y,
+                                         allocation.width - 1, allocation.height - 1);
+
+        } else {
+            draw_square_rectangle(window, fill, true, allocation.x + 1, allocation.y + 1,
+                                  allocation.width - 2, allocation.height - 2);
+            draw_square_rectangle(window, color_black, false, allocation.x, allocation.y,
+                                  allocation.width - 1, allocation.height - 1);
+        }
+
+        Gdk.GC gc = new Gdk.GC(window);
+        Gdk.Rectangle r = { 0, 0, 0, 0 };
+
+        // Due to a Vala compiler bug, we have to do this initialization here...
+        r.x = allocation.x;
+        r.y = allocation.y;
+        r.width = allocation.width;
+        r.height = allocation.height;
+
+        gc.set_clip_rectangle(r);
+
+        Pango.Layout layout;
+        if (clip.is_recording) {
+            layout = create_pango_layout("Recording");
+        } else if (!clip.clipfile.is_online()) {
+            layout = create_pango_layout("%s (Offline)".printf(clip.name));
+        }
+        else {
+            layout = create_pango_layout("%s".printf(clip.name));
+        }
+        int width, height;
+        layout.get_pixel_size(out width, out height);
+        Gdk.draw_layout(window, gc, allocation.x + 10, allocation.y + height, layout);
+    }
+
+    public override bool expose_event(Gdk.EventExpose event) {
+        draw();
+        return true;
+    }
+
+    public override bool button_press_event(Gdk.EventButton event) {
+        if (!transport_delegate.is_stopped()) {
+            return true;
+        }
+
+        event.x -= allocation.x;
+        bool primary_press = event.button == 1;
+        if (primary_press) {
+            button_down = true;
+            drag_point = (int)event.x;
+            snap_amount = 0;
+            snapped = false;
+        }
+
+        bool extend_selection = (event.state & Gdk.ModifierType.CONTROL_MASK) != 0;
+        // The clip is not responsible for changing the selection state.
+        // It may depend upon knowledge of multiple clips.  Let anyone who is interested
+        // update our state.
+        if (is_left_trim(event.x, event.y)) {
+            selection_request(this, false);
+            if (primary_press) {
+                trim_begin(this, Gdk.WindowEdge.WEST);
+                motion_mode = MotionMode.LEFT_TRIM;
+            }
+        } else if (is_right_trim(event.x, event.y)){
+            selection_request(this, false);
+            if (primary_press) {
+                trim_begin(this, Gdk.WindowEdge.EAST);
+                motion_mode = MotionMode.RIGHT_TRIM;
+            }
+        } else {
+            if (!is_selected) {
+                pending_selection = false;
+                selection_request(this, extend_selection);
+            } else {
+                pending_selection = true;
+            }
+        }
+
+        if (event.button == 3) {
+            context_menu.select_first(true);
+            context_menu.popup(null, null, null, event.button, event.time);
+        } else {
+            context_menu.popdown();
+        }
+
+        return true;
+    }
+
+    public override bool button_release_event(Gdk.EventButton event) {
+        if (!transport_delegate.is_stopped()) {
+            return true;
+        }
+
+        event.x -= allocation.x;
+        button_down = false;
+        if (event.button == 1) {
+            switch (motion_mode) {
+                case MotionMode.NONE: {
+                    if (pending_selection) {
+                        selection_request(this, true);
+                    }
+                }
+                break;
+                case MotionMode.DRAGGING: {
+                    int64 delta = time_provider.xsize_to_time((int) event.x - drag_point);
+                    if (motion_mode == MotionMode.DRAGGING) {
+                        move_commit(this, delta);
+                    }
+                }
+                break;
+                case MotionMode.LEFT_TRIM:
+                    trim_commit(this, Gdk.WindowEdge.WEST);
+                break;
+                case MotionMode.RIGHT_TRIM:
+                    trim_commit(this, Gdk.WindowEdge.EAST);
+                break;
+            }
+        }
+        motion_mode = MotionMode.NONE;
+        return true;
+    }
+
+    public override bool motion_notify_event(Gdk.EventMotion event) {
+        if (!transport_delegate.is_stopped()) {
+            return true;
+        }
+
+        event.x -= allocation.x;
+        int delta_pixels = (int)(event.x - drag_point) - snap_amount;
+        if (snapped) {
+            snap_amount += delta_pixels;
+            if (snap_amount.abs() < SNAP_DELTA) {
+                return true;
+            }
+            delta_pixels += snap_amount;
+            snap_amount = 0;
+            snapped = false;
+        }
+
+        int64 delta_time = time_provider.xsize_to_time(delta_pixels);
+
+        switch (motion_mode) {
+            case MotionMode.NONE:
+                if (!button_down && is_left_trim(event.x, event.y)) {
+                    window.set_cursor(left_trim_cursor);
+                } else if (!button_down && is_right_trim(event.x, event.y)) {
+                    window.set_cursor(right_trim_cursor);
+                } else if (is_selected && button_down) {
+                    if (delta_pixels.abs() > MIN_DRAG) {
+                        bool do_copy = (event.state & Gdk.ModifierType.CONTROL_MASK) != 0;
+                        if (do_copy) {
+                            window.set_cursor(plus_cursor);
+                        } else {
+                            window.set_cursor(hand_cursor);
+                        }
+                        motion_mode = MotionMode.DRAGGING;
+                        move_begin(this, do_copy);
+                    }
+                } else {
+                    window.set_cursor(null);
+                }
+            break;
+            case MotionMode.RIGHT_TRIM:
+            case MotionMode.LEFT_TRIM:
+                if (button_down) {
+                    if (motion_mode == MotionMode.LEFT_TRIM) {
+                        clip.trim(delta_time, Gdk.WindowEdge.WEST);
+                    } else {
+                        int64 duration = clip.duration;
+                        clip.trim(delta_time, Gdk.WindowEdge.EAST);
+                        if (duration != clip.duration) {
+                            drag_point += (int)delta_pixels;
+                        }
+                    }
+                }
+                return true;
+            case MotionMode.DRAGGING:
+                move_request(this, delta_time);
+                return true;
+        }
+        return false;
+    }
+
+    bool is_trim_height(double y) {
+        return y - allocation.y > allocation.height / 2;
+    }
+
+    bool is_left_trim(double x, double y) {
+        return is_trim_height(y) && x > 0 && x < TRIM_WIDTH;
+    }
+
+    bool is_right_trim(double x, double y) {
+        return is_trim_height(y) && x > allocation.width - TRIM_WIDTH && 
+            x < allocation.width;
+    }
+
+    public void select() {
+        if (!is_selected) {
+            selection_request(this, true);
+        }
+    }
+    
+    public void snap(int64 amount) {
+        snap_amount = time_provider.time_to_xsize(amount);
+        snapped = true;
+    }
+}