Initial commit
[fillmore] / src / marina / ClipLibraryView.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 ClipLibraryView : Gtk.EventBox {
10     public static Gtk.Menu context_menu;
11     Model.Project project;
12     Gtk.TreeView tree_view;
13     Gtk.TreeSelection selection;
14     Gtk.Label label = null;
15     Gtk.ListStore list_store;
16     int num_clipfiles;
17     Gee.ArrayList<string> files_dragging = new Gee.ArrayList<string>();
18
19     Gtk.IconTheme icon_theme;
20
21     Gdk.Pixbuf default_audio_icon;
22     Gdk.Pixbuf default_video_icon;
23     Gdk.Pixbuf default_error_icon;
24
25     enum SortMode {
26         NONE,
27         ABC
28     }
29
30     enum ColumnType {
31         THUMBNAIL,
32         NAME,
33         DURATION,
34         FILENAME
35     }
36
37     public signal void selection_changed(bool selected);
38
39     SortMode sort_mode;
40     Model.TimeSystem time_provider;
41
42     public ClipLibraryView(Model.Project p, Model.TimeSystem time_provider, string? drag_message,
43             Gdk.DragAction actions) {
44         Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, drag_target_entries, Gdk.DragAction.COPY);
45         project = p;
46         this.time_provider = time_provider;
47
48         icon_theme = Gtk.IconTheme.get_default();
49
50         list_store = new Gtk.ListStore(4, typeof (Gdk.Pixbuf), typeof (string),
51                                        typeof (string), typeof (string), -1);
52
53         tree_view = new Gtk.TreeView.with_model(list_store);
54
55         add_column(ColumnType.THUMBNAIL);
56         Gtk.TreeViewColumn name_column = add_column(ColumnType.NAME);
57         add_column(ColumnType.DURATION);
58         list_store.set_default_sort_func(name_sort);
59         list_store.set_sort_column_id(name_column.get_sort_column_id(), Gtk.SortType.ASCENDING);
60
61         num_clipfiles = 0;
62         if (drag_message != null) {
63             label = new Gtk.Label(drag_message);
64             label.modify_fg(Gtk.StateType.NORMAL, parse_color("#fff"));
65         }
66
67         modify_bg(Gtk.StateType.NORMAL, parse_color("#444"));
68         tree_view.modify_base(Gtk.StateType.NORMAL, parse_color("#444"));
69
70         tree_view.set_headers_visible(false);
71         project.clipfile_added.connect(on_clipfile_added);
72         project.clipfile_removed.connect(on_clipfile_removed);
73         project.cleared.connect(on_remove_all_rows);
74         project.time_signature_changed.connect(on_time_signature_changed);
75
76         Gtk.drag_source_set(tree_view, Gdk.ModifierType.BUTTON1_MASK, drag_target_entries, actions);
77         tree_view.drag_begin.connect(on_drag_begin);
78         tree_view.drag_data_get.connect(on_drag_data_get);
79         tree_view.cursor_changed.connect(on_cursor_changed);
80
81         selection = tree_view.get_selection();
82         selection.set_mode(Gtk.SelectionMode.MULTIPLE);
83         if (label != null) {
84             add(label);
85         }
86
87         // We have to have our own button press and release handlers
88         // since the normal drag-selection handling does not allow you
89         // to click outside any cell in the library to clear your selection,
90         // and also does not allow dragging multiple clips from the library
91         // to the timeline
92         tree_view.button_press_event.connect(on_button_pressed);
93         tree_view.button_release_event.connect(on_button_released);
94
95         try {
96             default_audio_icon =
97                 icon_theme.load_icon("audio-x-generic", 32, (Gtk.IconLookupFlags) 0);
98             default_video_icon =
99                 icon_theme.load_icon("video-x-generic", 32, (Gtk.IconLookupFlags) 0);
100             default_error_icon =
101                 icon_theme.load_icon("error", 32, (Gtk.IconLookupFlags) 0);
102         } catch (GLib.Error e) {
103             // TODO: what shall we do if these icons are not available?
104         }
105
106         sort_mode = SortMode.ABC;
107     }
108
109     Gtk.TreePath? find_first_selected() {
110         Gtk.TreeIter it;
111         Gtk.TreeModel model = tree_view.get_model();
112
113         bool b = model.get_iter_first(out it);
114         while (b) {
115             Gtk.TreePath path = model.get_path(it);
116             if (selection.path_is_selected(path))
117                 return path;
118
119             b = model.iter_next(ref it);
120         }
121         return null;
122     }
123
124     bool on_button_pressed(Gdk.EventButton b) {
125         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_button_pressed");
126
127         Gtk.TreePath path;
128         int cell_x;
129         int cell_y;
130
131         tree_view.get_path_at_pos((int) b.x, (int) b.y, out path, null, out cell_x, out cell_y);
132
133         if (path == null) {
134             selection.unselect_all();
135             return true;
136         }
137
138         bool shift_pressed = (b.state & Gdk.ModifierType.SHIFT_MASK) != 0;
139         bool control_pressed = (b.state & Gdk.ModifierType.CONTROL_MASK) != 0;
140
141         if (!control_pressed &&
142             !shift_pressed) {
143             if (!selection.path_is_selected(path))
144                 selection.unselect_all();
145         } else {
146             if (shift_pressed) {
147                 Gtk.TreePath first = find_first_selected();
148
149                 if (first != null)
150                     selection.select_range(first, path);
151             }
152         }
153         selection.select_path(path);
154
155         if (b.button == 3) {
156             selection_changed(true);
157             context_menu.select_first(true);
158             context_menu.popup(null, null, null, 0, b.time);
159         } else {
160             context_menu.popdown();
161         }
162
163         return true;
164     }
165
166     bool on_button_released(Gdk.EventButton b) {
167         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_button_released");
168         Gtk.TreePath path;
169         Gtk.TreeViewColumn column;
170
171         int cell_x;
172         int cell_y;
173
174         tree_view.get_path_at_pos((int) b.x, (int) b.y, out path, 
175                                   out column, out cell_x, out cell_y);
176
177        // The check for cell_x == 0 and cell_y == 0 is here since for whatever reason, this 
178        // function is called when we drop some clips onto the timeline.  We only need to mess with 
179        // the selection if we've actually clicked in the tree view, but I cannot find a way to 
180        // guarantee this, since the coordinates that the Gdk.EventButton structure and the 
181        // (cell_x, cell_y) pair give are always 0, 0 when this happens. 
182        // I can assume that clicking on 0, 0 exactly is next to impossible, so I feel this
183        // strange check is okay.
184
185         if (path == null ||
186             (cell_x == 0 && cell_y == 0)) {
187             selection_changed(false);
188             return true;
189         }
190
191         bool shift_pressed = (b.state & Gdk.ModifierType.SHIFT_MASK) != 0;
192         bool control_pressed = (b.state & Gdk.ModifierType.CONTROL_MASK) != 0;
193
194         if (!control_pressed &&
195             !shift_pressed) {
196             if (selection.path_is_selected(path))
197                 selection.unselect_all();
198         }
199         selection.select_path(path);
200         selection_changed(true);
201
202         return true;
203     }
204
205     void on_cursor_changed() {
206         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_cursor_changed");
207         selection_changed(has_selection());
208     }
209
210     public void unselect_all() {
211         selection.unselect_all();
212         selection_changed(false);
213     }
214
215     public override void drag_data_received(Gdk.DragContext context, int x, int y,
216                                             Gtk.SelectionData selection_data, uint drag_info,
217                                             uint time) {
218         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_data_received");
219         string[] a = selection_data.get_uris();
220         Gtk.drag_finish(context, true, false, time);
221
222         project.create_clip_importer(null, false, 0, false, (Gtk.Window) get_toplevel(), a.length);
223
224         try {
225             foreach (string s in a) {
226                 string filename;
227                 try {
228                     filename = GLib.Filename.from_uri(s);
229                 } catch (GLib.ConvertError e) { continue; }
230                 project.importer.add_file(filename);
231             }
232             project.importer.start();
233         } catch (Error e) {
234             project.error_occurred("Error importing", e.message);
235         }
236     }
237
238     
239     void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, 
240                             uint info, uint time) {
241         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_data_get");
242         string uri;
243         string[] uri_array = new string[0];
244
245         foreach (string s in files_dragging) {
246             try {
247                 uri = GLib.Filename.to_uri(s);
248             } catch (GLib.ConvertError e) {
249                 uri = s;
250                 warning("Cannot get URI for %s! (%s)\n", s, e.message);
251             }
252             uri_array += uri;
253         }
254         data.set_uris(uri_array);
255
256         Gtk.drag_set_icon_default(context);
257     }
258
259     int get_selected_rows(out GLib.List<Gtk.TreePath> paths) {
260         paths = selection.get_selected_rows(null);
261         return (int) paths.length();
262     }
263
264     void on_drag_begin(Gdk.DragContext c) {
265         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drag_begin");
266         GLib.List<Gtk.TreePath> paths;
267         if (get_selected_rows(out paths) > 0) {
268             bool set_pixbuf = false;
269             files_dragging.clear();
270             foreach (Gtk.TreePath t in paths) {
271                 Gtk.TreeIter iter;
272                 list_store.get_iter(out iter, t);
273
274                 string filename;
275                 list_store.get(iter, ColumnType.FILENAME, out filename, -1);
276                 files_dragging.add(filename);
277
278                 if (!set_pixbuf) {
279                     Gdk.Pixbuf pixbuf;
280                     list_store.get(iter, ColumnType.THUMBNAIL, out pixbuf, -1);
281
282                     Gtk.drag_set_icon_pixbuf(c, pixbuf, 0, 0);
283                     set_pixbuf = true;
284                 }
285             }
286         }
287     }
288
289     Gtk.TreeViewColumn add_column(ColumnType c) {
290         Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
291         Gtk.CellRenderer renderer;
292
293         if (c == ColumnType.THUMBNAIL) {
294             renderer = new Gtk.CellRendererPixbuf();
295         } else {
296             renderer = new Gtk.CellRendererText();
297             Gdk.Color color = parse_color("#FFF");
298             renderer.set("foreground-gdk", &color);
299         }
300
301         column.pack_start(renderer, true);
302         column.set_resizable(true);
303
304         if (c == ColumnType.THUMBNAIL) {
305             column.add_attribute(renderer, "pixbuf", tree_view.append_column(column) - 1);
306         } else {
307             column.add_attribute(renderer, "text", tree_view.append_column(column) - 1);
308         }
309         return column;
310     }
311
312     void update_iter(Gtk.TreeIter it, Model.ClipFile clip_file) {
313         Gdk.Pixbuf icon;
314
315         if (clip_file.is_online()) {
316             if (clip_file.thumbnail == null)
317                 icon = (clip_file.is_of_type(Model.MediaType.VIDEO) ? 
318                                                         default_video_icon : default_audio_icon);
319             else {
320                 icon = clip_file.thumbnail;
321             }
322         } else {
323             icon = default_error_icon;
324         }
325
326         list_store.set(it, ColumnType.THUMBNAIL, icon,
327                             ColumnType.NAME, isolate_filename(clip_file.filename),
328                             ColumnType.DURATION, time_provider.get_time_duration(clip_file.length),
329                             ColumnType.FILENAME, clip_file.filename, -1);
330     }
331
332     void on_clipfile_added(Model.ClipFile f) {
333         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_file_added");
334         Gtk.TreeIter it;
335
336         if (find_clipfile(f, out it) >= 0) {
337             list_store.remove(it);
338         } else {
339             if (num_clipfiles == 0) {
340                 if (label != null) {
341                     remove(label);
342                 }
343                 add(tree_view);
344                 tree_view.show();
345             }
346             num_clipfiles++;
347         }
348
349         list_store.append(out it);
350         update_iter(it, f);
351     }
352
353     int find_clipfile(Model.ClipFile f, out Gtk.TreeIter iter) {
354         Gtk.TreeModel model = tree_view.get_model();
355
356         bool b = model.get_iter_first(out iter);
357
358         int i = 0;
359         while (b) {
360             string filename;
361             model.get(iter, ColumnType.FILENAME, out filename);
362
363             if (filename == f.filename)
364                 return i;
365
366             i++;
367             b = model.iter_next(ref iter);
368         }
369         return -1;
370     }
371
372     public void on_clipfile_removed(Model.ClipFile f) {
373         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_file_removed");
374         Gtk.TreeIter it;
375
376         if (find_clipfile(f, out it) >= 0) {
377             remove_row(ref it);
378         }
379     }
380
381     bool remove_row(ref Gtk.TreeIter it) {
382         bool b = list_store.remove(it);
383         num_clipfiles--;
384         if (num_clipfiles == 0) {
385             remove(tree_view);
386             if (label != null) {
387                 add(label);
388                 label.show();
389             }
390         }
391         return b;
392     }
393
394     void on_remove_all_rows() {
395         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_remove_all_rows");
396         Gtk.TreeModel model = tree_view.get_model();
397         Gtk.TreeIter iter;
398
399         bool b = model.get_iter_first(out iter);
400
401         while (b) {
402             b = remove_row(ref iter);
403         }
404     }
405
406     void on_time_signature_changed(Fraction time_signature) {
407         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_time_signature_changed");
408         Gtk.TreeIter iter;
409         bool more_items = list_store.get_iter_first(out iter);
410         while (more_items) {
411             string filename;
412             list_store.get(iter, ColumnType.FILENAME, out filename, -1);
413             Model.ClipFile clip_file = project.find_clipfile(filename);
414             list_store.set(iter, ColumnType.DURATION,
415                 time_provider.get_time_duration(clip_file.length), -1);
416             more_items = list_store.iter_next(ref iter);
417         }
418     }
419
420     void delete_row(Gtk.TreeModel model, Gtk.TreePath path) {
421         Gtk.TreeIter it;
422         if (list_store.get_iter(out it, path)) {
423             string filename;
424             model.get(it, ColumnType.FILENAME, out filename, -1);
425             if (project.clipfile_on_track(filename)) {
426                 if (DialogUtils.delete_cancel("Clip is in use.  Delete anyway?") !=
427                     Gtk.ResponseType.YES)
428                     return;
429             }
430
431             project.remove_clipfile(filename);
432
433             if (Path.get_dirname(filename) == project.get_audio_path()) {
434                 if (DialogUtils.delete_keep("Delete clip from disk?  This action is not undoable.")
435                                                     == Gtk.ResponseType.YES) {
436                     if (FileUtils.unlink(filename) != 0) {
437                         project.error_occurred("Could not delete %s", filename);
438                     }
439                     project.undo_manager.reset();
440                 }
441             }
442         }
443     }
444
445     public bool has_selection() {
446         GLib.List<Gtk.TreePath> paths;
447         return get_selected_rows(out paths) != 0;
448     }
449
450     public Gee.ArrayList<string> get_selected_files() {
451         GLib.List<Gtk.TreePath> paths;
452         Gee.ArrayList<string> return_value = new Gee.ArrayList<string>();
453         if (get_selected_rows(out paths) != 0) {
454             foreach (Gtk.TreePath path in paths) {
455                 Gtk.TreeIter iter;
456                 if (list_store.get_iter(out iter, path)) {
457                     string name;
458                     list_store.get(iter, ColumnType.FILENAME, out name, -1);
459                     return_value.add(name);
460                 }
461             }
462         }
463         return return_value;
464     }
465
466     public void delete_selection() {
467         GLib.List<Gtk.TreePath> paths;
468         project.undo_manager.start_transaction("Delete Clips From Library");
469         if (get_selected_rows(out paths) > 0) {
470             for (int i = (int) paths.length() - 1; i >= 0; i--)
471                 delete_row(list_store, paths.nth_data(i));
472         }
473         project.undo_manager.end_transaction("Delete Clips From Library");
474     }
475
476     public void select_all() {
477         selection.select_all();
478     }
479
480      int name_sort(Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) {
481         string left;
482         string right;
483         model.get(a, ColumnType.NAME, out left);
484         model.get(b, ColumnType.NAME, out right);
485         return stricmp(left, right);
486      }
487 }