summaryrefslogtreecommitdiff
path: root/src/direct
diff options
context:
space:
mode:
Diffstat (limited to 'src/direct')
-rw-r--r--src/direct/Direct.vala35
-rw-r--r--src/direct/DirectPhoto.vala319
-rw-r--r--src/direct/DirectPhotoPage.vala587
-rw-r--r--src/direct/DirectView.vala50
-rw-r--r--src/direct/DirectWindow.vala98
-rw-r--r--src/direct/mk/direct.mk36
6 files changed, 1125 insertions, 0 deletions
diff --git a/src/direct/Direct.vala b/src/direct/Direct.vala
new file mode 100644
index 0000000..dd2a847
--- /dev/null
+++ b/src/direct/Direct.vala
@@ -0,0 +1,35 @@
+/* Copyright 2011-2014 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.
+ */
+
+/* This file is the master unit file for the Direct unit. It should be edited to include
+ * whatever code is deemed necessary.
+ *
+ * The init() and terminate() methods are mandatory.
+ *
+ * If the unit needs to be configured prior to initialization, add the proper parameters to
+ * the preconfigure() method, implement it, and ensure in init() that it's been called.
+ */
+
+namespace Direct {
+
+private File? initial_file = null;
+
+public void preconfigure(File initial_file) {
+ Direct.initial_file = initial_file;
+}
+
+public void init() throws Error {
+ assert(initial_file != null);
+
+ DirectPhoto.init(initial_file);
+}
+
+public void terminate() {
+ DirectPhoto.terminate();
+}
+
+}
+
diff --git a/src/direct/DirectPhoto.vala b/src/direct/DirectPhoto.vala
new file mode 100644
index 0000000..f7271ba
--- /dev/null
+++ b/src/direct/DirectPhoto.vala
@@ -0,0 +1,319 @@
+/* Copyright 2009-2014 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.
+ */
+
+public class DirectPhoto : Photo {
+ private const int PREVIEW_BEST_FIT = 360;
+
+ public static DirectPhotoSourceCollection global = null;
+
+ public signal void can_rotate_changed(bool b);
+
+ private Gdk.Pixbuf preview = null;
+ private bool loaded = false;
+
+ private DirectPhoto(PhotoRow row) {
+ base (row);
+ }
+
+ /**
+ * @brief Because all transformations are discarded on reimport by design, including
+ * Orientation, a JFIF file that is only rotated or flipped, then saved, has the orientation
+ * change the user made before saving removed (recall that fetch() remembers which images it
+ * has seen before and will only add a file to the file map once; every time it sees it
+ * again after this is considered a reimport). This will set the orientation to the
+ * specified value, fixing up both the row and the backing row.
+ *
+ * @warning Only reimported JFIF files should need this; non-lossy image types have their
+ * actual pixels physically rotated in the file when they're exported.
+ *
+ * @param dest The orientation to set the photo to; usually, this should be a value
+ * obtained by calling get_orientation() prior to export()ing a DirectPhoto.
+ */
+ public void fixup_orientation_after_reimport(Orientation dest) {
+ row.orientation = dest;
+ backing_photo_row.original_orientation = dest;
+ }
+
+ public static void init(File initial_file) {
+ init_photo();
+
+ global = new DirectPhotoSourceCollection(initial_file);
+ DirectPhoto photo;
+ string? reason = global.fetch(initial_file, out photo, false);
+ if (reason != null)
+ warning("fetch error: %s", reason);
+ global.add(photo);
+ }
+
+ public static void terminate() {
+ terminate_photo();
+ }
+
+ // Gets the dimensions of this photo's pixbuf when scaled to original
+ // size and saves them where get_raw_dimensions can find them.
+ private void save_dims() {
+ try {
+ backing_photo_row.dim = Dimensions.for_pixbuf(get_pixbuf_with_options(Scaling.for_original(),
+ Exception.CROP | Exception.STRAIGHTEN | Exception.ORIENTATION));
+ } catch (Error e) {
+ warning("Dimensions for image %s could not be gotten.", to_string());
+ }
+ }
+
+ // Loads a photo on demand.
+ public ImportResult demand_load() {
+ if (loaded) {
+ save_dims();
+ return ImportResult.SUCCESS;
+ }
+
+ Photo.ReimportMasterState reimport_state;
+ try {
+ prepare_for_reimport_master(out reimport_state);
+ finish_reimport_master(reimport_state);
+ } catch (Error err) {
+ warning("Database error on re-importing image: %s", err.message);
+ return ImportResult.DATABASE_ERROR;
+ }
+
+ loaded = true;
+ save_dims();
+ return ImportResult.SUCCESS;
+ }
+
+ // This method should only be called by DirectPhotoSourceCollection. Use
+ // DirectPhoto.global.fetch to import files into the system.
+ public static ImportResult internal_import(File file, out DirectPhoto photo) {
+ PhotoImportParams params = new PhotoImportParams.create_placeholder(file, ImportID.generate());
+ Photo.create_pre_import(params);
+ PhotoTable.get_instance().add(params.row);
+
+ photo = new DirectPhoto(params.row);
+
+ return ImportResult.SUCCESS;
+ }
+
+ public override Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error {
+ if (preview == null) {
+ preview = get_thumbnail(PREVIEW_BEST_FIT);
+
+ if (preview == null)
+ preview = get_pixbuf(scaling);
+ }
+
+ return scaling.perform_on_pixbuf(preview, Gdk.InterpType.BILINEAR, true);
+ }
+
+ public override void rotate(Rotation rotation) {
+ can_rotate_now = false;
+ can_rotate_changed(false);
+ base.rotate(rotation);
+ }
+
+ public override Gdk.Pixbuf get_pixbuf(Scaling scaling) throws Error {
+ Gdk.Pixbuf ret = base.get_pixbuf(scaling);
+ can_rotate_changed(true);
+ can_rotate_now = true;
+ return ret;
+ }
+
+ public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
+ return (get_metadata().get_preview_count() == 0) ? null :
+ get_orientation().rotate_pixbuf(get_metadata().get_preview(0).get_pixbuf());
+ }
+
+ protected override void notify_altered(Alteration alteration) {
+ preview = null;
+
+ base.notify_altered(alteration);
+ }
+
+ protected override bool has_user_generated_metadata() {
+ // TODO: implement this method
+ return false;
+ }
+
+ protected override void set_user_metadata_for_export(PhotoMetadata metadata) {
+ // TODO: implement this method, see ticket
+ }
+
+ protected override void apply_user_metadata_for_reimport(PhotoMetadata metadata) {
+ }
+
+ public override bool is_trashed() {
+ // always returns false -- direct-edit mode has no concept of the trash can
+ return false;
+ }
+
+ public override bool is_offline() {
+ // always returns false -- direct-edit mode has no concept of offline photos
+ return false;
+ }
+
+ public override void trash() {
+ // a no-op -- direct-edit mode has no concept of the trash can
+ }
+
+ public override void untrash() {
+ // a no-op -- direct-edit mode has no concept of the trash can
+ }
+
+ public override void mark_offline() {
+ // a no-op -- direct-edit mode has no concept of offline photos
+ }
+
+ public override void mark_online() {
+ // a no-op -- direct-edit mode has no concept of offline photos
+ }
+}
+
+public class DirectPhotoSourceCollection : DatabaseSourceCollection {
+ private const int DISCOVERED_FILES_BATCH_ADD = 500;
+ private Gee.Collection<DirectPhoto> prepared_photos = new Gee.ArrayList<DirectPhoto>();
+ private Gee.HashMap<File, DirectPhoto> file_map = new Gee.HashMap<File, DirectPhoto>(file_hash,
+ file_equal);
+ private DirectoryMonitor monitor;
+
+ public DirectPhotoSourceCollection(File initial_file) {
+ base("DirectPhotoSourceCollection", get_direct_key);
+
+ // only use the monitor for discovery in the specified directory, not its children
+ monitor = new DirectoryMonitor(initial_file.get_parent(), false, false);
+ monitor.file_discovered.connect(on_file_discovered);
+ monitor.discovery_completed.connect(on_discovery_completed);
+
+ monitor.start_discovery();
+ }
+
+ public override bool holds_type_of_source(DataSource source) {
+ return source is DirectPhoto;
+ }
+
+ private static int64 get_direct_key(DataSource source) {
+ DirectPhoto photo = (DirectPhoto) source;
+ PhotoID photo_id = photo.get_photo_id();
+
+ return photo_id.id;
+ }
+
+ public override void notify_items_added(Gee.Iterable<DataObject> added) {
+ foreach (DataObject object in added) {
+ DirectPhoto photo = (DirectPhoto) object;
+ File file = photo.get_file();
+
+ assert(!file_map.has_key(file));
+
+ file_map.set(file, photo);
+ }
+
+ base.notify_items_added(added);
+ }
+
+ public override void notify_items_removed(Gee.Iterable<DataObject> removed) {
+ foreach (DataObject object in removed) {
+ DirectPhoto photo = (DirectPhoto) object;
+ File file = photo.get_file();
+
+ bool is_removed = file_map.unset(file);
+ assert(is_removed);
+ }
+
+ base.notify_items_removed(removed);
+ }
+
+ public bool has_source_for_file(File file) {
+ return file_map.has_key(file);
+ }
+
+ private void on_file_discovered(File file, FileInfo info) {
+ // skip already-seen files
+ if (has_source_for_file(file))
+ return;
+
+ // only add files that look like photo files we support
+ if (!PhotoFileFormat.is_file_supported(file))
+ return;
+
+ DirectPhoto photo;
+ string? reason = fetch(file, out photo, false);
+ if (reason != null)
+ warning("Error fetching file: %s", reason);
+ prepared_photos.add(photo);
+ if (prepared_photos.size >= DISCOVERED_FILES_BATCH_ADD)
+ flush_prepared_photos();
+ }
+
+ private void on_discovery_completed() {
+ flush_prepared_photos();
+ }
+
+ private void flush_prepared_photos() {
+ add_many(prepared_photos);
+ prepared_photos.clear();
+ }
+
+ public bool has_file(File file) {
+ return file_map.has_key(file);
+ }
+
+ public void reimport_photo(DirectPhoto photo) {
+ photo.discard_prefetched();
+ DirectPhoto reimported_photo;
+ fetch(photo.get_file(), out reimported_photo, true);
+ }
+
+ // Returns an error string if unable to fetch, null otherwise
+ public string? fetch(File file, out DirectPhoto photo, bool reimport) {
+ // fetch from the map first, which ensures that only one DirectPhoto exists for each file
+ photo = file_map.get(file);
+ if (photo != null) {
+ string? reason = null;
+
+ if (reimport) {
+ try {
+ Orientation ori_tmp = Orientation.TOP_LEFT;
+ bool should_restore_ori = false;
+
+ if ((photo.only_metadata_changed()) ||
+ (photo.get_file_format() == PhotoFileFormat.JFIF)) {
+ ori_tmp = photo.get_orientation();
+ should_restore_ori = true;
+ }
+
+ Photo.ReimportMasterState reimport_state;
+ if (photo.prepare_for_reimport_master(out reimport_state)) {
+ photo.finish_reimport_master(reimport_state);
+ if (should_restore_ori) {
+ photo.fixup_orientation_after_reimport(ori_tmp);
+ }
+ }
+ else {
+ reason = ImportResult.FILE_ERROR.to_string();
+ }
+ } catch (Error err) {
+ reason = err.message;
+ }
+ }
+
+ return reason;
+ }
+
+ // for DirectPhoto, a fetch on an unknown file is an implicit import into the in-memory
+ // database (which automatically adds the new DirectPhoto object to DirectPhoto.global)
+ ImportResult result = DirectPhoto.internal_import(file, out photo);
+
+ return (result == ImportResult.SUCCESS) ? null : result.to_string();
+ }
+
+ public bool has_file_source(File file) {
+ return file_map.has_key(file);
+ }
+
+ public DirectPhoto? get_file_source(File file) {
+ return file_map.get(file);
+ }
+}
+
diff --git a/src/direct/DirectPhotoPage.vala b/src/direct/DirectPhotoPage.vala
new file mode 100644
index 0000000..4dfd520
--- /dev/null
+++ b/src/direct/DirectPhotoPage.vala
@@ -0,0 +1,587 @@
+/* Copyright 2009-2014 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.
+ */
+
+public class DirectPhotoPage : EditingHostPage {
+ private File initial_file;
+ private DirectViewCollection? view_controller = null;
+ private File current_save_dir;
+ private bool drop_if_dirty = false;
+
+ public DirectPhotoPage(File file) {
+ base (DirectPhoto.global, file.get_basename());
+
+ if (!check_editable_file(file)) {
+ Application.get_instance().panic();
+
+ return;
+ }
+
+ initial_file = file;
+ view_controller = new DirectViewCollection();
+ current_save_dir = file.get_parent();
+
+ DirectPhoto.global.items_altered.connect(on_photos_altered);
+
+ get_view().selection_group_altered.connect(on_selection_group_altered);
+ }
+
+ ~DirectPhotoPage() {
+ DirectPhoto.global.items_altered.disconnect(on_photos_altered);
+ }
+
+ protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
+ base.init_collect_ui_filenames(ui_filenames);
+
+ ui_filenames.add("direct_context.ui");
+ ui_filenames.add("direct.ui");
+ }
+
+ protected override Gtk.ActionEntry[] init_collect_action_entries() {
+ Gtk.ActionEntry[] actions = base.init_collect_action_entries();
+
+ Gtk.ActionEntry file = { "FileMenu", null, TRANSLATABLE, null, null, null };
+ file.label = _("_File");
+ actions += file;
+
+ Gtk.ActionEntry save = { "Save", Gtk.Stock.SAVE, TRANSLATABLE, "<Ctrl>S", TRANSLATABLE,
+ on_save };
+ save.label = _("_Save");
+ save.tooltip = _("Save photo");
+ actions += save;
+
+ Gtk.ActionEntry save_as = { "SaveAs", Gtk.Stock.SAVE_AS, TRANSLATABLE,
+ "<Ctrl><Shift>S", TRANSLATABLE, on_save_as };
+ save_as.label = _("Save _As...");
+ save_as.tooltip = _("Save photo with a different name");
+ actions += save_as;
+
+ Gtk.ActionEntry send_to = { "SendTo", "document-send", TRANSLATABLE, null,
+ TRANSLATABLE, on_send_to };
+ send_to.label = Resources.SEND_TO_MENU;
+ actions += send_to;
+
+ Gtk.ActionEntry print = { "Print", Gtk.Stock.PRINT, TRANSLATABLE, "<Ctrl>P",
+ TRANSLATABLE, on_print };
+ print.label = Resources.PRINT_MENU;
+ print.tooltip = _("Print the photo to a printer connected to your computer");
+ actions += print;
+
+ Gtk.ActionEntry edit = { "EditMenu", null, TRANSLATABLE, null, null, null };
+ edit.label = _("_Edit");
+ actions += edit;
+
+ Gtk.ActionEntry photo = { "PhotoMenu", null, "", null, null, null };
+ photo.label = _("_Photo");
+ actions += photo;
+
+ Gtk.ActionEntry tools = { "Tools", null, TRANSLATABLE, null, null, null };
+ tools.label = _("T_ools");
+ actions += tools;
+
+ Gtk.ActionEntry prev = { "PrevPhoto", Gtk.Stock.GO_BACK, TRANSLATABLE, null,
+ TRANSLATABLE, on_previous_photo };
+ prev.label = _("_Previous Photo");
+ prev.tooltip = _("Previous Photo");
+ actions += prev;
+
+ Gtk.ActionEntry next = { "NextPhoto", Gtk.Stock.GO_FORWARD, TRANSLATABLE, null,
+ TRANSLATABLE, on_next_photo };
+ next.label = _("_Next Photo");
+ next.tooltip = _("Next Photo");
+ actions += next;
+
+ Gtk.ActionEntry rotate_right = { "RotateClockwise", Resources.CLOCKWISE,
+ TRANSLATABLE, "<Ctrl>R", TRANSLATABLE, on_rotate_clockwise };
+ rotate_right.label = Resources.ROTATE_CW_MENU;
+ rotate_right.tooltip = Resources.ROTATE_CCW_TOOLTIP;
+ actions += rotate_right;
+
+ Gtk.ActionEntry rotate_left = { "RotateCounterclockwise", Resources.COUNTERCLOCKWISE,
+ TRANSLATABLE, "<Ctrl><Shift>R", TRANSLATABLE, on_rotate_counterclockwise };
+ rotate_left.label = Resources.ROTATE_CCW_MENU;
+ rotate_left.tooltip = Resources.ROTATE_CCW_TOOLTIP;
+ actions += rotate_left;
+
+ Gtk.ActionEntry hflip = { "FlipHorizontally", Resources.HFLIP, TRANSLATABLE, null,
+ TRANSLATABLE, on_flip_horizontally };
+ hflip.label = Resources.HFLIP_MENU;
+ actions += hflip;
+
+ Gtk.ActionEntry vflip = { "FlipVertically", Resources.VFLIP, TRANSLATABLE, null,
+ TRANSLATABLE, on_flip_vertically };
+ vflip.label = Resources.VFLIP_MENU;
+ actions += vflip;
+
+ Gtk.ActionEntry enhance = { "Enhance", Resources.ENHANCE, TRANSLATABLE, "<Ctrl>E",
+ TRANSLATABLE, on_enhance };
+ enhance.label = Resources.ENHANCE_MENU;
+ enhance.tooltip = Resources.ENHANCE_TOOLTIP;
+ actions += enhance;
+
+ Gtk.ActionEntry crop = { "Crop", Resources.CROP, TRANSLATABLE, "<Ctrl>O",
+ TRANSLATABLE, toggle_crop };
+ crop.label = Resources.CROP_MENU;
+ crop.tooltip = Resources.CROP_TOOLTIP;
+ actions += crop;
+
+ Gtk.ActionEntry straighten = { "Straighten", Gtk.Stock.REFRESH, TRANSLATABLE, "<Ctrl>A",
+ TRANSLATABLE, toggle_straighten };
+ straighten.label = Resources.STRAIGHTEN_MENU;
+ straighten.tooltip = Resources.STRAIGHTEN_TOOLTIP;
+ actions += straighten;
+
+ Gtk.ActionEntry red_eye = { "RedEye", Resources.REDEYE, TRANSLATABLE, "<Ctrl>Y",
+ TRANSLATABLE, toggle_redeye };
+ red_eye.label = Resources.RED_EYE_MENU;
+ red_eye.tooltip = Resources.RED_EYE_TOOLTIP;
+ actions += red_eye;
+
+ Gtk.ActionEntry adjust = { "Adjust", Resources.ADJUST, TRANSLATABLE, "<Ctrl>D",
+ TRANSLATABLE, toggle_adjust };
+ adjust.label = Resources.ADJUST_MENU;
+ adjust.tooltip = Resources.ADJUST_TOOLTIP;
+ actions += adjust;
+
+ Gtk.ActionEntry revert = { "Revert", Gtk.Stock.REVERT_TO_SAVED, TRANSLATABLE,
+ null, TRANSLATABLE, on_revert };
+ revert.label = Resources.REVERT_MENU;
+ actions += revert;
+
+ Gtk.ActionEntry adjust_date_time = { "AdjustDateTime", null, TRANSLATABLE, null,
+ TRANSLATABLE, on_adjust_date_time };
+ adjust_date_time.label = Resources.ADJUST_DATE_TIME_MENU;
+ actions += adjust_date_time;
+
+ Gtk.ActionEntry set_background = { "SetBackground", null, TRANSLATABLE, "<Ctrl>B",
+ TRANSLATABLE, on_set_background };
+ set_background.label = Resources.SET_BACKGROUND_MENU;
+ set_background.tooltip = Resources.SET_BACKGROUND_TOOLTIP;
+ actions += set_background;
+
+ Gtk.ActionEntry view = { "ViewMenu", null, TRANSLATABLE, null, null, null };
+ view.label = _("_View");
+ actions += view;
+
+ Gtk.ActionEntry help = { "HelpMenu", null, TRANSLATABLE, null, null, null };
+ help.label = _("_Help");
+ actions += help;
+
+ Gtk.ActionEntry increase_size = { "IncreaseSize", Gtk.Stock.ZOOM_IN, TRANSLATABLE,
+ "<Ctrl>plus", TRANSLATABLE, on_increase_size };
+ increase_size.label = _("Zoom _In");
+ increase_size.tooltip = _("Increase the magnification of the photo");
+ actions += increase_size;
+
+ Gtk.ActionEntry decrease_size = { "DecreaseSize", Gtk.Stock.ZOOM_OUT, TRANSLATABLE,
+ "<Ctrl>minus", TRANSLATABLE, on_decrease_size };
+ decrease_size.label = _("Zoom _Out");
+ decrease_size.tooltip = _("Decrease the magnification of the photo");
+ actions += decrease_size;
+
+ Gtk.ActionEntry best_fit = { "ZoomFit", Gtk.Stock.ZOOM_FIT, TRANSLATABLE,
+ "<Ctrl>0", TRANSLATABLE, snap_zoom_to_min };
+ best_fit.label = _("Fit to _Page");
+ best_fit.tooltip = _("Zoom the photo to fit on the screen");
+ actions += best_fit;
+
+ Gtk.ActionEntry actual_size = { "Zoom100", Gtk.Stock.ZOOM_100, TRANSLATABLE,
+ "<Ctrl>1", TRANSLATABLE, snap_zoom_to_isomorphic };
+ /// xgettext:no-c-format
+ actual_size.label = _("Zoom _100%");
+ /// xgettext:no-c-format
+ actual_size.tooltip = _("Zoom the photo to 100% magnification");
+ actions += actual_size;
+
+ Gtk.ActionEntry max_size = { "Zoom200", null, TRANSLATABLE,
+ "<Ctrl>2", TRANSLATABLE, snap_zoom_to_max };
+ /// xgettext:no-c-format
+ max_size.label = _("Zoom _200%");
+ /// xgettext:no-c-format
+ max_size.tooltip = _("Zoom the photo to 200% magnification");
+ actions += max_size;
+
+ return actions;
+ }
+
+ protected override InjectionGroup[] init_collect_injection_groups() {
+ InjectionGroup[] groups = base.init_collect_injection_groups();
+
+ InjectionGroup print_group = new InjectionGroup("/MenuBar/FileMenu/PrintPlaceholder");
+ print_group.add_menu_item("Print");
+
+ groups += print_group;
+
+ InjectionGroup bg_group = new InjectionGroup("/MenuBar/FileMenu/SetBackgroundPlaceholder");
+ bg_group.add_menu_item("SetBackground");
+
+ groups += bg_group;
+
+ return groups;
+ }
+
+ private static bool check_editable_file(File file) {
+ if (!FileUtils.test(file.get_path(), FileTest.EXISTS))
+ AppWindow.error_message(_("%s does not exist.").printf(file.get_path()));
+ else if (!FileUtils.test(file.get_path(), FileTest.IS_REGULAR))
+ AppWindow.error_message(_("%s is not a file.").printf(file.get_path()));
+ else if (!PhotoFileFormat.is_file_supported(file))
+ AppWindow.error_message(_("%s does not support the file format of\n%s.").printf(
+ Resources.APP_TITLE, file.get_path()));
+ else
+ return true;
+
+ return false;
+ }
+
+ public override void realize() {
+ if (base.realize != null)
+ base.realize();
+
+ DirectPhoto? photo = DirectPhoto.global.get_file_source(initial_file);
+
+ display_mirror_of(view_controller, photo);
+ initial_file = null;
+ }
+
+ protected override void photo_changing(Photo new_photo) {
+ if (get_photo() != null) {
+ DirectPhoto tmp = get_photo() as DirectPhoto;
+
+ if (tmp != null) {
+ tmp.can_rotate_changed.disconnect(on_dphoto_can_rotate_changed);
+ }
+ }
+
+ ((DirectPhoto) new_photo).demand_load();
+
+ DirectPhoto tmp = new_photo as DirectPhoto;
+
+ if (tmp != null) {
+ tmp.can_rotate_changed.connect(on_dphoto_can_rotate_changed);
+ }
+ }
+
+ public File get_current_file() {
+ return get_photo().get_file();
+ }
+
+ protected override bool on_context_buttonpress(Gdk.EventButton event) {
+ Gtk.Menu context_menu = (Gtk.Menu) ui.get_widget("/DirectContextMenu");
+ popup_context_menu(context_menu, event);
+
+ return true;
+ }
+
+ private void update_zoom_menu_item_sensitivity() {
+ set_action_sensitive("IncreaseSize", !get_zoom_state().is_max() && !get_photo_missing());
+ set_action_sensitive("DecreaseSize", !get_zoom_state().is_default() && !get_photo_missing());
+ }
+
+ protected override void on_increase_size() {
+ base.on_increase_size();
+
+ update_zoom_menu_item_sensitivity();
+ }
+
+ protected override void on_decrease_size() {
+ base.on_decrease_size();
+
+ update_zoom_menu_item_sensitivity();
+ }
+
+ private void on_photos_altered(Gee.Map<DataObject, Alteration> map) {
+ bool contains = false;
+ if (has_photo()) {
+ Photo photo = get_photo();
+ foreach (DataObject object in map.keys) {
+ if (((Photo) object) == photo) {
+ contains = true;
+
+ break;
+ }
+ }
+ }
+
+ bool sensitive = has_photo() && !get_photo_missing();
+ if (sensitive)
+ sensitive = contains;
+
+ set_action_sensitive("Save", sensitive && get_photo().get_file_format().can_write());
+ set_action_sensitive("Revert", sensitive);
+ }
+
+ private void on_selection_group_altered() {
+ // On EditingHostPage, the displayed photo is always selected, so this signal is fired
+ // whenever a new photo is displayed (which even happens on an in-place save; the changes
+ // are written and a new DirectPhoto is loaded into its place).
+ //
+ // In every case, reset the CommandManager, as the command stack is not valid against this
+ // new file.
+ get_command_manager().reset();
+ }
+
+ protected override bool on_double_click(Gdk.EventButton event) {
+ AppWindow.get_instance().end_fullscreen();
+
+ return base.on_double_click(event);
+ }
+
+ protected override void update_ui(bool missing) {
+ bool sensitivity = !missing;
+
+ set_action_sensitive("Save", sensitivity);
+ set_action_sensitive("SaveAs", sensitivity);
+ set_action_sensitive("SendTo", sensitivity);
+ set_action_sensitive("Publish", sensitivity);
+ set_action_sensitive("Print", sensitivity);
+ set_action_sensitive("CommonJumpToFile", sensitivity);
+
+ set_action_sensitive("CommonUndo", sensitivity);
+ set_action_sensitive("CommonRedo", sensitivity);
+
+ set_action_sensitive("IncreaseSize", sensitivity);
+ set_action_sensitive("DecreaseSize", sensitivity);
+ set_action_sensitive("ZoomFit", sensitivity);
+ set_action_sensitive("Zoom100", sensitivity);
+ set_action_sensitive("Zoom200", sensitivity);
+
+ set_action_sensitive("RotateClockwise", sensitivity);
+ set_action_sensitive("RotateCounterclockwise", sensitivity);
+ set_action_sensitive("FlipHorizontally", sensitivity);
+ set_action_sensitive("FlipVertically", sensitivity);
+ set_action_sensitive("Enhance", sensitivity);
+ set_action_sensitive("Crop", sensitivity);
+ set_action_sensitive("Straighten", sensitivity);
+ set_action_sensitive("RedEye", sensitivity);
+ set_action_sensitive("Adjust", sensitivity);
+ set_action_sensitive("Revert", sensitivity);
+ set_action_sensitive("AdjustDateTime", sensitivity);
+ set_action_sensitive("Fullscreen", sensitivity);
+
+ set_action_sensitive("SetBackground", has_photo() && !get_photo_missing());
+
+ base.update_ui(missing);
+ }
+
+ protected override void update_actions(int selected_count, int count) {
+ bool multiple = get_view().get_count() > 1;
+ bool revert_possible = has_photo() ? get_photo().has_transformations()
+ && !get_photo_missing() : false;
+ bool rotate_possible = has_photo() ? is_rotate_available(get_photo()) : false;
+ bool enhance_possible = has_photo() ? is_enhance_available(get_photo()) : false;
+
+ set_action_sensitive("PrevPhoto", multiple);
+ set_action_sensitive("NextPhoto", multiple);
+ set_action_sensitive("RotateClockwise", rotate_possible);
+ set_action_sensitive("RotateCounterclockwise", rotate_possible);
+ set_action_sensitive("FlipHorizontally", rotate_possible);
+ set_action_sensitive("FlipVertically", rotate_possible);
+ set_action_sensitive("Revert", revert_possible);
+ set_action_sensitive("Enhance", enhance_possible);
+
+ set_action_sensitive("SetBackground", has_photo());
+
+ if (has_photo()) {
+ set_action_sensitive("Crop", EditingTools.CropTool.is_available(get_photo(), Scaling.for_original()));
+ set_action_sensitive("RedEye", EditingTools.RedeyeTool.is_available(get_photo(),
+ Scaling.for_original()));
+ }
+
+ // can't write to raws, and trapping the output JPEG here is tricky,
+ // so don't allow date/time changes here.
+ if (get_photo() != null) {
+ set_action_sensitive("AdjustDateTime", (get_photo().get_file_format() != PhotoFileFormat.RAW));
+ } else {
+ set_action_sensitive("AdjustDateTime", false);
+ }
+
+ base.update_actions(selected_count, count);
+ }
+
+ private bool check_ok_to_close_photo(Photo photo) {
+ if (!photo.has_alterations())
+ return true;
+
+ if (drop_if_dirty) {
+ // need to remove transformations, or else they stick around in memory (reappearing
+ // if the user opens the file again)
+ photo.remove_all_transformations();
+
+ return true;
+ }
+
+ bool is_writeable = get_photo().get_file_format().can_write();
+ string save_option = is_writeable ? _("_Save") : _("_Save a Copy");
+
+ Gtk.ResponseType response = AppWindow.negate_affirm_cancel_question(
+ _("Lose changes to %s?").printf(photo.get_basename()), save_option,
+ _("Close _without Saving"));
+
+ if (response == Gtk.ResponseType.YES)
+ photo.remove_all_transformations();
+ else if (response == Gtk.ResponseType.NO) {
+ if (is_writeable)
+ save(photo.get_file(), 0, ScaleConstraint.ORIGINAL, Jpeg.Quality.HIGH,
+ get_photo().get_file_format());
+ else
+ on_save_as();
+ } else if ((response == Gtk.ResponseType.CANCEL) || (response == Gtk.ResponseType.DELETE_EVENT) ||
+ (response == Gtk.ResponseType.CLOSE)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool check_quit() {
+ return check_ok_to_close_photo(get_photo());
+ }
+
+ protected override bool confirm_replace_photo(Photo? old_photo, Photo new_photo) {
+ return (old_photo != null) ? check_ok_to_close_photo(old_photo) : true;
+ }
+
+ private void save(File dest, int scale, ScaleConstraint constraint, Jpeg.Quality quality,
+ PhotoFileFormat format, bool copy_unmodified = false, bool save_metadata = true) {
+ Scaling scaling = Scaling.for_constraint(constraint, scale, false);
+
+ try {
+ get_photo().export(dest, scaling, quality, format, copy_unmodified, save_metadata);
+ } catch (Error err) {
+ AppWindow.error_message(_("Error while saving to %s: %s").printf(dest.get_path(),
+ err.message));
+
+ return;
+ }
+
+ // Fetch the DirectPhoto and reimport.
+ DirectPhoto photo;
+ DirectPhoto.global.fetch(dest, out photo, true);
+
+ DirectView tmp_view = new DirectView(photo);
+ view_controller.add(tmp_view);
+
+ DirectPhoto.global.reimport_photo(photo);
+ display_mirror_of(view_controller, photo);
+ }
+
+ private void on_save() {
+ if (!get_photo().has_alterations() || !get_photo().get_file_format().can_write() ||
+ get_photo_missing())
+ return;
+
+ // save full-sized version right on top of the current file
+ save(get_photo().get_file(), 0, ScaleConstraint.ORIGINAL, Jpeg.Quality.HIGH,
+ get_photo().get_file_format());
+ }
+
+ private void on_save_as() {
+ ExportDialog export_dialog = new ExportDialog(_("Save As"));
+
+ int scale;
+ ScaleConstraint constraint;
+ ExportFormatParameters export_params = ExportFormatParameters.last();
+ if (!export_dialog.execute(out scale, out constraint, ref export_params))
+ return;
+
+ string filename = get_photo().get_export_basename_for_parameters(export_params);
+ PhotoFileFormat effective_export_format =
+ get_photo().get_export_format_for_parameters(export_params);
+
+ string[] output_format_extensions =
+ effective_export_format.get_properties().get_known_extensions();
+ Gtk.FileFilter output_format_filter = new Gtk.FileFilter();
+ foreach(string extension in output_format_extensions) {
+ string uppercase_extension = extension.up();
+ output_format_filter.add_pattern("*." + extension);
+ output_format_filter.add_pattern("*." + uppercase_extension);
+ }
+
+ Gtk.FileChooserDialog save_as_dialog = new Gtk.FileChooserDialog(_("Save As"),
+ AppWindow.get_instance(), Gtk.FileChooserAction.SAVE, Gtk.Stock.CANCEL,
+ Gtk.ResponseType.CANCEL, Gtk.Stock.OK, Gtk.ResponseType.OK);
+ save_as_dialog.set_select_multiple(false);
+ save_as_dialog.set_current_name(filename);
+ save_as_dialog.set_current_folder(current_save_dir.get_path());
+ save_as_dialog.add_filter(output_format_filter);
+ save_as_dialog.set_do_overwrite_confirmation(true);
+ save_as_dialog.set_local_only(false);
+
+ int response = save_as_dialog.run();
+ if (response == Gtk.ResponseType.OK) {
+ // flag to prevent asking user about losing changes to the old file (since they'll be
+ // loaded right into the new one)
+ drop_if_dirty = true;
+ save(File.new_for_uri(save_as_dialog.get_uri()), scale, constraint, export_params.quality,
+ effective_export_format, export_params.mode == ExportFormatMode.UNMODIFIED,
+ export_params.export_metadata);
+ drop_if_dirty = false;
+
+ current_save_dir = File.new_for_path(save_as_dialog.get_current_folder());
+ }
+
+ save_as_dialog.destroy();
+ }
+
+ private void on_send_to() {
+ if (has_photo())
+ DesktopIntegration.send_to((Gee.Collection<Photo>) get_view().get_selected_sources());
+ }
+
+ protected override bool on_app_key_pressed(Gdk.EventKey event) {
+ bool handled = true;
+
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "bracketright":
+ activate_action("RotateClockwise");
+ break;
+
+ case "bracketleft":
+ activate_action("RotateClockwise");
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ return handled ? true : base.on_app_key_pressed(event);
+ }
+
+ private void on_print() {
+ if (get_view().get_selected_count() > 0) {
+ PrintManager.get_instance().spool_photo(
+ (Gee.Collection<Photo>) get_view().get_selected_sources_of_type(typeof(Photo)));
+ }
+ }
+
+ private void on_dphoto_can_rotate_changed(bool should_allow_rotation) {
+ // since this signal handler can be called from a background thread (gah, don't get me
+ // started...), chain to the "enable-rotate" signal in the foreground thread, as it's
+ // tied to UI elements
+ Idle.add(() => {
+ enable_rotate(should_allow_rotation);
+
+ return false;
+ });
+ }
+
+ protected override DataView create_photo_view(DataSource source) {
+ return new DirectView((DirectPhoto) source);
+ }
+}
+
+public class DirectFullscreenPhotoPage : DirectPhotoPage {
+ public DirectFullscreenPhotoPage(File file) {
+ base(file);
+ }
+
+ protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
+ // We intentionally avoid calling the base class implementation since we don't want
+ // direct.ui.
+ ui_filenames.add("direct_context.ui");
+ }
+}
diff --git a/src/direct/DirectView.vala b/src/direct/DirectView.vala
new file mode 100644
index 0000000..a36ec68
--- /dev/null
+++ b/src/direct/DirectView.vala
@@ -0,0 +1,50 @@
+/* Copyright 2011-2014 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.
+ */
+
+public class DirectView : DataView {
+ private File file;
+ private string? collate_key = null;
+
+ public DirectView(DirectPhoto source) {
+ base ((DataSource) source);
+
+ this.file = ((Photo) source).get_file();
+ }
+
+ public File get_file() {
+ return file;
+ }
+
+ public string get_collate_key() {
+ if (collate_key == null)
+ collate_key = file.get_basename().collate_key_for_filename();
+
+ return collate_key;
+ }
+}
+
+private class DirectViewCollection : ViewCollection {
+ private class DirectViewManager : ViewManager {
+ public override DataView create_view(DataSource source) {
+ return new DirectView((DirectPhoto) source);
+ }
+ }
+
+ public DirectViewCollection() {
+ base ("DirectViewCollection");
+
+ set_comparator(filename_comparator, null);
+ monitor_source_collection(DirectPhoto.global, new DirectViewManager(), null);
+ }
+
+ private static int64 filename_comparator(void *a, void *b) {
+ DirectView *aview = (DirectView *) a;
+ DirectView *bview = (DirectView *) b;
+
+ return strcmp(aview->get_collate_key(), bview->get_collate_key());
+ }
+}
+
diff --git a/src/direct/DirectWindow.vala b/src/direct/DirectWindow.vala
new file mode 100644
index 0000000..b339ac4
--- /dev/null
+++ b/src/direct/DirectWindow.vala
@@ -0,0 +1,98 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class DirectWindow : AppWindow {
+ private DirectPhotoPage direct_photo_page;
+
+ public DirectWindow(File file) {
+ direct_photo_page = new DirectPhotoPage(file);
+ direct_photo_page.get_view().items_altered.connect(on_photo_changed);
+ direct_photo_page.get_view().items_state_changed.connect(on_photo_changed);
+
+ set_current_page(direct_photo_page);
+
+ update_title(file, false);
+
+ direct_photo_page.switched_to();
+
+ // simple layout: menu on top, photo in center, toolbar along bottom (mimicking the
+ // PhotoPage in the library, but without the sidebar)
+ Gtk.Box layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+ layout.pack_start(direct_photo_page.get_menubar(), false, false, 0);
+ layout.pack_start(direct_photo_page, true, true, 0);
+ layout.pack_end(direct_photo_page.get_toolbar(), false, false, 0);
+
+ add(layout);
+ }
+
+ public static DirectWindow get_app() {
+ return (DirectWindow) instance;
+ }
+
+ public DirectPhotoPage get_direct_page() {
+ return (DirectPhotoPage) get_current_page();
+ }
+
+ public void update_title(File file, bool modified) {
+ title = "%s%s (%s) - %s".printf((modified) ? "*" : "", file.get_basename(),
+ get_display_pathname(file.get_parent()), Resources.APP_TITLE);
+ }
+
+ protected override void on_fullscreen() {
+ File file = get_direct_page().get_current_file();
+
+ go_fullscreen(new DirectFullscreenPhotoPage(file));
+ }
+
+ public override string get_app_role() {
+ return Resources.APP_DIRECT_ROLE;
+ }
+
+ private void on_photo_changed() {
+ Photo? photo = direct_photo_page.get_photo();
+ if (photo != null)
+ update_title(photo.get_file(), photo.has_alterations());
+ }
+
+ protected override void on_quit() {
+ if (!get_direct_page().check_quit())
+ return;
+
+ Config.Facade.get_instance().set_direct_window_state(maximized, dimensions);
+
+ base.on_quit();
+ }
+
+ public override bool delete_event(Gdk.EventAny event) {
+ if (!get_direct_page().check_quit())
+ return true;
+
+ return (base.delete_event != null) ? base.delete_event(event) : false;
+ }
+
+ public override bool button_press_event(Gdk.EventButton event) {
+ if (event.type == Gdk.EventType.2BUTTON_PRESS) {
+ on_fullscreen();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public override bool key_press_event(Gdk.EventKey event) {
+ // check for an escape
+ if (Gdk.keyval_name(event.keyval) == "Escape") {
+ on_quit();
+
+ return true;
+ }
+
+ // ...then let the base class take over
+ return (base.key_press_event != null) ? base.key_press_event(event) : false;
+ }
+}
+
diff --git a/src/direct/mk/direct.mk b/src/direct/mk/direct.mk
new file mode 100644
index 0000000..4c0f226
--- /dev/null
+++ b/src/direct/mk/direct.mk
@@ -0,0 +1,36 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Direct
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := direct
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ DirectWindow.vala \
+ DirectPhoto.vala \
+ DirectPhotoPage.vala \
+ DirectView.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES := \
+ Db \
+ Util \
+ Photos \
+ Slideshow \
+ Core
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+