diff options
Diffstat (limited to 'src/direct')
-rw-r--r-- | src/direct/Direct.vala | 35 | ||||
-rw-r--r-- | src/direct/DirectPhoto.vala | 319 | ||||
-rw-r--r-- | src/direct/DirectPhotoPage.vala | 587 | ||||
-rw-r--r-- | src/direct/DirectView.vala | 50 | ||||
-rw-r--r-- | src/direct/DirectWindow.vala | 98 | ||||
-rw-r--r-- | src/direct/mk/direct.mk | 36 |
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 + |