Initial commit
[fillmore] / src / lombard / lombard.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 int debug_level;
10 const OptionEntry[] options = {
11     { "debug-level", 0, 0, OptionArg.INT, &debug_level,
12         "Control amount of diagnostic information",
13         "[0 (minimal),5 (maximum)]" },
14     { null }
15 };
16
17 class App : Gtk.Window, TransportDelegate {
18     Gtk.DrawingArea drawing_area;
19
20     Model.VideoProject project;
21     View.VideoOutput video_output;
22     View.AudioOutput audio_output;
23     View.OggVorbisExport export_connector;
24
25     TimeLine timeline;
26     ClipLibraryView library;
27     View.StatusBar status_bar;
28
29     Gtk.HPaned h_pane;
30
31     Gtk.ScrolledWindow library_scrolled;
32     Gtk.ScrolledWindow timeline_scrolled;
33     Gtk.Adjustment h_adjustment;
34
35     double prev_adjustment_lower;
36     double prev_adjustment_upper;
37
38     Gtk.ActionGroup main_group;
39
40     int64 center_time = -1;
41
42     Gtk.VBox vbox = null;
43     Gtk.MenuBar menubar;
44     Gtk.UIManager manager;
45
46     string project_filename;
47     Gee.ArrayList<string> load_errors;
48     bool loading;
49
50     public const string NAME = "Lombard";
51     const string LibraryToggle = "Library";
52
53     const Gtk.ActionEntry[] entries = {
54         { "Project", null, "_Project", null, null, null },
55         { "Open", Gtk.STOCK_OPEN, "_Open...", null, null, on_open },
56         { "Save", Gtk.STOCK_SAVE, null, null, null, on_save },
57         { "SaveAs", Gtk.STOCK_SAVE_AS, "Save _As...", "<Shift><Control>S", null, on_save_as },
58         { "Play", Gtk.STOCK_MEDIA_PLAY, "_Play / Pause", "space", null, on_play_pause },
59         { "Export", null, "_Export...", "<Control>E", null, on_export },
60         { "Quit", Gtk.STOCK_QUIT, null, null, null, on_quit },
61
62         { "Edit", null, "_Edit", null, null, null },
63         { "Undo", Gtk.STOCK_UNDO, null, "<Control>Z", null, on_undo },
64         { "Cut", Gtk.STOCK_CUT, null, null, null, on_cut },
65         { "Copy", Gtk.STOCK_COPY, null, null, null, on_copy },
66         { "Paste", Gtk.STOCK_PASTE, null, null, null, on_paste },
67         { "Delete", Gtk.STOCK_DELETE, null, "Delete", null, on_delete },
68         { "SelectAll", Gtk.STOCK_SELECT_ALL, null, "<Control>A", null, on_select_all },
69         { "SplitAtPlayhead", null, "_Split at Playhead", "<Control>P", null, on_split_at_playhead },
70         { "TrimToPlayhead", null, "Trim to Play_head", "<Control>H", null, on_trim_to_playhead },
71         { "ClipProperties", Gtk.STOCK_PROPERTIES, "Properti_es", "<Alt>Return", 
72             null, on_clip_properties },
73
74         { "View", null, "_View", null, null, null },
75         { "ZoomIn", Gtk.STOCK_ZOOM_IN, "Zoom _In", "<Control>plus", null, on_zoom_in },
76         { "ZoomOut", Gtk.STOCK_ZOOM_OUT, "Zoom _Out", "<Control>minus", null, on_zoom_out },
77         { "ZoomProject", null, "Fit to _Window", "<Shift>Z", null, on_zoom_to_project },
78
79         { "Go", null, "_Go", null, null, null },
80         { "Start", Gtk.STOCK_GOTO_FIRST, "_Start", "Home", null, on_go_start },
81         { "End", Gtk.STOCK_GOTO_LAST, "_End", "End", null, on_go_end },
82
83         { "Help", null, "_Help", null, null, null },
84         { "Contents", Gtk.STOCK_HELP, "_Contents", "F1", 
85             "More information on Lombard", on_help_contents},
86         { "About", Gtk.STOCK_ABOUT, null, null, null, on_about },
87         { "SaveGraph", null, "Save _Graph", null, "Save graph", on_save_graph }
88     };
89
90     const Gtk.ToggleActionEntry[] check_actions = { 
91         { LibraryToggle, null, "_Library", "F9", null, on_view_library, true },
92         { "Snap", null, "_Snap to Clip Edges", null, null, on_snap, true }
93     };
94
95     const string ui = """
96 <ui>
97   <menubar name="MenuBar">
98     <menu name="Project" action="Project">
99       <menuitem name="Open" action="Open"/>
100       <menuitem name="Save" action="Save"/>
101       <menuitem name="SaveAs" action="SaveAs"/>
102       <separator/>
103       <menuitem name="Play" action="Play"/>
104       <separator/>
105       <menuitem name="Export" action="Export"/>
106       <menuitem name="Quit" action="Quit"/>
107     </menu>
108     <menu name="EditMenu" action="Edit">
109       <menuitem name="EditUndo" action="Undo"/>
110       <separator/>
111       <menuitem name="EditCut" action="Cut"/>
112       <menuitem name="EditCopy" action="Copy"/>
113       <menuitem name="EditPaste" action="Paste"/>
114       <menuitem name="EditDelete" action="Delete"/>
115       <separator/>
116       <menuitem name="EditSelectAll" action="SelectAll"/>
117       <separator/>
118       <menuitem name="ClipSplitAtPlayhead" action="SplitAtPlayhead"/>
119       <menuitem name="ClipTrimToPlayhead" action="TrimToPlayhead"/>
120       <separator/>
121       <menuitem name="ClipViewProperties" action="ClipProperties"/>
122     </menu>
123     <menu name="ViewMenu" action="View">
124         <menuitem name="ViewLibrary" action="Library"/>
125         <separator/>
126         <menuitem name="ViewZoomIn" action="ZoomIn"/>
127         <menuitem name="ViewZoomOut" action="ZoomOut"/>
128         <menuitem name="ViewZoomProject" action="ZoomProject"/>
129         <separator/>
130         <menuitem name="Snap" action="Snap"/>
131     </menu>
132     <menu name="GoMenu" action="Go">
133       <menuitem name="GoStart" action="Start"/>
134       <menuitem name="GoEnd" action="End"/>
135     </menu>
136     <menu name="HelpMenu" action="Help">
137       <menuitem name="HelpContents" action="Contents"/>
138       <separator/>
139       <menuitem name="HelpAbout" action="About"/>
140       <menuitem name="SaveGraph" action="SaveGraph"/>
141     </menu>
142   </menubar>
143
144   <popup name="ClipContextMenu">
145     <menuitem name="ClipContextCut" action="Cut"/>
146     <menuitem name="ClipContextCopy" action="Copy"/>
147     <separator/>
148     <menuitem name="ClipContextProperties" action="ClipProperties"/>
149   </popup>
150   <popup name="LibraryContextMenu">
151     <menuitem name="ClipContextProperties" action="ClipProperties"/>
152   </popup>
153 </ui>
154 """;
155
156     const DialogUtils.filter_description_struct[] filters = {
157         { "Lombard Project Files", Model.Project.LOMBARD_FILE_EXTENSION },
158         { "Fillmore Project Files", Model.Project.FILLMORE_FILE_EXTENSION }
159     };
160
161     const DialogUtils.filter_description_struct[] export_filters = {
162         { "Ogg Files", "ogg" }
163     };
164
165     public App(string? project_file) throws Error {
166         try {
167             set_icon_from_file(
168                 AppDirs.get_resources_dir().get_child("lombard_icon.png").get_path());
169         } catch (GLib.Error e) {
170             warning("Could not load application icon: %s", e.message);
171         }
172         
173         if (debug_level > -1) {
174             set_logging_level((Logging.Level)debug_level);
175         }
176         ClassFactory.set_transport_delegate(this);
177         set_default_size(600, 500);
178         project_filename = project_file;
179
180         load_errors = new Gee.ArrayList<string>();
181         drawing_area = new Gtk.DrawingArea();
182         drawing_area.realize.connect(on_drawing_realize);
183         drawing_area.modify_bg(Gtk.StateType.NORMAL, parse_color("#000"));
184
185         main_group = new Gtk.ActionGroup("main");
186         main_group.add_actions(entries, this);
187         main_group.add_toggle_actions(check_actions, this);
188
189         manager = new Gtk.UIManager();
190         manager.insert_action_group(main_group, 0);
191         try {
192             manager.add_ui_from_string(ui, -1);
193         } catch (Error e) { error("%s", e.message); }
194
195         menubar = (Gtk.MenuBar) get_widget(manager, "/MenuBar");
196
197         project = new Model.VideoProject(project_filename);
198         project.snap_to_clip = true;
199         project.name_changed.connect(set_project_name);
200         project.load_error.connect(on_load_error);
201         project.load_complete.connect(on_load_complete);
202         project.error_occurred.connect(do_error_dialog);
203         project.undo_manager.undo_changed.connect(on_undo_changed);
204         project.media_engine.post_export.connect(on_post_export);
205         project.playstate_changed.connect(on_playstate_changed);
206
207         audio_output = new View.AudioOutput(project.media_engine.get_project_audio_caps());
208         project.media_engine.connect_output(audio_output);
209
210         timeline = new TimeLine(project, project.time_provider,
211             Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
212         timeline.selection_changed.connect(on_timeline_selection_changed);
213         timeline.track_changed.connect(on_track_changed);
214         timeline.drag_data_received.connect(on_drag_data_received);
215         timeline.size_allocate.connect(on_timeline_size_allocate);
216         project.media_engine.position_changed.connect(on_position_changed);
217         project.media_engine.callback_pulse.connect(on_callback_pulse);
218         ClipView.context_menu = (Gtk.Menu) manager.get_widget("/ClipContextMenu");
219         ClipLibraryView.context_menu = (Gtk.Menu) manager.get_widget("/LibraryContextMenu");
220
221         library = new ClipLibraryView(project, project.time_provider, "Drag clips here.",
222             Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
223         library.selection_changed.connect(on_library_selection_changed);
224         library.drag_data_received.connect(on_drag_data_received);
225
226         status_bar = new View.StatusBar(project, project.time_provider, TimeLine.BAR_HEIGHT);
227
228         library_scrolled = new Gtk.ScrolledWindow(null, null);
229         library_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
230         library_scrolled.add_with_viewport(library);
231
232         toggle_library(true);
233
234         Gtk.MenuItem? save_graph = (Gtk.MenuItem?) 
235             get_widget(manager, "/MenuBar/HelpMenu/SaveGraph");
236
237         // TODO: only destroy it if --debug is not specified on the command line
238         // or conversely, only add it if --debug is specified on the command line
239         if (save_graph != null) {
240             save_graph.destroy();
241         }
242
243         add_accel_group(manager.get_accel_group());
244
245         on_undo_changed(false);
246
247         delete_event.connect(on_delete_event);
248
249         if (project_filename == null) {
250             default_track_set();
251             on_load_complete();
252         }
253
254         update_menu();
255         show_all();
256     }
257
258     void default_track_set() {
259         project.add_track(new Model.VideoTrack(project));
260         project.add_track(new Model.AudioTrack(project, "Audio Track"));
261     }
262
263     bool on_delete_event() {
264         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_delete_event");
265         on_quit();
266         return true;
267     }
268
269     void on_quit() {
270         if (project.undo_manager.is_dirty) {
271             switch (DialogUtils.save_close_cancel(this, null, "Save changes before closing?")) {
272                 case Gtk.ResponseType.ACCEPT:
273                     if (!do_save()) {
274                         return;
275                     }
276                     break;
277                 case Gtk.ResponseType.CLOSE:
278                     break;
279                 case Gtk.ResponseType.DELETE_EVENT: // when user presses escape.
280                 case Gtk.ResponseType.CANCEL:
281                     return;
282                 default:
283                     assert(false);
284                     break;
285             }
286         }
287
288         Gtk.main_quit();
289     }
290
291     void toggle_library(bool showing) {
292         if (vbox == null) {
293             vbox = new Gtk.VBox(false, 0);
294             vbox.pack_start(menubar, false, false, 0);
295
296             Gtk.VPaned v_pane = new Gtk.VPaned();
297             v_pane.set_position(290);
298
299             h_pane = new Gtk.HPaned();
300             h_pane.set_position(300);
301             h_pane.child2_resize = 1;
302             h_pane.child1_resize = 0;
303
304             if (showing) {
305                 h_pane.add1(library_scrolled);
306                 h_pane.add2(drawing_area);
307             } else {
308                 h_pane.add2(drawing_area);
309             }
310             h_pane.child2.size_allocate.connect(on_library_size_allocate);
311             v_pane.add1(h_pane);
312
313             timeline_scrolled = new Gtk.ScrolledWindow(null, null);
314             timeline_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
315             timeline_scrolled.add_with_viewport(timeline);
316
317             Gtk.VBox timeline_vbox = new Gtk.VBox(false, 0);
318             timeline_vbox.pack_start(status_bar, false, false, 0);
319             timeline_vbox.pack_start(timeline_scrolled, true, true, 0);
320             v_pane.add2(timeline_vbox);
321
322             v_pane.child1_resize = 1;
323             v_pane.child2_resize = 0;
324
325             h_adjustment = timeline_scrolled.get_hadjustment();
326             h_adjustment.changed.connect(on_adjustment_changed);
327             prev_adjustment_lower = h_adjustment.get_lower();
328             prev_adjustment_upper = h_adjustment.get_upper();
329
330             vbox.pack_start(v_pane, true, true, 0);
331
332             add(vbox);
333         } else {
334             project.library_visible = showing;
335             if (showing) {
336                 h_pane.add1(library_scrolled);
337             } else {
338                 h_pane.remove(library_scrolled);
339             }
340         }
341         show_all();
342     }
343
344     void on_drawing_realize() {
345         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_drawing_realize");
346         loading = true;
347         project.load(project_filename);
348         try {
349             video_output = new View.VideoOutput(drawing_area);
350             project.media_engine.connect_output(video_output);
351         } catch (Error e) {
352             do_error_dialog("Could not create video output", e.message);
353         }
354     }
355
356     void on_adjustment_changed(Gtk.Adjustment a) {
357         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_adjustment_changed");
358         if (prev_adjustment_upper != a.get_upper() ||
359             prev_adjustment_lower != a.get_lower()) {
360
361             prev_adjustment_lower = a.get_lower();
362             prev_adjustment_upper = a.get_upper();
363         }
364     }
365
366     void on_drag_data_received(Gtk.Widget w, Gdk.DragContext context, int x, int y,
367                                 Gtk.SelectionData selection_data, uint drag_info, uint time) {
368         present();
369     }
370
371     public void set_project_name(string? filename) {
372         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "set_project_name");
373         set_title(project.get_file_display_name());
374     }
375
376     public static void do_error_dialog(string message, string? minor_message) {
377         DialogUtils.error(message, minor_message);
378     }
379
380     public void on_load_error(string message) {
381         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_error");
382         load_errors.add(message);
383     }
384
385     public void on_load_complete() {
386         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_complete");
387         queue_draw();
388         if (project.find_video_track() == null) {
389             project.add_track(new Model.VideoTrack(project));
390         }
391
392         project.media_engine.pipeline.set_state(Gst.State.PAUSED);
393         h_pane.set_position(h_pane.allocation.width - project.library_width);
394         Gtk.ToggleAction action = main_group.get_action(LibraryToggle) as Gtk.ToggleAction;
395         if (action.get_active() != project.library_visible) {
396             action.set_active(project.library_visible);
397         }
398
399         action = main_group.get_action("Snap") as Gtk.ToggleAction;
400         if (action.get_active() != project.snap_to_clip) {
401             action.set_active(project.snap_to_clip);
402         }
403
404         if (project.library_visible) {
405             if (h_pane.child1 != library_scrolled) {
406                 h_pane.add1(library_scrolled);
407             }
408         } else {
409             if (h_pane.child1 == library_scrolled) {
410                 h_pane.remove(library_scrolled);
411             }
412         }
413
414         if (load_errors.size > 0) {
415             string message = "";
416             foreach (string s in load_errors) {
417                 message = message + s + "\n";
418             }
419             do_error_dialog("An error occurred loading the project.", message);
420         }
421         loading = false;
422     }
423
424     void on_library_size_allocate(Gdk.Rectangle rectangle) {
425         if (!loading && h_pane.child1 == library_scrolled) {
426             project.library_width = rectangle.width;
427         }
428     }
429
430     // Loader code
431
432     public void load_file(string name, Model.LibraryImporter im) {
433         if (get_file_extension(name) == Model.Project.LOMBARD_FILE_EXTENSION ||
434             get_file_extension(name) == Model.Project.FILLMORE_FILE_EXTENSION)
435             load_project(name);
436         else {
437             try {
438                 im.add_file(name);
439             } catch (Error e) {
440                 do_error_dialog("Error loading file", e.message);
441             }
442         }
443     }
444
445     void on_open() {
446         load_errors.clear();
447         GLib.SList<string> filenames;
448         if (DialogUtils.open(this, filters, true, true, out filenames)) {
449             project.create_clip_importer(null, false, 0, false, null, 0);
450             project.importer.started.connect(on_importer_started);
451             try {
452                 foreach (string s in filenames) {
453                     string str;
454                     try {
455                         str = GLib.Filename.from_uri(s);
456                     } catch (GLib.ConvertError e) { str = s; }
457                     load_file(str, project.importer);
458                 }
459                 project.importer.start();
460             } catch (Error e) {
461                 do_error_dialog("Could not open file", e.message);
462             }
463         }
464     }
465
466     void on_importer_started(Model.ClipImporter i, int num) {
467         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_importer_started");
468         new MultiFileProgress(this, num, "Import", i);
469     }
470
471     bool do_save_dialog() {
472         string? filename = project.get_project_file();
473         bool create_directory = filename == null;
474         if (DialogUtils.save(this, "Save Project", create_directory, filters, ref filename)) {
475             project.save(filename);
476             return true;
477         }
478         return false;
479     }
480
481     void on_save_as() {
482         do_save_dialog();
483     }
484
485     void on_save() {
486         do_save();
487     }
488
489     bool do_save() {
490         if (project.get_project_file() != null) {
491             project.save(null);
492             return true;
493         }
494         return do_save_dialog();
495     }
496
497     public void load_project(string filename) {
498         loading = true;
499
500         try {
501             project.media_engine.disconnect_output(video_output);
502             video_output = new View.VideoOutput(drawing_area);
503             project.media_engine.connect_output(video_output);
504         } catch (Error e) {
505             do_error_dialog("Could not create video output", e.message);
506         }
507
508         project.load(filename);
509
510     }
511
512     const float SCROLL_MARGIN = 0.05f;
513
514     void scroll_toward_center(int xpos) {
515         int cursor_pos = xpos - (int) h_adjustment.value;
516
517         // Move the cursor position toward the center of the window.  We compute
518         // the remaining distance and move by its square root; this results in
519         // a smooth decelerating motion.
520         int page_size = (int) h_adjustment.page_size;
521         int diff = page_size / 2 - cursor_pos;
522         int d = sign(diff) * (int) Math.sqrt(diff.abs());
523         cursor_pos += d;
524
525         int x = int.max(0, xpos - cursor_pos);
526         int max_value = (int)(h_adjustment.upper - timeline_scrolled.allocation.width);
527         if (x > max_value) {
528             x = max_value;
529         }
530         h_adjustment.set_value(x);
531
532         h_adjustment.set_value(x);
533     }
534
535     public void on_split_at_playhead() {
536         project.split_at_playhead();
537     }
538
539     public void on_trim_to_playhead() {
540         project.trim_to_playhead();
541     }
542
543     public void on_clip_properties() {
544         Fraction? frames_per_second = null;
545         project.get_framerate_fraction(out frames_per_second);
546         if (library.has_selection()) {
547             Gee.ArrayList<string> files = library.get_selected_files();
548             if (files.size == 1) {
549                 string file_name = files.get(0);
550                 Model.ClipFile? clip_file = project.find_clipfile(file_name);
551                 DialogUtils.show_clip_properties(this, null, clip_file, frames_per_second);
552             }
553         } else {
554             Gee.ArrayList<ClipView> clips = timeline.selected_clips;
555             if (clips.size == 1) {
556                 ClipView clip_view = clips.get(0);
557                 DialogUtils.show_clip_properties(this, clip_view, null, frames_per_second);
558             }
559         }
560     }
561
562     public void on_position_changed() {
563         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_position_changed");
564         update_menu();
565     }
566
567     void on_callback_pulse() {
568         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_callback_pulse");
569         if (project.transport_is_playing()) {
570             scroll_toward_center(project.time_provider.time_to_xpos(project.media_engine.position));
571         }
572         timeline.queue_draw();
573     }
574
575     public void on_track_changed() {
576         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_track_changed");
577         update_menu();
578     }
579
580     void update_menu() {
581         bool library_selected = library.has_selection();
582         bool clip_selected = timeline.is_clip_selected();
583         bool stopped = is_stopped();
584         bool clip_is_trimmed = false;
585         bool playhead_on_clip = project.playhead_on_clip();
586         bool dir;
587         bool can_trim = project.can_trim(out dir);
588         bool one_selected = false;
589         if (library_selected) {
590             one_selected = library.get_selected_files().size == 1;
591         } else if (clip_selected) {
592             one_selected = timeline.selected_clips.size == 1;
593         }
594
595         if (clip_selected) {
596             foreach (ClipView clip_view in timeline.selected_clips) {
597                 clip_is_trimmed = clip_view.clip.is_trimmed();
598                 if (clip_is_trimmed) {
599                     break;
600                 }
601             }
602         }
603         // File menu
604         set_sensitive_group(main_group, "Open", stopped);
605         set_sensitive_group(main_group, "Save", stopped);
606         set_sensitive_group(main_group, "SaveAs", stopped);
607         set_sensitive_group(main_group, "Export", project.can_export());
608
609         // Edit Menu
610         set_sensitive_group(main_group, "Undo", stopped && project.undo_manager.can_undo);
611         set_sensitive_group(main_group, "Delete", stopped && (clip_selected || library_selected));
612         set_sensitive_group(main_group, "Cut", stopped && clip_selected);
613         set_sensitive_group(main_group, "Copy", stopped && clip_selected);
614         set_sensitive_group(main_group, "Paste", stopped && timeline.clipboard.clips.size > 0);
615         set_sensitive_group(main_group, "ClipProperties", one_selected);
616
617         set_sensitive_group(main_group, "SplitAtPlayhead", stopped && playhead_on_clip);
618         set_sensitive_group(main_group, "TrimToPlayhead", stopped && can_trim);
619         
620         // View Menu
621         set_sensitive_group(main_group, "ZoomProject", project.get_length() != 0);
622
623     }
624
625     public void on_timeline_selection_changed(bool selected) { 
626         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_timeline_selection_changed");
627         if (selected)
628             library.unselect_all();
629         update_menu();
630     }
631
632     public void on_library_selection_changed(bool selected) {
633         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_library_selection_changed");
634         if (selected) {
635             timeline.deselect_all_clips();
636             timeline.queue_draw();
637         }
638         update_menu();
639     }
640
641     // We must use a key press event to handle the up arrow and down arrow keys,
642     // since GTK does not allow them to be used as accelerators.
643     public override bool key_press_event(Gdk.EventKey event) {
644         switch (event.keyval) {
645             case KeySyms.KP_Enter:
646             case KeySyms.Return:
647                 if ((event.state & GDK_SHIFT_ALT_CONTROL_MASK) != 0)
648                     return base.key_press_event(event);
649                 on_go_start();
650                 break;
651             case KeySyms.Left:
652                 if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
653                     project.go_previous();
654                 } else {
655                     project.go_previous_frame();
656                 }
657                 break;
658             case KeySyms.Right:
659                 if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
660                     project.go_next();
661                 } else {
662                     project.go_next_frame();
663                 }
664                 break;
665             case KeySyms.KP_Add:
666             case KeySyms.equal:
667             case KeySyms.plus:
668                 on_zoom_in();
669                 break;
670             case KeySyms.KP_Subtract:
671             case KeySyms.minus:
672             case KeySyms.underscore:
673                 on_zoom_out();
674                 break;
675             default:
676                 return base.key_press_event(event);
677         }
678         return true;
679     }
680
681     void on_snap() {
682         project.snap_to_clip = !project.snap_to_clip;
683     }
684
685     void on_view_library() {
686         Gtk.ToggleAction action = main_group.get_action(LibraryToggle) as Gtk.ToggleAction;
687         toggle_library(action.get_active());
688     }
689
690     int64 get_zoom_center_time() {
691         return project.transport_get_position();
692     }
693
694     void do_zoom(float increment) {
695         center_time = get_zoom_center_time();
696         timeline.zoom(increment);
697     }
698
699     void on_zoom_in() {
700         do_zoom(0.1f);
701     }
702
703     void on_zoom_out() {
704         do_zoom(-0.1f);
705     }
706
707     void on_zoom_to_project() {
708         timeline.zoom_to_project(h_adjustment.page_size);
709     }
710
711     void on_timeline_size_allocate(Gdk.Rectangle rectangle) {
712         if (center_time != -1) {
713             int new_center_pixel = project.time_provider.time_to_xpos(center_time);
714             int page_size = (int)(h_adjustment.get_page_size() / 2);
715             h_adjustment.clamp_page(new_center_pixel - page_size, new_center_pixel + page_size);
716             center_time = -1;
717         }
718     }
719
720     void set_sensitive_group(Gtk.ActionGroup group, string group_path, bool sensitive) {
721         Gtk.Action action = group.get_action(group_path);
722         action.set_sensitive(sensitive);
723     }
724
725     // File commands
726
727     void on_play_pause() {
728         if (project.transport_is_playing())
729             project.media_engine.pause();
730         else {
731         // TODO: we should be calling play() here, which in turn would call 
732         // do_play(Model.PlayState).  This is not currently how the code is organized.
733         // This is part of a checkin that is already large, so putting this off for another
734         // checkin for ease of testing.
735             project.media_engine.do_play(PlayState.PLAYING);
736         }
737     }
738
739     void on_export() {
740         string filename = null;
741         if (DialogUtils.save(this, "Export", false, export_filters, ref filename)) {
742             new MultiFileProgress(this, 1, "Export", project.media_engine);
743             project.media_engine.disconnect_output(audio_output);
744             project.media_engine.disconnect_output(video_output);
745             try {
746                 export_connector = new View.OggVorbisExport(
747                     View.MediaConnector.MediaTypes.Audio | View.MediaConnector.MediaTypes.Video,
748                     filename, project.media_engine.get_project_audio_export_caps());
749                 project.media_engine.connect_output(export_connector);
750                 project.media_engine.start_export(filename);
751             } catch (Error e) {
752                 do_error_dialog("Could not export file", e.message);
753             }
754         }
755     }
756
757     void on_post_export(bool canceled) {
758         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_post_export");
759         project.media_engine.disconnect_output(export_connector);
760         project.media_engine.connect_output(audio_output);
761         project.media_engine.connect_output(video_output);
762         if (canceled) {
763             GLib.FileUtils.remove(export_connector.get_filename());
764         }
765         export_connector = null;
766     }
767
768     // Edit commands
769
770     void on_undo() {
771         project.undo();
772     }
773
774     void on_delete() {
775         if (library.has_selection())
776             library.delete_selection();
777         else
778             timeline.delete_selection();
779     }
780
781     void on_cut() {
782         timeline.do_cut();
783     }
784
785     void on_copy() {
786         timeline.do_copy();
787     }
788
789     void on_paste() {
790         timeline.paste();
791     }
792
793     void on_playstate_changed(PlayState playstate) {
794         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_playstate_changed");
795         if (playstate == PlayState.STOPPED) {
796             update_menu();
797         }
798     }
799
800     void on_undo_changed(bool can_undo) {
801         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_undo_changed");
802         Gtk.MenuItem? undo = (Gtk.MenuItem?) get_widget(manager, "/MenuBar/EditMenu/EditUndo");
803         assert(undo != null);
804         //undo.set_label("_Undo " + project.undo_manager.get_undo_title());
805         set_sensitive_group(main_group, "Undo", is_stopped() && project.undo_manager.can_undo);
806     }
807
808     void on_select_all() {
809         if (library.has_selection()) {
810             library.select_all();
811         } else {
812             timeline.select_all();
813         }
814     }
815
816     // Go commands
817
818     void on_go_start() { project.go_start(); }
819
820     void on_go_end() { project.go_end(); }
821
822     // Help commands
823
824     void on_help_contents() {
825         try {
826             Gtk.show_uri(null, "http://trac.yorba.org/wiki/UsingLombard0.1", 0);
827         } catch (GLib.Error e) {
828         }
829     }
830
831     void on_about() {
832         Gtk.show_about_dialog(this,
833             "version", project.get_version(),
834             "comments", "A video editor",
835             "copyright", "Copyright 2009-2010 Yorba Foundation",
836             "website", "http://www.yorba.org",
837             "license", project.get_license(),
838             "website-label", "Visit the Yorba web site",
839             "authors", project.authors
840         );
841     }
842
843     void on_save_graph() {
844         project.print_graph(project.media_engine.pipeline, "save_graph");
845     }
846
847     // Transport Delegate methods
848     bool is_recording() {
849         return project.transport_is_recording();
850     }
851
852     bool is_playing() {
853         return project.transport_is_playing();
854     }
855
856     bool is_stopped() {
857         return !(is_playing() || is_recording());
858     }
859 }
860
861 extern const string _PROGRAM_NAME;
862
863 void main(string[] args) {
864     debug_level = -1;
865     OptionContext context = new OptionContext(
866         " [project file] - Create and edit movies");
867     context.add_main_entries(options, null);
868     context.add_group(Gst.init_get_option_group());
869
870     try {
871         context.parse(ref args);
872     } catch (GLib.Error arg_error) {
873         stderr.printf("%s\nRun 'lombard --help' for a full list of available command line options.", 
874             arg_error.message);
875         return;
876     }
877     Gtk.init(ref args);
878
879     try {
880         GLib.Environment.set_application_name("Lombard");
881
882         AppDirs.init(args[0], _PROGRAM_NAME);
883         Gst.init(ref args);
884
885         if (args.length > 2) {
886             stderr.printf("usage: %s [project-file]\n", args[0]);
887             return;
888         }
889
890         string? project_file = null;
891         if (args.length > 1) {
892             project_file = args[1];
893             try {
894                 project_file = GLib.Filename.from_uri(project_file);
895             } catch (GLib.Error e) { }
896         }
897
898         string str = GLib.Environment.get_variable("LOMBARD_DEBUG");
899         debug_enabled = (str != null && (str[0] >= '1'));
900         ClassFactory.set_class_factory(new ClassFactory());
901         View.MediaEngine.can_run();
902
903         new App(project_file);
904         Gtk.main();
905     } catch (Error e) {
906         App.do_error_dialog("Could not launch application", "%s.".printf(e.message));
907     }
908 }
909