Initial commit
[fillmore] / src / marina / ClipLibraryView.vala
diff --git a/src/marina/ClipLibraryView.vala b/src/marina/ClipLibraryView.vala
new file mode 100644 (file)
index 0000000..c405add
--- /dev/null
@@ -0,0 +1,487 @@
+/* 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 ClipLibraryView : Gtk.EventBox {
+    public static Gtk.Menu context_menu;
+    Model.Project project;
+    Gtk.TreeView tree_view;
+    Gtk.TreeSelection selection;
+    Gtk.Label label = null;
+    Gtk.ListStore list_store;
+    int num_clipfiles;
+    Gee.ArrayList<string> files_dragging = new Gee.ArrayList<string>();
+
+    Gtk.IconTheme icon_theme;
+
+    Gdk.Pixbuf default_audio_icon;
+    Gdk.Pixbuf default_video_icon;
+    Gdk.Pixbuf default_error_icon;
+
+    enum SortMode {
+        NONE,
+        ABC
+    }
+
+    enum ColumnType {
+        THUMBNAIL,
+        NAME,
+        DURATION,
+        FILENAME
+    }
+
+    public signal void selection_changed(bool selected);
+
+    SortMode sort_mode;
+    Model.TimeSystem time_provider;
+
+    public ClipLibraryView(Model.Project p, Model.TimeSystem time_provider, string? drag_message,
+            Gdk.DragAction actions) {
+        Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, drag_target_entries, Gdk.DragAction.COPY);
+        project = p;
+        this.time_provider = time_provider;
+
+        icon_theme = Gtk.IconTheme.get_default();
+
+        list_store = new Gtk.ListStore(4, typeof (Gdk.Pixbuf), typeof (string),
+                                       typeof (string), typeof (string), -1);
+
+        tree_view = new Gtk.TreeView.with_model(list_store);
+
+        add_column(ColumnType.THUMBNAIL);
+        Gtk.TreeViewColumn name_column = add_column(ColumnType.NAME);
+        add_column(ColumnType.DURATION);
+        list_store.set_default_sort_func(name_sort);
+        list_store.set_sort_column_id(name_column.get_sort_column_id(), Gtk.SortType.ASCENDING);
+
+        num_clipfiles = 0;
+        if (drag_message != null) {
+            label = new Gtk.Label(drag_message);
+            label.modify_fg(Gtk.StateType.NORMAL, parse_color("#fff"));
+        }
+
+        modify_bg(Gtk.StateType.NORMAL, parse_color("#444"));
+        tree_view.modify_base(Gtk.StateType.NORMAL, parse_color("#444"));
+
+        tree_view.set_headers_visible(false);
+        project.clipfile_added.connect(on_clipfile_added);
+        project.clipfile_removed.connect(on_clipfile_removed);
+        project.cleared.connect(on_remove_all_rows);
+        project.time_signature_changed.connect(on_time_signature_changed);
+
+        Gtk.drag_source_set(tree_view, Gdk.ModifierType.BUTTON1_MASK, drag_target_entries, actions);
+        tree_view.drag_begin.connect(on_drag_begin);
+        tree_view.drag_data_get.connect(on_drag_data_get);
+        tree_view.cursor_changed.connect(on_cursor_changed);
+
+        selection = tree_view.get_selection();
+        selection.set_mode(Gtk.SelectionMode.MULTIPLE);
+        if (label != null) {
+            add(label);
+        }
+
+        // We have to have our own button press and release handlers
+        // since the normal drag-selection handling does not allow you
+        // to click outside any cell in the library to clear your selection,
+        // and also does not allow dragging multiple clips from the library
+        // to the timeline
+        tree_view.button_press_event.connect(on_button_pressed);
+        tree_view.button_release_event.connect(on_button_released);
+
+        try {
+            default_audio_icon =
+                icon_theme.load_icon("audio-x-generic", 32, (Gtk.IconLookupFlags) 0);
+            default_video_icon =
+                icon_theme.load_icon("video-x-generic", 32, (Gtk.IconLookupFlags) 0);
+            default_error_icon =
+                icon_theme.load_icon("error", 32, (Gtk.IconLookupFlags) 0);
+        } catch (GLib.Error e) {
+            // TODO: what shall we do if these icons are not available?
+        }
+
+        sort_mode = SortMode.ABC;
+    }
+
+    Gtk.TreePath? find_first_selected() {
+        Gtk.TreeIter it;
+        Gtk.TreeModel model = tree_view.get_model();
+
+        bool b = model.get_iter_first(out it);
+        while (b) {
+            Gtk.TreePath path = model.get_path(it);
+            if (selection.path_is_selected(path))
+                return path;
+
+            b = model.iter_next(ref it);
+        }
+        return null;
+    }
+
+    bool on_button_pressed(Gdk.EventButton b) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_button_pressed");
+
+        Gtk.TreePath path;
+        int cell_x;
+        int cell_y;
+
+        tree_view.get_path_at_pos((int) b.x, (int) b.y, out path, null, out cell_x, out cell_y);
+
+        if (path == null) {
+            selection.unselect_all();
+            return true;
+        }
+
+        bool shift_pressed = (b.state & Gdk.ModifierType.SHIFT_MASK) != 0;
+        bool control_pressed = (b.state & Gdk.ModifierType.CONTROL_MASK) != 0;
+
+        if (!control_pressed &&
+            !shift_pressed) {
+            if (!selection.path_is_selected(path))
+                selection.unselect_all();
+        } else {
+            if (shift_pressed) {
+                Gtk.TreePath first = find_first_selected();
+
+                if (first != null)
+                    selection.select_range(first, path);
+            }
+        }
+        selection.select_path(path);
+
+        if (b.button == 3) {
+            selection_changed(true);
+            context_menu.select_first(true);
+            context_menu.popup(null, null, null, 0, b.time);
+        } else {
+            context_menu.popdown();
+        }
+
+        return true;
+    }
+
+    bool on_button_released(Gdk.EventButton b) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_button_released");
+        Gtk.TreePath path;
+        Gtk.TreeViewColumn column;
+
+        int cell_x;
+        int cell_y;
+
+        tree_view.get_path_at_pos((int) b.x, (int) b.y, out path, 
+                                  out column, out cell_x, out cell_y);
+
+       // The check for cell_x == 0 and cell_y == 0 is here since for whatever reason, this 
+       // function is called when we drop some clips onto the timeline.  We only need to mess with 
+       // the selection if we've actually clicked in the tree view, but I cannot find a way to 
+       // guarantee this, since the coordinates that the Gdk.EventButton structure and the 
+       // (cell_x, cell_y) pair give are always 0, 0 when this happens. 
+       // I can assume that clicking on 0, 0 exactly is next to impossible, so I feel this
+       // strange check is okay.
+
+        if (path == null ||
+            (cell_x == 0 && cell_y == 0)) {
+            selection_changed(false);
+            return true;
+        }
+
+        bool shift_pressed = (b.state & Gdk.ModifierType.SHIFT_MASK) != 0;
+        bool control_pressed = (b.state & Gdk.ModifierType.CONTROL_MASK) != 0;
+
+        if (!control_pressed &&
+            !shift_pressed) {
+            if (selection.path_is_selected(path))
+                selection.unselect_all();
+        }
+        selection.select_path(path);
+        selection_changed(true);
+
+        return true;
+    }
+
+    void on_cursor_changed() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_cursor_changed");
+        selection_changed(has_selection());
+    }
+
+    public void unselect_all() {
+        selection.unselect_all();
+        selection_changed(false);
+    }
+
+    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);
+
+        project.create_clip_importer(null, false, 0, false, (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);
+        }
+    }
+
+    
+    void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, 
+                            uint info, uint time) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_data_get");
+        string uri;
+        string[] uri_array = new string[0];
+
+        foreach (string s in files_dragging) {
+            try {
+                uri = GLib.Filename.to_uri(s);
+            } catch (GLib.ConvertError e) {
+                uri = s;
+                warning("Cannot get URI for %s! (%s)\n", s, e.message);
+            }
+            uri_array += uri;
+        }
+        data.set_uris(uri_array);
+
+        Gtk.drag_set_icon_default(context);
+    }
+
+    int get_selected_rows(out GLib.List<Gtk.TreePath> paths) {
+        paths = selection.get_selected_rows(null);
+        return (int) paths.length();
+    }
+
+    void on_drag_begin(Gdk.DragContext c) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_begin");
+        GLib.List<Gtk.TreePath> paths;
+        if (get_selected_rows(out paths) > 0) {
+            bool set_pixbuf = false;
+            files_dragging.clear();
+            foreach (Gtk.TreePath t in paths) {
+                Gtk.TreeIter iter;
+                list_store.get_iter(out iter, t);
+
+                string filename;
+                list_store.get(iter, ColumnType.FILENAME, out filename, -1);
+                files_dragging.add(filename);
+
+                if (!set_pixbuf) {
+                    Gdk.Pixbuf pixbuf;
+                    list_store.get(iter, ColumnType.THUMBNAIL, out pixbuf, -1);
+
+                    Gtk.drag_set_icon_pixbuf(c, pixbuf, 0, 0);
+                    set_pixbuf = true;
+                }
+            }
+        }
+    }
+
+    Gtk.TreeViewColumn add_column(ColumnType c) {
+        Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
+        Gtk.CellRenderer renderer;
+
+        if (c == ColumnType.THUMBNAIL) {
+            renderer = new Gtk.CellRendererPixbuf();
+        } else {
+            renderer = new Gtk.CellRendererText();
+            Gdk.Color color = parse_color("#FFF");
+            renderer.set("foreground-gdk", &color);
+        }
+
+        column.pack_start(renderer, true);
+        column.set_resizable(true);
+
+        if (c == ColumnType.THUMBNAIL) {
+            column.add_attribute(renderer, "pixbuf", tree_view.append_column(column) - 1);
+        } else {
+            column.add_attribute(renderer, "text", tree_view.append_column(column) - 1);
+        }
+        return column;
+    }
+
+    void update_iter(Gtk.TreeIter it, Model.ClipFile clip_file) {
+        Gdk.Pixbuf icon;
+
+        if (clip_file.is_online()) {
+            if (clip_file.thumbnail == null)
+                icon = (clip_file.is_of_type(Model.MediaType.VIDEO) ? 
+                                                        default_video_icon : default_audio_icon);
+            else {
+                icon = clip_file.thumbnail;
+            }
+        } else {
+            icon = default_error_icon;
+        }
+
+        list_store.set(it, ColumnType.THUMBNAIL, icon,
+                            ColumnType.NAME, isolate_filename(clip_file.filename),
+                            ColumnType.DURATION, time_provider.get_time_duration(clip_file.length),
+                            ColumnType.FILENAME, clip_file.filename, -1);
+    }
+
+    void on_clipfile_added(Model.ClipFile f) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_file_added");
+        Gtk.TreeIter it;
+
+        if (find_clipfile(f, out it) >= 0) {
+            list_store.remove(it);
+        } else {
+            if (num_clipfiles == 0) {
+                if (label != null) {
+                    remove(label);
+                }
+                add(tree_view);
+                tree_view.show();
+            }
+            num_clipfiles++;
+        }
+
+        list_store.append(out it);
+        update_iter(it, f);
+    }
+
+    int find_clipfile(Model.ClipFile f, out Gtk.TreeIter iter) {
+        Gtk.TreeModel model = tree_view.get_model();
+
+        bool b = model.get_iter_first(out iter);
+
+        int i = 0;
+        while (b) {
+            string filename;
+            model.get(iter, ColumnType.FILENAME, out filename);
+
+            if (filename == f.filename)
+                return i;
+
+            i++;
+            b = model.iter_next(ref iter);
+        }
+        return -1;
+    }
+
+    public void on_clipfile_removed(Model.ClipFile f) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_file_removed");
+        Gtk.TreeIter it;
+
+        if (find_clipfile(f, out it) >= 0) {
+            remove_row(ref it);
+        }
+    }
+
+    bool remove_row(ref Gtk.TreeIter it) {
+        bool b = list_store.remove(it);
+        num_clipfiles--;
+        if (num_clipfiles == 0) {
+            remove(tree_view);
+            if (label != null) {
+                add(label);
+                label.show();
+            }
+        }
+        return b;
+    }
+
+    void on_remove_all_rows() {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_remove_all_rows");
+        Gtk.TreeModel model = tree_view.get_model();
+        Gtk.TreeIter iter;
+
+        bool b = model.get_iter_first(out iter);
+
+        while (b) {
+            b = remove_row(ref iter);
+        }
+    }
+
+    void on_time_signature_changed(Fraction time_signature) {
+        emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_time_signature_changed");
+        Gtk.TreeIter iter;
+        bool more_items = list_store.get_iter_first(out iter);
+        while (more_items) {
+            string filename;
+            list_store.get(iter, ColumnType.FILENAME, out filename, -1);
+            Model.ClipFile clip_file = project.find_clipfile(filename);
+            list_store.set(iter, ColumnType.DURATION,
+                time_provider.get_time_duration(clip_file.length), -1);
+            more_items = list_store.iter_next(ref iter);
+        }
+    }
+
+    void delete_row(Gtk.TreeModel model, Gtk.TreePath path) {
+        Gtk.TreeIter it;
+        if (list_store.get_iter(out it, path)) {
+            string filename;
+            model.get(it, ColumnType.FILENAME, out filename, -1);
+            if (project.clipfile_on_track(filename)) {
+                if (DialogUtils.delete_cancel("Clip is in use.  Delete anyway?") !=
+                    Gtk.ResponseType.YES)
+                    return;
+            }
+
+            project.remove_clipfile(filename);
+
+            if (Path.get_dirname(filename) == project.get_audio_path()) {
+                if (DialogUtils.delete_keep("Delete clip from disk?  This action is not undoable.")
+                                                    == Gtk.ResponseType.YES) {
+                    if (FileUtils.unlink(filename) != 0) {
+                        project.error_occurred("Could not delete %s", filename);
+                    }
+                    project.undo_manager.reset();
+                }
+            }
+        }
+    }
+
+    public bool has_selection() {
+        GLib.List<Gtk.TreePath> paths;
+        return get_selected_rows(out paths) != 0;
+    }
+
+    public Gee.ArrayList<string> get_selected_files() {
+        GLib.List<Gtk.TreePath> paths;
+        Gee.ArrayList<string> return_value = new Gee.ArrayList<string>();
+        if (get_selected_rows(out paths) != 0) {
+            foreach (Gtk.TreePath path in paths) {
+                Gtk.TreeIter iter;
+                if (list_store.get_iter(out iter, path)) {
+                    string name;
+                    list_store.get(iter, ColumnType.FILENAME, out name, -1);
+                    return_value.add(name);
+                }
+            }
+        }
+        return return_value;
+    }
+
+    public void delete_selection() {
+        GLib.List<Gtk.TreePath> paths;
+        project.undo_manager.start_transaction("Delete Clips From Library");
+        if (get_selected_rows(out paths) > 0) {
+            for (int i = (int) paths.length() - 1; i >= 0; i--)
+                delete_row(list_store, paths.nth_data(i));
+        }
+        project.undo_manager.end_transaction("Delete Clips From Library");
+    }
+
+    public void select_all() {
+        selection.select_all();
+    }
+
+     int name_sort(Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) {
+        string left;
+        string right;
+        model.get(a, ColumnType.NAME, out left);
+        model.get(b, ColumnType.NAME, out right);
+        return stricmp(left, right);
+     }
+}