diff options
Diffstat (limited to 'src/Commands.vala')
-rw-r--r-- | src/Commands.vala | 2505 |
1 files changed, 2505 insertions, 0 deletions
diff --git a/src/Commands.vala b/src/Commands.vala new file mode 100644 index 0000000..04b771c --- /dev/null +++ b/src/Commands.vala @@ -0,0 +1,2505 @@ +/* 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. + */ + +// PageCommand stores the current page when a Command is created. Subclasses can call return_to_page() +// if it's appropriate to return to that page when executing an undo() or redo(). +public abstract class PageCommand : Command { + private Page? page; + private bool auto_return = true; + private Photo library_photo = null; + private CollectionPage collection_page = null; + + public PageCommand(string name, string explanation) { + base (name, explanation); + + page = AppWindow.get_instance().get_current_page(); + + if (page != null) { + page.destroy.connect(on_page_destroyed); + + // If the command occurred on a LibaryPhotoPage, the PageCommand must record additional + // objects to be restore it to its old state: a specific photo to focus on, a page to return + // to, and a view collection to operate over. Note that these objects can be cleared if + // the page goes into the background. The required objects are stored below. + LibraryPhotoPage photo_page = page as LibraryPhotoPage; + if (photo_page != null) { + library_photo = photo_page.get_photo(); + collection_page = photo_page.get_controller_page(); + + if (library_photo != null && collection_page != null) { + library_photo.destroyed.connect(on_photo_destroyed); + collection_page.destroy.connect(on_controller_destroyed); + } else { + library_photo = null; + collection_page = null; + } + } + } + } + + ~PageCommand() { + if (page != null) + page.destroy.disconnect(on_page_destroyed); + + if (library_photo != null) + library_photo.destroyed.disconnect(on_photo_destroyed); + + if (collection_page != null) + collection_page.destroy.disconnect(on_controller_destroyed); + } + + public void set_auto_return_to_page(bool auto_return) { + this.auto_return = auto_return; + } + + public override void prepare() { + if (auto_return) + return_to_page(); + + base.prepare(); + } + + public void return_to_page() { + LibraryPhotoPage photo_page = page as LibraryPhotoPage; + + if (photo_page != null) { + if (library_photo != null && collection_page != null) { + bool photo_in_collection = false; + int count = collection_page.get_view().get_count(); + for (int i = 0; i < count; i++) { + if ( ((Thumbnail) collection_page.get_view().get_at(i)).get_media_source() == library_photo) { + photo_in_collection = true; + break; + } + } + + if (photo_in_collection) + LibraryWindow.get_app().switch_to_photo_page(collection_page, library_photo); + } + } else if (page != null) + AppWindow.get_instance().set_current_page(page); + } + + private void on_page_destroyed() { + page.destroy.disconnect(on_page_destroyed); + page = null; + } + + private void on_photo_destroyed() { + library_photo.destroyed.disconnect(on_photo_destroyed); + library_photo = null; + } + + private void on_controller_destroyed() { + collection_page.destroy.disconnect(on_controller_destroyed); + collection_page = null; + } + +} + +public abstract class SingleDataSourceCommand : PageCommand { + protected DataSource source; + + public SingleDataSourceCommand(DataSource source, string name, string explanation) { + base(name, explanation); + + this.source = source; + + source.destroyed.connect(on_source_destroyed); + } + + ~SingleDataSourceCommand() { + source.destroyed.disconnect(on_source_destroyed); + } + + public DataSource get_source() { + return source; + } + + private void on_source_destroyed() { + // too much risk in simply removing this from the CommandManager; if this is considered too + // broad a brushstroke, can return to this later + get_command_manager().reset(); + } +} + +public abstract class SimpleProxyableCommand : PageCommand { + private SourceProxy proxy; + private Gee.HashSet<SourceProxy> proxies = new Gee.HashSet<SourceProxy>(); + + public SimpleProxyableCommand(Proxyable proxyable, string name, string explanation) { + base (name, explanation); + + proxy = proxyable.get_proxy(); + proxy.broken.connect(on_proxy_broken); + } + + ~SimpleProxyableCommand() { + proxy.broken.disconnect(on_proxy_broken); + clear_added_proxies(); + } + + public override void execute() { + execute_on_source(proxy.get_source()); + } + + protected abstract void execute_on_source(DataSource source); + + public override void undo() { + undo_on_source(proxy.get_source()); + } + + protected abstract void undo_on_source(DataSource source); + + // If the Command deals with other Proxyables during processing, it can add them here and the + // SimpleProxyableCommand will deal with created a SourceProxy and if it signals it's broken. + // Note that these cannot be removed programatically, but only cleared en masse; it's expected + // this is fine for the nature of a Command. + protected void add_proxyables(Gee.Collection<Proxyable> proxyables) { + foreach (Proxyable proxyable in proxyables) { + SourceProxy added_proxy = proxyable.get_proxy(); + added_proxy.broken.connect(on_proxy_broken); + proxies.add(added_proxy); + } + } + + // See add_proxyables() for a note on use. + protected void clear_added_proxies() { + foreach (SourceProxy added_proxy in proxies) + added_proxy.broken.disconnect(on_proxy_broken); + + proxies.clear(); + } + + private void on_proxy_broken() { + debug("on_proxy_broken"); + get_command_manager().reset(); + } +} + +public abstract class SinglePhotoTransformationCommand : SingleDataSourceCommand { + private PhotoTransformationState state; + + public SinglePhotoTransformationCommand(Photo photo, string name, string explanation) { + base(photo, name, explanation); + + state = photo.save_transformation_state(); + state.broken.connect(on_state_broken); + } + + ~SinglePhotoTransformationCommand() { + state.broken.disconnect(on_state_broken); + } + + public override void undo() { + ((Photo) source).load_transformation_state(state); + } + + private void on_state_broken() { + get_command_manager().reset(); + } +} + +public abstract class GenericPhotoTransformationCommand : SingleDataSourceCommand { + private PhotoTransformationState original_state = null; + private PhotoTransformationState transformed_state = null; + + public GenericPhotoTransformationCommand(Photo photo, string name, string explanation) { + base(photo, name, explanation); + } + + ~GenericPhotoTransformationCommand() { + if (original_state != null) + original_state.broken.disconnect(on_state_broken); + + if (transformed_state != null) + transformed_state.broken.disconnect(on_state_broken); + } + + public override void execute() { + Photo photo = (Photo) source; + + original_state = photo.save_transformation_state(); + original_state.broken.connect(on_state_broken); + + execute_on_photo(photo); + + transformed_state = photo.save_transformation_state(); + transformed_state.broken.connect(on_state_broken); + } + + public abstract void execute_on_photo(Photo photo); + + public override void undo() { + // use the original state of the photo + ((Photo) source).load_transformation_state(original_state); + } + + public override void redo() { + // use the state of the photo after transformation + ((Photo) source).load_transformation_state(transformed_state); + } + + protected virtual bool can_compress(Command command) { + return false; + } + + public override bool compress(Command command) { + if (!can_compress(command)) + return false; + + GenericPhotoTransformationCommand generic = command as GenericPhotoTransformationCommand; + if (generic == null) + return false; + + if (generic.source != source) + return false; + + // execute this new (and successive) command + generic.execute(); + + // save it's new transformation state as ours + transformed_state = generic.transformed_state; + + return true; + } + + private void on_state_broken() { + get_command_manager().reset(); + } +} + +public abstract class MultipleDataSourceCommand : PageCommand { + protected const int MIN_OPS_FOR_PROGRESS_WINDOW = 5; + + protected Gee.ArrayList<DataSource> source_list = new Gee.ArrayList<DataSource>(); + + private string progress_text; + private string undo_progress_text; + private Gee.ArrayList<DataSource> acted_upon = new Gee.ArrayList<DataSource>(); + private Gee.HashSet<SourceCollection> hooked_collections = new Gee.HashSet<SourceCollection>(); + + public MultipleDataSourceCommand(Gee.Iterable<DataView> iter, string progress_text, + string undo_progress_text, string name, string explanation) { + base(name, explanation); + + this.progress_text = progress_text; + this.undo_progress_text = undo_progress_text; + + foreach (DataView view in iter) { + DataSource source = view.get_source(); + SourceCollection? collection = (SourceCollection) source.get_membership(); + + if (collection != null) { + hooked_collections.add(collection); + } + source_list.add(source); + } + + foreach (SourceCollection current_collection in hooked_collections) { + current_collection.item_destroyed.connect(on_source_destroyed); + } + } + + ~MultipleDataSourceCommand() { + foreach (SourceCollection current_collection in hooked_collections) { + current_collection.item_destroyed.disconnect(on_source_destroyed); + } + } + + public Gee.Iterable<DataSource> get_sources() { + return source_list; + } + + public int get_source_count() { + return source_list.size; + } + + private void on_source_destroyed(DataSource source) { + // as with SingleDataSourceCommand, too risky to selectively remove commands from the stack, + // although this could be reconsidered in the future + if (source_list.contains(source)) + get_command_manager().reset(); + } + + public override void execute() { + acted_upon.clear(); + + start_transaction(); + execute_all(true, true, source_list, acted_upon); + commit_transaction(); + } + + public abstract void execute_on_source(DataSource source); + + public override void undo() { + if (acted_upon.size > 0) { + start_transaction(); + execute_all(false, false, acted_upon, null); + commit_transaction(); + + acted_upon.clear(); + } + } + + public abstract void undo_on_source(DataSource source); + + private void start_transaction() { + foreach (SourceCollection sources in hooked_collections) { + MediaSourceCollection? media_collection = sources as MediaSourceCollection; + if (media_collection != null) + media_collection.transaction_controller.begin(); + } + } + + private void commit_transaction() { + foreach (SourceCollection sources in hooked_collections) { + MediaSourceCollection? media_collection = sources as MediaSourceCollection; + if (media_collection != null) + media_collection.transaction_controller.commit(); + } + } + + private void execute_all(bool exec, bool can_cancel, Gee.ArrayList<DataSource> todo, + Gee.ArrayList<DataSource>? completed) { + AppWindow.get_instance().set_busy_cursor(); + + int count = 0; + int total = todo.size; + int two_percent = (int) ((double) total / 50.0); + if (two_percent <= 0) + two_percent = 1; + + string text = exec ? progress_text : undo_progress_text; + + Cancellable cancellable = null; + ProgressDialog progress = null; + if (total >= MIN_OPS_FOR_PROGRESS_WINDOW) { + cancellable = can_cancel ? new Cancellable() : null; + progress = new ProgressDialog(AppWindow.get_instance(), text, cancellable); + } + + foreach (DataSource source in todo) { + if (exec) + execute_on_source(source); + else + undo_on_source(source); + + if (completed != null) + completed.add(source); + + if (progress != null) { + if ((++count % two_percent) == 0) { + progress.set_fraction(count, total); + spin_event_loop(); + } + + if (cancellable != null && cancellable.is_cancelled()) + break; + } + } + + if (progress != null) + progress.close(); + + AppWindow.get_instance().set_normal_cursor(); + } +} + +// TODO: Upgrade MultipleDataSourceAtOnceCommand to use TransactionControllers. +public abstract class MultipleDataSourceAtOnceCommand : PageCommand { + private Gee.HashSet<DataSource> sources = new Gee.HashSet<DataSource>(); + private Gee.HashSet<SourceCollection> hooked_collections = new Gee.HashSet<SourceCollection>(); + + public MultipleDataSourceAtOnceCommand(Gee.Collection<DataSource> sources, string name, + string explanation) { + base (name, explanation); + + this.sources.add_all(sources); + + foreach (DataSource source in this.sources) { + SourceCollection? membership = source.get_membership() as SourceCollection; + if (membership != null) + hooked_collections.add(membership); + } + + foreach (SourceCollection source_collection in hooked_collections) + source_collection.items_destroyed.connect(on_sources_destroyed); + } + + ~MultipleDataSourceAtOnceCommand() { + foreach (SourceCollection source_collection in hooked_collections) + source_collection.items_destroyed.disconnect(on_sources_destroyed); + } + + public override void execute() { + AppWindow.get_instance().set_busy_cursor(); + + DatabaseTable.begin_transaction(); + MediaCollectionRegistry.get_instance().freeze_all(); + + execute_on_all(sources); + + MediaCollectionRegistry.get_instance().thaw_all(); + try { + DatabaseTable.commit_transaction(); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } finally { + AppWindow.get_instance().set_normal_cursor(); + } + } + + protected abstract void execute_on_all(Gee.Collection<DataSource> sources); + + public override void undo() { + AppWindow.get_instance().set_busy_cursor(); + + DatabaseTable.begin_transaction(); + MediaCollectionRegistry.get_instance().freeze_all(); + + undo_on_all(sources); + + MediaCollectionRegistry.get_instance().thaw_all(); + try { + DatabaseTable.commit_transaction(); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } finally { + AppWindow.get_instance().set_normal_cursor(); + } + } + + protected abstract void undo_on_all(Gee.Collection<DataSource> sources); + + private void on_sources_destroyed(Gee.Collection<DataSource> destroyed) { + foreach (DataSource source in destroyed) { + if (sources.contains(source)) { + get_command_manager().reset(); + + break; + } + } + } +} + +public abstract class MultiplePhotoTransformationCommand : MultipleDataSourceCommand { + private Gee.HashMap<Photo, PhotoTransformationState> map = new Gee.HashMap< + Photo, PhotoTransformationState>(); + + public MultiplePhotoTransformationCommand(Gee.Iterable<DataView> iter, string progress_text, + string undo_progress_text, string name, string explanation) { + base(iter, progress_text, undo_progress_text, name, explanation); + + foreach (DataSource source in source_list) { + Photo photo = (Photo) source; + PhotoTransformationState state = photo.save_transformation_state(); + state.broken.connect(on_state_broken); + + map.set(photo, state); + } + } + + ~MultiplePhotoTransformationCommand() { + foreach (PhotoTransformationState state in map.values) + state.broken.disconnect(on_state_broken); + } + + public override void undo_on_source(DataSource source) { + Photo photo = (Photo) source; + + PhotoTransformationState state = map.get(photo); + assert(state != null); + + photo.load_transformation_state(state); + } + + private void on_state_broken() { + get_command_manager().reset(); + } +} + +public class RotateSingleCommand : SingleDataSourceCommand { + private Rotation rotation; + + public RotateSingleCommand(Photo photo, Rotation rotation, string name, string explanation) { + base(photo, name, explanation); + + this.rotation = rotation; + } + + public override void execute() { + ((Photo) source).rotate(rotation); + } + + public override void undo() { + ((Photo) source).rotate(rotation.opposite()); + } +} + +public class RotateMultipleCommand : MultipleDataSourceCommand { + private Rotation rotation; + + public RotateMultipleCommand(Gee.Iterable<DataView> iter, Rotation rotation, string name, + string explanation, string progress_text, string undo_progress_text) { + base(iter, progress_text, undo_progress_text, name, explanation); + + this.rotation = rotation; + } + + public override void execute_on_source(DataSource source) { + ((Photo) source).rotate(rotation); + } + + public override void undo_on_source(DataSource source) { + ((Photo) source).rotate(rotation.opposite()); + } +} + +public class EditTitleCommand : SingleDataSourceCommand { + private string new_title; + private string? old_title; + + public EditTitleCommand(MediaSource source, string new_title) { + base(source, Resources.EDIT_TITLE_LABEL, ""); + + this.new_title = new_title; + old_title = source.get_title(); + } + + public override void execute() { + ((MediaSource) source).set_title(new_title); + } + + public override void undo() { + ((MediaSource) source).set_title(old_title); + } +} + +public class EditCommentCommand : SingleDataSourceCommand { + private string new_comment; + private string? old_comment; + + public EditCommentCommand(MediaSource source, string new_comment) { + base(source, Resources.EDIT_COMMENT_LABEL, ""); + + this.new_comment = new_comment; + old_comment = source.get_comment(); + } + + public override void execute() { + ((MediaSource) source).set_comment(new_comment); + } + + public override void undo() { + ((MediaSource) source).set_comment(old_comment); + } +} + +public class EditMultipleTitlesCommand : MultipleDataSourceAtOnceCommand { + public string new_title; + public Gee.HashMap<MediaSource, string?> old_titles = new Gee.HashMap<MediaSource, string?>(); + + public EditMultipleTitlesCommand(Gee.Collection<MediaSource> media_sources, string new_title) { + base (media_sources, Resources.EDIT_TITLE_LABEL, ""); + + this.new_title = new_title; + foreach (MediaSource media in media_sources) + old_titles.set(media, media.get_title()); + } + + public override void execute_on_all(Gee.Collection<DataSource> sources) { + foreach (DataSource source in sources) + ((MediaSource) source).set_title(new_title); + } + + public override void undo_on_all(Gee.Collection<DataSource> sources) { + foreach (DataSource source in sources) + ((MediaSource) source).set_title(old_titles.get((MediaSource) source)); + } +} + +public class EditMultipleCommentsCommand : MultipleDataSourceAtOnceCommand { + public string new_comment; + public Gee.HashMap<MediaSource, string?> old_comments = new Gee.HashMap<MediaSource, string?>(); + + public EditMultipleCommentsCommand(Gee.Collection<MediaSource> media_sources, string new_comment) { + base (media_sources, Resources.EDIT_COMMENT_LABEL, ""); + + this.new_comment = new_comment; + foreach (MediaSource media in media_sources) + old_comments.set(media, media.get_comment()); + } + + public override void execute_on_all(Gee.Collection<DataSource> sources) { + foreach (DataSource source in sources) + ((MediaSource) source).set_comment(new_comment); + } + + public override void undo_on_all(Gee.Collection<DataSource> sources) { + foreach (DataSource source in sources) + ((MediaSource) source).set_comment(old_comments.get((MediaSource) source)); + } +} + +public class RenameEventCommand : SimpleProxyableCommand { + private string new_name; + private string? old_name; + + public RenameEventCommand(Event event, string new_name) { + base(event, Resources.RENAME_EVENT_LABEL, ""); + + this.new_name = new_name; + old_name = event.get_raw_name(); + } + + public override void execute_on_source(DataSource source) { + ((Event) source).rename(new_name); + } + + public override void undo_on_source(DataSource source) { + ((Event) source).rename(old_name); + } +} + +public class EditEventCommentCommand : SimpleProxyableCommand { + private string new_comment; + private string? old_comment; + + public EditEventCommentCommand(Event event, string new_comment) { + base(event, Resources.EDIT_COMMENT_LABEL, ""); + + this.new_comment = new_comment; + old_comment = event.get_comment(); + } + + public override void execute_on_source(DataSource source) { + ((Event) source).set_comment(new_comment); + } + + public override void undo_on_source(DataSource source) { + ((Event) source).set_comment(old_comment); + } +} + +public class SetKeyPhotoCommand : SingleDataSourceCommand { + private MediaSource new_primary_source; + private MediaSource old_primary_source; + + public SetKeyPhotoCommand(Event event, MediaSource new_primary_source) { + base(event, Resources.MAKE_KEY_PHOTO_LABEL, ""); + + this.new_primary_source = new_primary_source; + old_primary_source = event.get_primary_source(); + } + + public override void execute() { + ((Event) source).set_primary_source(new_primary_source); + } + + public override void undo() { + ((Event) source).set_primary_source(old_primary_source); + } +} + +public class RevertSingleCommand : GenericPhotoTransformationCommand { + public RevertSingleCommand(Photo photo) { + base(photo, Resources.REVERT_LABEL, ""); + } + + public override void execute_on_photo(Photo photo) { + photo.remove_all_transformations(); + } + + public override bool compress(Command command) { + RevertSingleCommand revert_single_command = command as RevertSingleCommand; + if (revert_single_command == null) + return false; + + if (revert_single_command.source != source) + return false; + + // no need to execute anything; multiple successive reverts on the same photo are as good + // as one + return true; + } +} + +public class RevertMultipleCommand : MultiplePhotoTransformationCommand { + public RevertMultipleCommand(Gee.Iterable<DataView> iter) { + base(iter, _("Reverting"), _("Undoing Revert"), Resources.REVERT_LABEL, + ""); + } + + public override void execute_on_source(DataSource source) { + ((Photo) source).remove_all_transformations(); + } +} + +public class EnhanceSingleCommand : GenericPhotoTransformationCommand { + public EnhanceSingleCommand(Photo photo) { + base(photo, Resources.ENHANCE_LABEL, Resources.ENHANCE_TOOLTIP); + } + + public override void execute_on_photo(Photo photo) { + AppWindow.get_instance().set_busy_cursor(); +#if MEASURE_ENHANCE + Timer overall_timer = new Timer(); +#endif + + photo.enhance(); + +#if MEASURE_ENHANCE + overall_timer.stop(); + debug("Auto-Enhance overall time: %f sec", overall_timer.elapsed()); +#endif + AppWindow.get_instance().set_normal_cursor(); + } + + public override bool compress(Command command) { + EnhanceSingleCommand enhance_single_command = command as EnhanceSingleCommand; + if (enhance_single_command == null) + return false; + + if (enhance_single_command.source != source) + return false; + + // multiple successive enhances on the same photo are as good as a single + return true; + } +} + +public class EnhanceMultipleCommand : MultiplePhotoTransformationCommand { + public EnhanceMultipleCommand(Gee.Iterable<DataView> iter) { + base(iter, _("Enhancing"), _("Undoing Enhance"), Resources.ENHANCE_LABEL, + Resources.ENHANCE_TOOLTIP); + } + + public override void execute_on_source(DataSource source) { + ((Photo) source).enhance(); + } +} + +public class StraightenCommand : GenericPhotoTransformationCommand { + private double theta; + private Box crop; // straightening can change the crop rectangle + + public StraightenCommand(Photo photo, double theta, Box crop, string name, string explanation) { + base(photo, name, explanation); + + this.theta = theta; + this.crop = crop; + } + + public override void execute_on_photo(Photo photo) { + // thaw collection so both alterations are signalled at the same time + DataCollection? collection = photo.get_membership(); + if (collection != null) + collection.freeze_notifications(); + + photo.set_straighten(theta); + photo.set_crop(crop); + + if (collection != null) + collection.thaw_notifications(); + } +} + +public class CropCommand : GenericPhotoTransformationCommand { + private Box crop; + + public CropCommand(Photo photo, Box crop, string name, string explanation) { + base(photo, name, explanation); + + this.crop = crop; + } + + public override void execute_on_photo(Photo photo) { + photo.set_crop(crop); + } +} + +public class AdjustColorsSingleCommand : GenericPhotoTransformationCommand { + private PixelTransformationBundle transformations; + + public AdjustColorsSingleCommand(Photo photo, PixelTransformationBundle transformations, + string name, string explanation) { + base(photo, name, explanation); + + this.transformations = transformations; + } + + public override void execute_on_photo(Photo photo) { + AppWindow.get_instance().set_busy_cursor(); + + photo.set_color_adjustments(transformations); + + AppWindow.get_instance().set_normal_cursor(); + } + + public override bool can_compress(Command command) { + return command is AdjustColorsSingleCommand; + } +} + +public class AdjustColorsMultipleCommand : MultiplePhotoTransformationCommand { + private PixelTransformationBundle transformations; + + public AdjustColorsMultipleCommand(Gee.Iterable<DataView> iter, + PixelTransformationBundle transformations, string name, string explanation) { + base(iter, _("Applying Color Transformations"), _("Undoing Color Transformations"), + name, explanation); + + this.transformations = transformations; + } + + public override void execute_on_source(DataSource source) { + ((Photo) source).set_color_adjustments(transformations); + } +} + +public class RedeyeCommand : GenericPhotoTransformationCommand { + private EditingTools.RedeyeInstance redeye_instance; + + public RedeyeCommand(Photo photo, EditingTools.RedeyeInstance redeye_instance, string name, + string explanation) { + base(photo, name, explanation); + + this.redeye_instance = redeye_instance; + } + + public override void execute_on_photo(Photo photo) { + photo.add_redeye_instance(redeye_instance); + } +} + +public abstract class MovePhotosCommand : Command { + // Piggyback on a private command so that processing to determine new_event can occur before + // construction, if needed + protected class RealMovePhotosCommand : MultipleDataSourceCommand { + private SourceProxy new_event_proxy = null; + private Gee.HashMap<MediaSource, SourceProxy?> old_events = new Gee.HashMap< + MediaSource, SourceProxy?>(); + + public RealMovePhotosCommand(Event? new_event, Gee.Iterable<DataView> source_views, + string progress_text, string undo_progress_text, string name, string explanation) { + base(source_views, progress_text, undo_progress_text, name, explanation); + + // get proxies for each media source's event + foreach (DataSource source in source_list) { + MediaSource current_media = (MediaSource) source; + Event? old_event = current_media.get_event(); + SourceProxy? old_event_proxy = (old_event != null) ? old_event.get_proxy() : null; + + // if any of the proxies break, the show's off + if (old_event_proxy != null) + old_event_proxy.broken.connect(on_proxy_broken); + + old_events.set(current_media, old_event_proxy); + } + + // stash the proxy of the new event + new_event_proxy = new_event.get_proxy(); + new_event_proxy.broken.connect(on_proxy_broken); + } + + ~RealMovePhotosCommand() { + new_event_proxy.broken.disconnect(on_proxy_broken); + + foreach (SourceProxy? proxy in old_events.values) { + if (proxy != null) + proxy.broken.disconnect(on_proxy_broken); + } + } + + public override void execute() { + // Are we at an event page already? + if ((LibraryWindow.get_app().get_current_page() is EventPage)) { + Event evt = ((EventPage) LibraryWindow.get_app().get_current_page()).get_event(); + + // Will moving these empty this event? + if (evt.get_media_count() == source_list.size) { + // Yes - jump away from this event, since it will have zero + // entries and is going to be removed. + LibraryWindow.get_app().switch_to_event((Event) new_event_proxy.get_source()); + } + } else { + // We're in a library or tag page. + + // Are we moving these to a newly-created (and therefore empty) event? + if (((Event) new_event_proxy.get_source()).get_media_count() == 0) { + // Yes - jump to the new event. + LibraryWindow.get_app().switch_to_event((Event) new_event_proxy.get_source()); + } + } + + // Otherwise - don't jump; users found the jumping disconcerting. + + // create the new event + base.execute(); + } + + public override void execute_on_source(DataSource source) { + ((MediaSource) source).set_event((Event?) new_event_proxy.get_source()); + } + + public override void undo_on_source(DataSource source) { + MediaSource current_media = (MediaSource) source; + SourceProxy? event_proxy = old_events.get(current_media); + + current_media.set_event(event_proxy != null ? (Event?) event_proxy.get_source() : null); + } + + private void on_proxy_broken() { + get_command_manager().reset(); + } + } + + protected RealMovePhotosCommand real_command; + + public MovePhotosCommand(string name, string explanation) { + base(name, explanation); + } + + public override void prepare() { + assert(real_command != null); + real_command.prepare(); + } + + public override void execute() { + assert(real_command != null); + real_command.execute(); + } + + public override void undo() { + assert(real_command != null); + real_command.undo(); + } +} + +public class NewEventCommand : MovePhotosCommand { + public NewEventCommand(Gee.Iterable<DataView> iter) { + base(Resources.NEW_EVENT_LABEL, ""); + + // get the primary or "key" source for the new event (which is simply the first one) + MediaSource key_source = null; + foreach (DataView view in iter) { + MediaSource current_source = (MediaSource) view.get_source(); + + if (key_source == null) { + key_source = current_source; + break; + } + } + + // key photo is required for an event + assert(key_source != null); + + Event new_event = Event.create_empty_event(key_source); + + real_command = new RealMovePhotosCommand(new_event, iter, _("Creating New Event"), + _("Removing Event"), Resources.NEW_EVENT_LABEL, + ""); + } +} + +public class SetEventCommand : MovePhotosCommand { + public SetEventCommand(Gee.Iterable<DataView> iter, Event new_event) { + base(Resources.SET_PHOTO_EVENT_LABEL, Resources.SET_PHOTO_EVENT_TOOLTIP); + + real_command = new RealMovePhotosCommand(new_event, iter, _("Moving Photos to New Event"), + _("Setting Photos to Previous Event"), Resources.SET_PHOTO_EVENT_LABEL, + ""); + } +} + +public class MergeEventsCommand : MovePhotosCommand { + public MergeEventsCommand(Gee.Iterable<DataView> iter) { + base (Resources.MERGE_LABEL, ""); + + // Because it requires fewer operations to merge small events onto large ones, + // rather than the other way round, we try to choose the event with the most + // sources as the 'master', preferring named events over unnamed ones so that + // names can persist. + Event master_event = null; + int named_evt_src_count = 0; + int unnamed_evt_src_count = 0; + Gee.ArrayList<ThumbnailView> media_thumbs = new Gee.ArrayList<ThumbnailView>(); + + foreach (DataView view in iter) { + Event event = (Event) view.get_source(); + + // First event we've examined? + if (master_event == null) { + // Yes. Make it the master for now and remember it as + // having the most sources (out of what we've seen so far). + master_event = event; + unnamed_evt_src_count = master_event.get_media_count(); + if (event.has_name()) + named_evt_src_count = master_event.get_media_count(); + } else { + // No. Check whether this event has a name and whether + // it has more sources than any other we've seen... + if (event.has_name()) { + if (event.get_media_count() > named_evt_src_count) { + named_evt_src_count = event.get_media_count(); + master_event = event; + } + } else if (named_evt_src_count == 0) { + // Per the original app design, named events -always- trump + // unnamed ones, so only choose an unnamed one if we haven't + // seen any named ones yet. + if (event.get_media_count() > unnamed_evt_src_count) { + unnamed_evt_src_count = event.get_media_count(); + master_event = event; + } + } + } + + // store all media sources in this operation; they will be moved to the master event + // (keep proxies of their original event for undo) + foreach (MediaSource media_source in event.get_media()) + media_thumbs.add(new ThumbnailView(media_source)); + } + + assert(master_event != null); + assert(media_thumbs.size > 0); + + real_command = new RealMovePhotosCommand(master_event, media_thumbs, _("Merging"), + _("Unmerging"), Resources.MERGE_LABEL, ""); + } +} + +public class DuplicateMultiplePhotosCommand : MultipleDataSourceCommand { + private Gee.HashMap<LibraryPhoto, LibraryPhoto> dupes = new Gee.HashMap<LibraryPhoto, LibraryPhoto>(); + private int failed = 0; + + public DuplicateMultiplePhotosCommand(Gee.Iterable<DataView> iter) { + base (iter, _("Duplicating photos"), _("Removing duplicated photos"), + Resources.DUPLICATE_PHOTO_LABEL, Resources.DUPLICATE_PHOTO_TOOLTIP); + + LibraryPhoto.global.item_destroyed.connect(on_photo_destroyed); + } + + ~DuplicateMultiplePhotosCommand() { + LibraryPhoto.global.item_destroyed.disconnect(on_photo_destroyed); + } + + private void on_photo_destroyed(DataSource source) { + // if one of the duplicates is destroyed, can no longer undo it (which destroys it again) + if (dupes.values.contains((LibraryPhoto) source)) + get_command_manager().reset(); + } + + public override void execute() { + dupes.clear(); + failed = 0; + + base.execute(); + + if (failed > 0) { + string error_string = (ngettext("Unable to duplicate one photo due to a file error", + "Unable to duplicate %d photos due to file errors", failed)).printf(failed); + AppWindow.error_message(error_string); + } + } + + public override void execute_on_source(DataSource source) { + LibraryPhoto photo = (LibraryPhoto) source; + + try { + LibraryPhoto dupe = photo.duplicate(); + dupes.set(photo, dupe); + } catch (Error err) { + critical("Unable to duplicate file %s: %s", photo.get_file().get_path(), err.message); + failed++; + } + } + + public override void undo() { + // disconnect from monitoring the duplicates' destruction, as undo() does exactly that + LibraryPhoto.global.item_destroyed.disconnect(on_photo_destroyed); + + base.undo(); + + // be sure to drop everything that was destroyed + dupes.clear(); + failed = 0; + + // re-monitor for duplicates' destruction + LibraryPhoto.global.item_destroyed.connect(on_photo_destroyed); + } + + public override void undo_on_source(DataSource source) { + LibraryPhoto photo = (LibraryPhoto) source; + + Marker marker = LibraryPhoto.global.mark(dupes.get(photo)); + LibraryPhoto.global.destroy_marked(marker, true); + } +} + +public class SetRatingSingleCommand : SingleDataSourceCommand { + private Rating last_rating; + private Rating new_rating; + private bool set_direct; + private bool incrementing; + + public SetRatingSingleCommand(DataSource source, Rating rating) { + base (source, Resources.rating_label(rating), ""); + set_direct = true; + new_rating = rating; + + last_rating = ((LibraryPhoto)source).get_rating(); + } + + public SetRatingSingleCommand.inc_dec(DataSource source, bool is_incrementing) { + base (source, is_incrementing ? Resources.INCREASE_RATING_LABEL : + Resources.DECREASE_RATING_LABEL, ""); + set_direct = false; + incrementing = is_incrementing; + + last_rating = ((MediaSource) source).get_rating(); + } + + public override void execute() { + if (set_direct) + ((MediaSource) source).set_rating(new_rating); + else { + if (incrementing) + ((MediaSource) source).increase_rating(); + else + ((MediaSource) source).decrease_rating(); + } + } + + public override void undo() { + ((MediaSource) source).set_rating(last_rating); + } +} + +public class SetRatingCommand : MultipleDataSourceCommand { + private Gee.HashMap<DataSource, Rating> last_rating_map; + private Rating new_rating; + private bool set_direct; + private bool incrementing; + private int action_count = 0; + + public SetRatingCommand(Gee.Iterable<DataView> iter, Rating rating) { + base (iter, Resources.rating_progress(rating), _("Restoring previous rating"), + Resources.rating_label(rating), ""); + set_direct = true; + new_rating = rating; + + save_source_states(iter); + } + + public SetRatingCommand.inc_dec(Gee.Iterable<DataView> iter, bool is_incrementing) { + base (iter, + is_incrementing ? _("Increasing ratings") : _("Decreasing ratings"), + is_incrementing ? _("Decreasing ratings") : _("Increasing ratings"), + is_incrementing ? Resources.INCREASE_RATING_LABEL : Resources.DECREASE_RATING_LABEL, + ""); + set_direct = false; + incrementing = is_incrementing; + + save_source_states(iter); + } + + private void save_source_states(Gee.Iterable<DataView> iter) { + last_rating_map = new Gee.HashMap<DataSource, Rating>(); + + foreach (DataView view in iter) { + DataSource source = view.get_source(); + last_rating_map[source] = ((MediaSource) source).get_rating(); + } + } + + public override void execute() { + action_count = 0; + base.execute(); + } + + public override void undo() { + action_count = 0; + base.undo(); + } + + public override void execute_on_source(DataSource source) { + if (set_direct) + ((MediaSource) source).set_rating(new_rating); + else { + if (incrementing) + ((MediaSource) source).increase_rating(); + else + ((MediaSource) source).decrease_rating(); + } + } + + public override void undo_on_source(DataSource source) { + ((MediaSource) source).set_rating(last_rating_map[source]); + } +} + +public class SetRawDeveloperCommand : MultipleDataSourceCommand { + private Gee.HashMap<Photo, RawDeveloper> last_developer_map; + private Gee.HashMap<Photo, PhotoTransformationState> last_transformation_map; + private RawDeveloper new_developer; + + public SetRawDeveloperCommand(Gee.Iterable<DataView> iter, RawDeveloper developer) { + base (iter, _("Setting RAW developer"), _("Restoring previous RAW developer"), + _("Set Developer"), ""); + new_developer = developer; + save_source_states(iter); + } + + private void save_source_states(Gee.Iterable<DataView> iter) { + last_developer_map = new Gee.HashMap<Photo, RawDeveloper>(); + last_transformation_map = new Gee.HashMap<Photo, PhotoTransformationState>(); + + foreach (DataView view in iter) { + Photo? photo = view.get_source() as Photo; + if (is_raw_photo(photo)) { + last_developer_map[photo] = photo.get_raw_developer(); + last_transformation_map[photo] = photo.save_transformation_state(); + } + } + } + + public override void execute() { + base.execute(); + } + + public override void undo() { + base.undo(); + } + + public override void execute_on_source(DataSource source) { + Photo? photo = source as Photo; + if (is_raw_photo(photo)) { + if (new_developer == RawDeveloper.CAMERA && !photo.is_raw_developer_available(RawDeveloper.CAMERA)) + photo.set_raw_developer(RawDeveloper.EMBEDDED); + else + photo.set_raw_developer(new_developer); + } + } + + public override void undo_on_source(DataSource source) { + Photo? photo = source as Photo; + if (is_raw_photo(photo)) { + photo.set_raw_developer(last_developer_map[photo]); + photo.load_transformation_state(last_transformation_map[photo]); + } + } + + private bool is_raw_photo(Photo? photo) { + return photo != null && photo.get_master_file_format() == PhotoFileFormat.RAW; + } +} + +public class AdjustDateTimePhotoCommand : SingleDataSourceCommand { + private Dateable dateable; + private Event? prev_event; + private int64 time_shift; + private bool modify_original; + + public AdjustDateTimePhotoCommand(Dateable dateable, int64 time_shift, bool modify_original) { + base(dateable, Resources.ADJUST_DATE_TIME_LABEL, ""); + + this.dateable = dateable; + this.time_shift = time_shift; + this.modify_original = modify_original; + } + + public override void execute() { + set_time(dateable, dateable.get_exposure_time() + (time_t) time_shift); + + prev_event = dateable.get_event(); + + ViewCollection all_events = new ViewCollection("tmp"); + + foreach (DataObject dobj in Event.global.get_all()) { + Event event = dobj as Event; + if (event != null) { + all_events.add(new EventView(event)); + } + } + Event.generate_single_event(dateable, all_events, null); + } + + public override void undo() { + set_time(dateable, dateable.get_exposure_time() - (time_t) time_shift); + + dateable.set_event(prev_event); + } + + private void set_time(Dateable dateable, time_t exposure_time) { + if (modify_original && dateable is Photo) { + try { + ((Photo)dateable).set_exposure_time_persistent(exposure_time); + } catch(GLib.Error err) { + AppWindow.error_message(_("Original photo could not be adjusted.")); + } + } else { + dateable.set_exposure_time(exposure_time); + } + } +} + +public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand { + private int64 time_shift; + private bool keep_relativity; + private bool modify_originals; + private Gee.Map<Dateable, Event?> prev_events; + + // used when photos are batch changed instead of shifted uniformly + private time_t? new_time = null; + private Gee.HashMap<Dateable, time_t?> old_times; + private Gee.ArrayList<Dateable> error_list; + + public AdjustDateTimePhotosCommand(Gee.Iterable<DataView> iter, int64 time_shift, + bool keep_relativity, bool modify_originals) { + base(iter, _("Adjusting Date and Time"), _("Undoing Date and Time Adjustment"), + Resources.ADJUST_DATE_TIME_LABEL, ""); + + this.time_shift = time_shift; + this.keep_relativity = keep_relativity; + this.modify_originals = modify_originals; + + // TODO: implement modify originals option + + prev_events = new Gee.HashMap<Dateable, Event?>(); + + // this should be replaced by a first function when we migrate to Gee's List + foreach (DataView view in iter) { + prev_events.set(view.get_source() as Dateable, (view.get_source() as MediaSource).get_event()); + + if (new_time == null) { + new_time = ((Dateable) view.get_source()).get_exposure_time() + + (time_t) time_shift; + break; + } + } + + old_times = new Gee.HashMap<Dateable, time_t?>(); + } + + public override void execute() { + error_list = new Gee.ArrayList<Dateable>(); + base.execute(); + + if (error_list.size > 0) { + multiple_object_error_dialog(error_list, + ngettext("One original photo could not be adjusted.", + "The following original photos could not be adjusted.", error_list.size), + _("Time Adjustment Error")); + } + + ViewCollection all_events = new ViewCollection("tmp"); + + foreach (Dateable d in prev_events.keys) { + foreach (DataObject dobj in Event.global.get_all()) { + Event event = dobj as Event; + if (event != null) { + all_events.add(new EventView(event)); + } + } + Event.generate_single_event(d, all_events, null); + } + } + + public override void undo() { + error_list = new Gee.ArrayList<Dateable>(); + base.undo(); + + if (error_list.size > 0) { + multiple_object_error_dialog(error_list, + ngettext("Time adjustments could not be undone on the following photo file.", + "Time adjustments could not be undone on the following photo files.", + error_list.size), _("Time Adjustment Error")); + } + } + + private void set_time(Dateable dateable, time_t exposure_time) { + // set_exposure_time_persistent wouldn't work on videos, + // since we can't actually write them from inside shotwell, + // so check whether we're working on a Photo or a Video + if (modify_originals && (dateable is Photo)) { + try { + ((Photo) dateable).set_exposure_time_persistent(exposure_time); + } catch(GLib.Error err) { + error_list.add(dateable); + } + } else { + // modifying originals is disabled, or this is a + // video + dateable.set_exposure_time(exposure_time); + } + } + + public override void execute_on_source(DataSource source) { + Dateable dateable = ((Dateable) source); + + if (keep_relativity && dateable.get_exposure_time() != 0) { + set_time(dateable, dateable.get_exposure_time() + (time_t) time_shift); + } else { + old_times.set(dateable, dateable.get_exposure_time()); + set_time(dateable, new_time); + } + + ViewCollection all_events = new ViewCollection("tmp"); + + foreach (DataObject dobj in Event.global.get_all()) { + Event event = dobj as Event; + if (event != null) { + all_events.add(new EventView(event)); + } + } + Event.generate_single_event(dateable, all_events, null); + } + + public override void undo_on_source(DataSource source) { + Dateable photo = ((Dateable) source); + + if (old_times.has_key(photo)) { + set_time(photo, old_times.get(photo)); + old_times.unset(photo); + } else { + set_time(photo, photo.get_exposure_time() - (time_t) time_shift); + } + + (source as MediaSource).set_event(prev_events.get(source as Dateable)); + } +} + +public class AddTagsCommand : PageCommand { + private Gee.HashMap<SourceProxy, Gee.ArrayList<MediaSource>> map = + new Gee.HashMap<SourceProxy, Gee.ArrayList<MediaSource>>(); + + public AddTagsCommand(string[] paths, Gee.Collection<MediaSource> sources) { + base (Resources.add_tags_label(paths), ""); + + // load/create the tags here rather than in execute() so that we can merely use the proxy + // to access it ... this is important with the redo() case, where the tags may have been + // created by another proxy elsewhere + foreach (string path in paths) { + Gee.List<string> paths_to_create = + HierarchicalTagUtilities.enumerate_parent_paths(path); + paths_to_create.add(path); + + foreach (string create_path in paths_to_create) { + Tag tag = Tag.for_path(create_path); + SourceProxy tag_proxy = tag.get_proxy(); + + // for each Tag, only attach sources which are not already attached, otherwise undo() + // will not be symmetric + Gee.ArrayList<MediaSource> add_sources = new Gee.ArrayList<MediaSource>(); + foreach (MediaSource source in sources) { + if (!tag.contains(source)) + add_sources.add(source); + } + + if (add_sources.size > 0) { + tag_proxy.broken.connect(on_proxy_broken); + map.set(tag_proxy, add_sources); + } + } + } + + LibraryPhoto.global.item_destroyed.connect(on_source_destroyed); + Video.global.item_destroyed.connect(on_source_destroyed); + } + + ~AddTagsCommand() { + foreach (SourceProxy tag_proxy in map.keys) + tag_proxy.broken.disconnect(on_proxy_broken); + + LibraryPhoto.global.item_destroyed.disconnect(on_source_destroyed); + Video.global.item_destroyed.disconnect(on_source_destroyed); + } + + public override void execute() { + foreach (SourceProxy tag_proxy in map.keys) + ((Tag) tag_proxy.get_source()).attach_many(map.get(tag_proxy)); + } + + public override void undo() { + foreach (SourceProxy tag_proxy in map.keys) { + Tag tag = (Tag) tag_proxy.get_source(); + + tag.detach_many(map.get(tag_proxy)); + } + } + + private void on_source_destroyed(DataSource source) { + foreach (Gee.ArrayList<MediaSource> sources in map.values) { + if (sources.contains((MediaSource) source)) { + get_command_manager().reset(); + + return; + } + } + } + + private void on_proxy_broken() { + get_command_manager().reset(); + } +} + +public class RenameTagCommand : SimpleProxyableCommand { + private string old_name; + private string new_name; + + // NOTE: new_name should be a name, not a path + public RenameTagCommand(Tag tag, string new_name) { + base (tag, Resources.rename_tag_label(tag.get_user_visible_name(), new_name), + tag.get_name()); + + old_name = tag.get_user_visible_name(); + this.new_name = new_name; + } + + protected override void execute_on_source(DataSource source) { + if (!((Tag) source).rename(new_name)) + AppWindow.error_message(Resources.rename_tag_exists_message(new_name)); + } + + protected override void undo_on_source(DataSource source) { + if (!((Tag) source).rename(old_name)) + AppWindow.error_message(Resources.rename_tag_exists_message(old_name)); + } +} + +public class DeleteTagCommand : SimpleProxyableCommand { + Gee.List<SourceProxy>? recursive_victim_proxies = null; + + public DeleteTagCommand(Tag tag) { + base (tag, Resources.delete_tag_label(tag.get_user_visible_name()), tag.get_name()); + } + + protected override void execute_on_source(DataSource source) { + Tag tag = (Tag) source; + + // process children first, if any + Gee.List<Tag> recursive_victims = tag.get_hierarchical_children(); + if (recursive_victims.size > 0) { + // save proxies for these Tags and then delete, in order .. can't use mark_many() or + // add_proxyables() here because they make no guarantee of order + recursive_victim_proxies = new Gee.ArrayList<SourceProxy>(); + foreach (Tag victim in recursive_victims) { + SourceProxy proxy = victim.get_proxy(); + proxy.broken.connect(on_proxy_broken); + recursive_victim_proxies.add(proxy); + + Tag.global.destroy_marked(Tag.global.mark(victim), false); + } + } + + // destroy parent tag, which is already proxied + Tag.global.destroy_marked(Tag.global.mark(source), false); + } + + protected override void undo_on_source(DataSource source) { + // merely instantiating the Tag will rehydrate it ... should always work, because the + // undo stack is cleared if the proxy ever breaks + assert(source is Tag); + + // rehydrate the children, in reverse order + if (recursive_victim_proxies != null) { + for (int i = recursive_victim_proxies.size - 1; i >= 0; i--) { + SourceProxy proxy = recursive_victim_proxies.get(i); + + DataSource victim_source = proxy.get_source(); + assert(victim_source is Tag); + + proxy.broken.disconnect(on_proxy_broken); + } + + recursive_victim_proxies = null; + } + } + + private void on_proxy_broken() { + get_command_manager().reset(); + } +} + +public class NewChildTagCommand : SimpleProxyableCommand { + Tag? created_child = null; + + public NewChildTagCommand(Tag tag) { + base (tag, _("Create Tag"), tag.get_name()); + } + + protected override void execute_on_source(DataSource source) { + Tag tag = (Tag) source; + created_child = tag.create_new_child(); + } + + protected override void undo_on_source(DataSource source) { + Tag.global.destroy_marked(Tag.global.mark(created_child), true); + } + + public Tag get_created_child() { + assert(created_child != null); + + return created_child; + } +} + +public class NewRootTagCommand : PageCommand { + SourceProxy? created_proxy = null; + + public NewRootTagCommand() { + base (_("Create Tag"), ""); + } + + protected override void execute() { + if (created_proxy == null) + created_proxy = Tag.create_new_root().get_proxy(); + else + created_proxy.get_source(); + } + + protected override void undo() { + Tag.global.destroy_marked(Tag.global.mark(created_proxy.get_source()), true); + } + + public Tag get_created_tag() { + return (Tag) created_proxy.get_source(); + } +} + +public class ReparentTagCommand : PageCommand { + string from_path; + string to_path; + string? to_path_parent_path; + Gee.List<SourceProxy>? src_before_state = null; + Gee.List<SourceProxy>? dest_before_state = null; + Gee.List<SourceProxy>? after_state = null; + Gee.HashSet<MediaSource> sources_in_play = new Gee.HashSet<MediaSource>(); + Gee.Map<string, Gee.Set<MediaSource>> dest_parent_attachments = null; + Gee.Map<string, Gee.Set<MediaSource>> src_parent_detachments = null; + Gee.Map<string, Gee.Set<MediaSource>> in_play_child_structure = null; + Gee.Map<string, Gee.Set<MediaSource>> existing_dest_child_structure = null; + Gee.Set<MediaSource>? existing_dest_membership = null; + bool to_path_exists = false; + + public ReparentTagCommand(Tag tag, string new_parent_path) { + base (_("Move Tag \"%s\"").printf(tag.get_user_visible_name()), ""); + + this.from_path = tag.get_path(); + + bool has_children = (tag.get_hierarchical_children().size > 0); + string basename = tag.get_user_visible_name(); + + if (new_parent_path == Tag.PATH_SEPARATOR_STRING) + this.to_path = (has_children) ? (Tag.PATH_SEPARATOR_STRING + basename) : basename; + else if (new_parent_path.has_prefix(Tag.PATH_SEPARATOR_STRING)) + this.to_path = new_parent_path + Tag.PATH_SEPARATOR_STRING + basename; + else + this.to_path = Tag.PATH_SEPARATOR_STRING + new_parent_path + Tag.PATH_SEPARATOR_STRING + + basename; + + string? new_to_path = HierarchicalTagUtilities.get_root_path_form(to_path); + if (new_to_path != null) + this.to_path = new_to_path; + + if (Tag.global.exists(this.to_path)) + to_path_exists = true; + + sources_in_play.add_all(tag.get_sources()); + + LibraryPhoto.global.items_destroyed.connect(on_items_destroyed); + Video.global.items_destroyed.connect(on_items_destroyed); + } + + ~ReparentTagCommand() { + LibraryPhoto.global.items_destroyed.disconnect(on_items_destroyed); + Video.global.items_destroyed.disconnect(on_items_destroyed); + } + + private void on_items_destroyed(Gee.Collection<DataSource> destroyed) { + foreach (DataSource source in destroyed) { + if (sources_in_play.contains((MediaSource) source)) + get_command_manager().reset(); + } + } + + private Gee.Map<string, Gee.Set<MediaSource>> get_child_structure_at(string client_path) { + string? path = HierarchicalTagUtilities.get_root_path_form(client_path); + path = (path != null) ? path : client_path; + + Gee.Map<string, Gee.Set<MediaSource>> result = + new Gee.HashMap<string, Gee.Set<MediaSource>>(); + + if (!Tag.global.exists(path)) + return result; + + Tag tag = Tag.for_path(path); + + string path_prefix = tag.get_path() + Tag.PATH_SEPARATOR_STRING; + foreach (Tag t in tag.get_hierarchical_children()) { + string child_subpath = t.get_path().replace(path_prefix, ""); + + result.set(child_subpath, new Gee.HashSet<MediaSource>()); + result.get(child_subpath).add_all(t.get_sources()); + } + + return result; + } + + private void restore_child_attachments_at(string client_path, + Gee.Map<string, Gee.Set<MediaSource>> child_structure) { + + string? new_path = HierarchicalTagUtilities.get_root_path_form(client_path); + string path = (new_path != null) ? new_path : client_path; + + assert(Tag.global.exists(path)); + Tag tag = Tag.for_path(path); + + foreach (string child_subpath in child_structure.keys) { + string child_path = tag.get_path() + Tag.PATH_SEPARATOR_STRING + child_subpath; + + if (!tag.get_path().has_prefix(Tag.PATH_SEPARATOR_STRING)) { + tag.promote(); + child_path = tag.get_path() + Tag.PATH_SEPARATOR_STRING + child_subpath; + } + + assert(Tag.global.exists(child_path)); + + foreach (MediaSource s in child_structure.get(child_subpath)) + Tag.for_path(child_path).attach(s); + } + } + + private void reattach_in_play_sources_at(string client_path) { + string? new_path = HierarchicalTagUtilities.get_root_path_form(client_path); + string path = (new_path != null) ? new_path : client_path; + + assert(Tag.global.exists(path)); + + Tag tag = Tag.for_path(path); + + foreach (MediaSource s in sources_in_play) + tag.attach(s); + } + + private void save_before_state() { + assert(src_before_state == null); + assert(dest_before_state == null); + + src_before_state = new Gee.ArrayList<SourceProxy>(); + dest_before_state = new Gee.ArrayList<SourceProxy>(); + + // capture the child structure of the from tag + assert(in_play_child_structure == null); + in_play_child_structure = get_child_structure_at(from_path); + + // save the state of the from tag + assert(Tag.global.exists(from_path)); + Tag from_tag = Tag.for_path(from_path); + src_before_state.add(from_tag.get_proxy()); + + // capture the child structure of the parent of the to tag, if the to tag has a parent + Gee.List<string> parent_paths = HierarchicalTagUtilities.enumerate_parent_paths(to_path); + if (parent_paths.size > 0) + to_path_parent_path = parent_paths.get(parent_paths.size - 1); + if (to_path_parent_path != null) { + assert(existing_dest_child_structure == null); + existing_dest_child_structure = get_child_structure_at(to_path_parent_path); + } + + // if the to tag doesn't have a parent, then capture the structure of the to tag itself + if (to_path_parent_path == null) { + assert(existing_dest_child_structure == null); + assert(existing_dest_membership == null); + existing_dest_child_structure = get_child_structure_at(to_path); + existing_dest_membership = new Gee.HashSet<MediaSource>(); + existing_dest_membership.add_all(Tag.for_path(to_path).get_sources()); + } + + // save the state of the to tag's parent + if (to_path_parent_path != null) { + string? new_tpp = HierarchicalTagUtilities.get_root_path_form(to_path_parent_path); + to_path_parent_path = (new_tpp != null) ? new_tpp : to_path_parent_path; + assert(Tag.global.exists(to_path_parent_path)); + dest_before_state.add(Tag.for_path(to_path_parent_path).get_proxy()); + } + + // if the to tag doesn't have a parent, save the state of the to tag itself + if (to_path_parent_path == null) { + dest_before_state.add(Tag.for_path(to_path).get_proxy()); + } + + // save the state of the children of the from tag in order from most basic to most derived + Gee.List<Tag> from_children = from_tag.get_hierarchical_children(); + for (int i = from_children.size - 1; i >= 0; i--) + src_before_state.add(from_children.get(i).get_proxy()); + + // save the state of the children of the to tag's parent in order from most basic to most + // derived + if (to_path_parent_path != null) { + Gee.List<Tag> to_children = Tag.for_path(to_path_parent_path).get_hierarchical_children(); + for (int i = to_children.size - 1; i >= 0; i--) + dest_before_state.add(to_children.get(i).get_proxy()); + } + + // if the to tag doesn't have a parent, then save the state of the to tag's direct + // children, if any + if (to_path_parent_path == null) { + Gee.List<Tag> to_children = Tag.for_path(to_path).get_hierarchical_children(); + for (int i = to_children.size - 1; i >= 0; i--) + dest_before_state.add(to_children.get(i).get_proxy()); + } + } + + private void restore_before_state() { + assert(src_before_state != null); + assert(existing_dest_child_structure != null); + + // unwind the destination tree to its pre-merge state + if (to_path_parent_path != null) { + string? new_tpp = HierarchicalTagUtilities.get_root_path_form(to_path_parent_path); + to_path_parent_path = (new_tpp != null) ? new_tpp : to_path_parent_path; + } + + string unwind_target = (to_path_parent_path != null) ? to_path_parent_path : to_path; + foreach (Tag t in Tag.for_path(unwind_target).get_hierarchical_children()) { + string child_subpath = t.get_path().replace(unwind_target, ""); + if (child_subpath.has_prefix(Tag.PATH_SEPARATOR_STRING)) + child_subpath = child_subpath.substring(1); + + if (!existing_dest_child_structure.has_key(child_subpath)) { + Tag.global.destroy_marked(Tag.global.mark(t), true); + } else { + Gee.Set<MediaSource> starting_sources = new Gee.HashSet<MediaSource>(); + starting_sources.add_all(t.get_sources()); + foreach (MediaSource source in starting_sources) + if (!(existing_dest_child_structure.get(child_subpath).contains(source))) + t.detach(source); + } + } + + for (int i = 0; i < src_before_state.size; i++) + src_before_state.get(i).get_source(); + + for (int i = 0; i < dest_before_state.size; i++) + dest_before_state.get(i).get_source(); + + if (to_path_parent_path != null) { + string? new_path = HierarchicalTagUtilities.get_root_path_form(to_path_parent_path); + string path = (new_path != null) ? new_path : to_path_parent_path; + + assert(Tag.global.exists(path)); + + Tag t = Tag.for_path(path); + + Gee.List<Tag> kids = t.get_hierarchical_children(); + foreach (Tag kidtag in kids) + kidtag.detach_many(kidtag.get_sources()); + + restore_child_attachments_at(path, existing_dest_child_structure); + } else { + assert(existing_dest_membership != null); + Tag.for_path(to_path).detach_many(Tag.for_path(to_path).get_sources()); + Tag.for_path(to_path).attach_many(existing_dest_membership); + + Gee.List<Tag> kids = Tag.for_path(to_path).get_hierarchical_children(); + foreach (Tag kidtag in kids) + kidtag.detach_many(kidtag.get_sources()); + + restore_child_attachments_at(to_path, existing_dest_child_structure); + } + } + + private void save_after_state() { + assert(after_state == null); + + after_state = new Gee.ArrayList<SourceProxy>(); + + // save the state of the to tag + assert(Tag.global.exists(to_path)); + Tag to_tag = Tag.for_path(to_path); + after_state.add(to_tag.get_proxy()); + + // save the state of the children of the to tag in order from most basic to most derived + Gee.List<Tag> to_children = to_tag.get_hierarchical_children(); + for (int i = to_children.size - 1; i >= 0; i--) + after_state.add(to_children.get(i).get_proxy()); + } + + private void restore_after_state() { + assert(after_state != null); + + for (int i = 0; i < after_state.size; i++) + after_state.get(i).get_source(); + } + + private void prepare_parent(string path) { + // find our new parent tag (if one exists) and promote it + Tag? new_parent = null; + if (path.has_prefix(Tag.PATH_SEPARATOR_STRING)) { + Gee.List<string> parent_paths = HierarchicalTagUtilities.enumerate_parent_paths(path); + if (parent_paths.size > 0) { + string immediate_parent_path = parent_paths.get(parent_paths.size - 1); + if (Tag.global.exists(immediate_parent_path)) + new_parent = Tag.for_path(immediate_parent_path); + else if (Tag.global.exists(immediate_parent_path.substring(1))) + new_parent = Tag.for_path(immediate_parent_path.substring(1)); + else + assert_not_reached(); + } + } + if (new_parent != null) + new_parent.promote(); + } + + private void do_source_parent_detachments() { + assert(Tag.global.exists(from_path)); + Tag from_tag = Tag.for_path(from_path); + + // see if this copy operation will detach any media items from the source tag's parents + if (src_parent_detachments == null) { + src_parent_detachments = new Gee.HashMap<string, Gee.Set<MediaSource>>(); + foreach (MediaSource source in from_tag.get_sources()) { + Tag? current_parent = from_tag.get_hierarchical_parent(); + int running_attach_count = from_tag.get_attachment_count(source) + 1; + while (current_parent != null) { + string current_parent_path = current_parent.get_path(); + if (!src_parent_detachments.has_key(current_parent_path)) + src_parent_detachments.set(current_parent_path, new Gee.HashSet<MediaSource>()); + + int curr_parent_attach_count = current_parent.get_attachment_count(source); + + assert (curr_parent_attach_count >= running_attach_count); + + // if this parent tag has no other child tags that the current media item is + // attached to + if (curr_parent_attach_count == running_attach_count) + src_parent_detachments.get(current_parent_path).add(source); + + running_attach_count++; + current_parent = current_parent.get_hierarchical_parent(); + } + } + } + + // perform collected detachments + foreach (string p in src_parent_detachments.keys) + foreach (MediaSource s in src_parent_detachments.get(p)) + Tag.for_path(p).detach(s); + } + + private void do_source_parent_reattachments() { + assert(src_parent_detachments != null); + + foreach (string p in src_parent_detachments.keys) + foreach (MediaSource s in src_parent_detachments.get(p)) + Tag.for_path(p).attach(s); + } + + private void do_destination_parent_detachments() { + assert(dest_parent_attachments != null); + + foreach (string p in dest_parent_attachments.keys) + foreach (MediaSource s in dest_parent_attachments.get(p)) + Tag.for_path(p).detach(s); + } + + private void do_destination_parent_reattachments() { + assert(dest_parent_attachments != null); + + foreach (string p in dest_parent_attachments.keys) + foreach (MediaSource s in dest_parent_attachments.get(p)) + Tag.for_path(p).attach(s); + } + + private void copy_subtree(string from, string to) { + assert(Tag.global.exists(from)); + Tag from_tag = Tag.for_path(from); + + // get (or create) a tag for the destination path + Tag to_tag = Tag.for_path(to); + + // see if this copy operation will attach any new media items to the destination's parents, + // if so, record them for later undo/redo + dest_parent_attachments = new Gee.HashMap<string, Gee.Set<MediaSource>>(); + foreach (MediaSource source in from_tag.get_sources()) { + Tag? current_parent = to_tag.get_hierarchical_parent(); + while (current_parent != null) { + string current_parent_path = current_parent.get_path(); + if (!dest_parent_attachments.has_key(current_parent_path)) + dest_parent_attachments.set(current_parent_path, new Gee.HashSet<MediaSource>()); + + if (!current_parent.contains(source)) + dest_parent_attachments.get(current_parent_path).add(source); + + current_parent = current_parent.get_hierarchical_parent(); + } + } + + foreach (MediaSource source in from_tag.get_sources()) + to_tag.attach(source); + + // loop through the children of the from tag in order from most basic to most derived, + // creating corresponding child tags on the to tag and attaching corresponding sources + Gee.List<Tag> from_children = from_tag.get_hierarchical_children(); + for (int i = from_children.size - 1; i >= 0; i--) { + Tag from_child = from_children.get(i); + + string child_subpath = from_child.get_path().replace(from + Tag.PATH_SEPARATOR_STRING, + ""); + + Tag to_child = Tag.for_path(to_tag.get_path() + Tag.PATH_SEPARATOR_STRING + + child_subpath); + + foreach (MediaSource source in from_child.get_sources()) + to_child.attach(source); + } + } + + private void destroy_subtree(string client_path) { + string? victim_path = HierarchicalTagUtilities.get_root_path_form(client_path); + if (victim_path == null) + victim_path = client_path; + + if (!Tag.global.exists(victim_path)) + return; + + Tag victim = Tag.for_path(victim_path); + + // destroy the children of the victim in order from most derived to most basic + Gee.List<Tag> victim_children = victim.get_hierarchical_children(); + for (int i = 0; i < victim_children.size; i++) + Tag.global.destroy_marked(Tag.global.mark(victim_children.get(i)), true); + + // destroy the victim itself + Tag.global.destroy_marked(Tag.global.mark(victim), true); + } + + public override void execute() { + if (after_state == null) { + save_before_state(); + + prepare_parent(to_path); + + copy_subtree(from_path, to_path); + + save_after_state(); + + do_source_parent_detachments(); + + destroy_subtree(from_path); + } else { + prepare_parent(to_path); + + restore_after_state(); + + restore_child_attachments_at(to_path, in_play_child_structure); + reattach_in_play_sources_at(to_path); + + do_source_parent_detachments(); + do_destination_parent_reattachments(); + + destroy_subtree(from_path); + } + } + + public override void undo() { + assert(src_before_state != null); + + prepare_parent(from_path); + + restore_before_state(); + + if (!to_path_exists) + destroy_subtree(to_path); + + restore_child_attachments_at(from_path, in_play_child_structure); + reattach_in_play_sources_at(from_path); + + do_source_parent_reattachments(); + do_destination_parent_detachments(); + + HierarchicalTagUtilities.cleanup_root_path(to_path); + HierarchicalTagUtilities.cleanup_root_path(from_path); + if (to_path_parent_path != null) + HierarchicalTagUtilities.cleanup_root_path(to_path_parent_path); + } +} + +public class ModifyTagsCommand : SingleDataSourceCommand { + private MediaSource media; + private Gee.ArrayList<SourceProxy> to_add = new Gee.ArrayList<SourceProxy>(); + private Gee.ArrayList<SourceProxy> to_remove = new Gee.ArrayList<SourceProxy>(); + + public ModifyTagsCommand(MediaSource media, Gee.Collection<Tag> new_tag_list) { + base (media, Resources.MODIFY_TAGS_LABEL, ""); + + this.media = media; + + // Prepare to remove all existing tags, if any, from the current media source. + Gee.List<Tag>? original_tags = Tag.global.fetch_for_source(media); + if (original_tags != null) { + foreach (Tag tag in original_tags) { + SourceProxy proxy = tag.get_proxy(); + to_remove.add(proxy); + proxy.broken.connect(on_proxy_broken); + } + } + + // Prepare to add all new tags; remember, if a tag is added, its parent must be + // added as well. So enumerate all paths to add and then get the tags for them. + Gee.SortedSet<string> new_paths = new Gee.TreeSet<string>(); + foreach (Tag new_tag in new_tag_list) { + string new_tag_path = new_tag.get_path(); + + new_paths.add(new_tag_path); + new_paths.add_all(HierarchicalTagUtilities.enumerate_parent_paths(new_tag_path)); + } + + foreach (string path in new_paths) { + assert(Tag.global.exists(path)); + + SourceProxy proxy = Tag.for_path(path).get_proxy(); + to_add.add(proxy); + proxy.broken.connect(on_proxy_broken); + } + } + + ~ModifyTagsCommand() { + foreach (SourceProxy proxy in to_add) + proxy.broken.disconnect(on_proxy_broken); + + foreach (SourceProxy proxy in to_remove) + proxy.broken.disconnect(on_proxy_broken); + } + + public override void execute() { + foreach (SourceProxy proxy in to_remove) + ((Tag) proxy.get_source()).detach(media); + + foreach (SourceProxy proxy in to_add) + ((Tag) proxy.get_source()).attach(media); + } + + public override void undo() { + foreach (SourceProxy proxy in to_add) + ((Tag) proxy.get_source()).detach(media); + + foreach (SourceProxy proxy in to_remove) + ((Tag) proxy.get_source()).attach(media); + } + + private void on_proxy_broken() { + get_command_manager().reset(); + } +} + +public class TagUntagPhotosCommand : SimpleProxyableCommand { + private Gee.Collection<MediaSource> sources; + private bool attach; + private Gee.MultiMap<Tag, MediaSource>? detached_from = null; + private Gee.List<Tag>? attached_to = null; + + public TagUntagPhotosCommand(Tag tag, Gee.Collection<MediaSource> sources, int count, bool attach) { + base (tag, + attach ? Resources.tag_photos_label(tag.get_user_visible_name(), count) + : Resources.untag_photos_label(tag.get_user_visible_name(), count), + tag.get_name()); + + this.sources = sources; + this.attach = attach; + + LibraryPhoto.global.item_destroyed.connect(on_source_destroyed); + Video.global.item_destroyed.connect(on_source_destroyed); + } + + ~TagUntagPhotosCommand() { + LibraryPhoto.global.item_destroyed.disconnect(on_source_destroyed); + Video.global.item_destroyed.disconnect(on_source_destroyed); + } + + public override void execute_on_source(DataSource source) { + if (attach) + do_attach((Tag) source); + else + do_detach((Tag) source); + } + + public override void undo_on_source(DataSource source) { + if (attach) + do_detach((Tag) source); + else + do_attach((Tag) source); + } + + private void do_attach(Tag tag) { + // if not attaching previously detached Tags, attach and done + if (detached_from == null) { + tag.attach_many(sources); + + attached_to = new Gee.ArrayList<Tag>(); + + Tag curr_tmp = tag; + + while (curr_tmp != null) { + attached_to.add(curr_tmp); + curr_tmp = curr_tmp.get_hierarchical_parent(); + } + + return; + } + + // reattach + foreach (Tag detached_tag in detached_from.get_all_keys()) + detached_tag.attach_many(detached_from.get(detached_tag)); + + detached_from = null; + clear_added_proxies(); + } + + private void do_detach(Tag tag) { + if (attached_to == null) { + // detaching a MediaSource from a Tag may result in the MediaSource being detached from + // many tags (due to heirarchical tagging), so save the MediaSources for each detached + // Tag for reversing the process + detached_from = tag.detach_many(sources); + + // since the "master" Tag (supplied in the ctor) is not necessarily the only one being + // saved, add proxies for all of the other ones as well + add_proxyables(detached_from.get_keys()); + } else { + foreach (Tag t in attached_to) { + foreach (MediaSource ms in sources) { + // is this photo/video attached to this tag elsewhere? + if (t.get_attachment_count(ms) < 2) { + //no, remove it. + t.detach(ms); + } + } + } + } + } + + private void on_source_destroyed(DataSource source) { + debug("on_source_destroyed: %s", source.to_string()); + if (sources.contains((MediaSource) source)) + get_command_manager().reset(); + } +} + +public class RenameSavedSearchCommand : SingleDataSourceCommand { + private SavedSearch search; + private string old_name; + private string new_name; + + public RenameSavedSearchCommand(SavedSearch search, string new_name) { + base (search, Resources.rename_search_label(search.get_name(), new_name), search.get_name()); + + this.search = search; + old_name = search.get_name(); + this.new_name = new_name; + } + + public override void execute() { + if (!search.rename(new_name)) + AppWindow.error_message(Resources.rename_search_exists_message(new_name)); + } + + public override void undo() { + if (!search.rename(old_name)) + AppWindow.error_message(Resources.rename_search_exists_message(old_name)); + } +} + +public class DeleteSavedSearchCommand : SingleDataSourceCommand { + private SavedSearch search; + + public DeleteSavedSearchCommand(SavedSearch search) { + base (search, Resources.delete_search_label(search.get_name()), search.get_name()); + + this.search = search; + } + + public override void execute() { + SavedSearchTable.get_instance().remove(search); + } + + public override void undo() { + search.reconstitute(); + } +} + +public class TrashUntrashPhotosCommand : PageCommand { + private Gee.Collection<MediaSource> sources; + private bool to_trash; + + public TrashUntrashPhotosCommand(Gee.Collection<MediaSource> sources, bool to_trash) { + base ( + to_trash ? _("Move Photos to Trash") : _("Restore Photos from Trash"), + to_trash ? _("Move the photos to the Shotwell trash") : _("Restore the photos back to the Shotwell library")); + + this.sources = sources; + this.to_trash = to_trash; + + LibraryPhoto.global.item_destroyed.connect(on_photo_destroyed); + Video.global.item_destroyed.connect(on_photo_destroyed); + } + + ~TrashUntrashPhotosCommand() { + LibraryPhoto.global.item_destroyed.disconnect(on_photo_destroyed); + Video.global.item_destroyed.disconnect(on_photo_destroyed); + } + + private ProgressDialog? get_progress_dialog(bool to_trash) { + if (sources.size <= 5) + return null; + + ProgressDialog dialog = new ProgressDialog(AppWindow.get_instance(), + to_trash ? _("Moving Photos to Trash") : _("Restoring Photos From Trash")); + dialog.update_display_every((sources.size / 5).clamp(2, 10)); + + return dialog; + } + + public override void execute() { + ProgressDialog? dialog = get_progress_dialog(to_trash); + + ProgressMonitor monitor = null; + if (dialog != null) + monitor = dialog.monitor; + + if (to_trash) + trash(monitor); + else + untrash(monitor); + + if (dialog != null) + dialog.close(); + } + + public override void undo() { + ProgressDialog? dialog = get_progress_dialog(!to_trash); + + ProgressMonitor monitor = null; + if (dialog != null) + monitor = dialog.monitor; + + if (to_trash) + untrash(monitor); + else + trash(monitor); + + if (dialog != null) + dialog.close(); + } + + private void trash(ProgressMonitor? monitor) { + int ctr = 0; + int count = sources.size; + + LibraryPhoto.global.transaction_controller.begin(); + Video.global.transaction_controller.begin(); + + foreach (MediaSource source in sources) { + source.trash(); + if (monitor != null) + monitor(++ctr, count); + } + + LibraryPhoto.global.transaction_controller.commit(); + Video.global.transaction_controller.commit(); + } + + private void untrash(ProgressMonitor? monitor) { + int ctr = 0; + int count = sources.size; + + LibraryPhoto.global.transaction_controller.begin(); + Video.global.transaction_controller.begin(); + + foreach (MediaSource source in sources) { + source.untrash(); + if (monitor != null) + monitor(++ctr, count); + } + + LibraryPhoto.global.transaction_controller.commit(); + Video.global.transaction_controller.commit(); + } + + private void on_photo_destroyed(DataSource source) { + // in this case, don't need to reset the command manager, simply remove the photo from the + // internal list and allow the others to be moved to and from the trash + sources.remove((MediaSource) source); + + // however, if all photos missing, then remove this from the command stack, and there's + // only one way to do that + if (sources.size == 0) + get_command_manager().reset(); + } +} + +public class FlagUnflagCommand : MultipleDataSourceAtOnceCommand { + private const int MIN_PROGRESS_BAR_THRESHOLD = 1000; + private const string FLAG_SELECTED_STRING = _("Flag selected photos"); + private const string UNFLAG_SELECTED_STRING = _("Unflag selected photos"); + private const string FLAG_PROGRESS = _("Flagging selected photos"); + private const string UNFLAG_PROGRESS = _("Unflagging selected photos"); + + private bool flag; + private ProgressDialog progress_dialog = null; + + public FlagUnflagCommand(Gee.Collection<MediaSource> sources, bool flag) { + base (sources, + flag ? _("Flag") : _("Unflag"), + flag ? FLAG_SELECTED_STRING : UNFLAG_SELECTED_STRING); + + this.flag = flag; + + if (sources.size >= MIN_PROGRESS_BAR_THRESHOLD) { + progress_dialog = new ProgressDialog(null, + flag ? FLAG_PROGRESS : UNFLAG_PROGRESS); + + progress_dialog.show_all(); + } + } + + public override void execute_on_all(Gee.Collection<DataSource> sources) { + int num_processed = 0; + + foreach (DataSource source in sources) { + flag_unflag(source, flag); + + num_processed++; + + if (progress_dialog != null) { + progress_dialog.set_fraction(num_processed, sources.size); + progress_dialog.queue_draw(); + spin_event_loop(); + } + } + + if (progress_dialog != null) + progress_dialog.hide(); + } + + public override void undo_on_all(Gee.Collection<DataSource> sources) { + foreach (DataSource source in sources) + flag_unflag(source, !flag); + } + + private void flag_unflag(DataSource source, bool flag) { + Flaggable? flaggable = source as Flaggable; + if (flaggable != null) { + if (flag) + flaggable.mark_flagged(); + else + flaggable.mark_unflagged(); + } + } +} |