/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ // // Going forward, Shotwell will use MediaInterfaces, which allow for various operations and features // to be added only to the MediaSources that support them (or make sense for). For example, adding // a library-mode photo or video to an Event makes perfect sense, but does not make sense for a // direct-mode photo. All three are MediaSources, and to make DirectPhoto descend from another // base class is only inviting chaos and a tremendous amount of replicated code. // // A key point to make of all MediaInterfaces is that they require MediaSource as a base class. // Thus, any code dealing with one of these interfaces knows they are also dealing with a // MediaSource. // // TODO: Make Eventable and Taggable interfaces, which are the only types Event and Tag will deal // with (rather than MediaSources). // // TODO: Make Trashable interface, which are much like Flaggable. // // TODO: ContainerSources may also have specific needs in the future; an interface-based system // may make sense as well when that need arises. // // // TransactionController // // Because many operations in Shotwell need to be performed on collections of objects all at once, // and that most of these objects are backed by a database, the TransactionController object gives // a way to generically group a series of operations on one or more similar objects into a single // transaction. This class is listed here because it's used by the various media interfaces to offer // multiple operations. // // begin() and commit() may be called multiple times in layering fashion. The implementation // accounts for this. If either throws an exception it should be assumed that the object is in // a "clean" state; that is, if begin() throws an exception, there is no need to call commit(), // and if commit() throws an exception, it does not need to be called again to revert the object // state. // // This means that any user who calls begin() *must* match it with a corresponding commit(), even // if there is an error during the transaction. It is up to the user to back out any undesired // changes. // // Because of the nature of this object, it's assumed that every object type will share one // between all callers. // // The object is thread-safe. There is no guarantee that the underlying persistent store is, // however. public abstract class TransactionController { private int count = 0; ~TransactionController() { lock (count) { assert(count == 0); } } public void begin() { lock (count) { if (count++ != 0) return; try { begin_impl(); } catch (Error err) { // unwind count--; if (err is DatabaseError) AppWindow.database_error((DatabaseError) err); else AppWindow.panic("%s".printf(err.message)); } } } // For thread safety, this method will only be called under the protection of a mutex. public abstract void begin_impl() throws Error; public void commit() { lock (count) { assert(count > 0); if (--count != 0) return; // no need to unwind the count here; it's already unwound. try { commit_impl(); } catch (Error err) { if (err is DatabaseError) AppWindow.database_error((DatabaseError) err); else AppWindow.panic("%s".printf(err.message)); } } } // For thread safety, this method will only be called under the protection of a mutex. public abstract void commit_impl() throws Error; } // // Flaggable // // Flaggable media can be marked for later use in batch operations. // // The mark_flagged() and mark_unflagged() methods should fire "metadata:flags" and "metadata:flagged" // alterations if the flag has changed. public interface Flaggable : MediaSource { public abstract bool is_flagged(); public abstract void mark_flagged(); public abstract void mark_unflagged(); public static void mark_many_flagged_unflagged(Gee.Collection? flag, Gee.Collection? unflag, TransactionController controller) throws Error { controller.begin(); if (flag != null) { foreach (Flaggable flaggable in flag) flaggable.mark_flagged(); } if (unflag != null) { foreach (Flaggable flaggable in unflag) flaggable.mark_unflagged(); } controller.commit(); } } // // Monitorable // // Monitorable media can be updated at startup or run-time about changes to their backing file(s). // // The mark_online() and mark_offline() methods should fire "metadata:flags" and "metadata:online-state" // alterations if the flag has changed. // // The set_master_file() method should fire "backing:master" alteration and "metadata:name" if // the name of the file is determined by the filename (which is default behavior). It should also // call notify_master_file_replaced(). // // The set_master_timestamp() method should fire "metadata:master-timestamp" alteration. public interface Monitorable : MediaSource { public abstract bool is_offline(); public abstract void mark_online(); public abstract void mark_offline(); public static void mark_many_online_offline(Gee.Collection? online, Gee.Collection? offline, TransactionController controller) throws Error { controller.begin(); if (online != null) { foreach (Monitorable monitorable in online) monitorable.mark_online(); } if (offline != null) { foreach (Monitorable monitorable in offline) monitorable.mark_offline(); } controller.commit(); } public abstract void set_master_file(File file); public static void set_many_master_file(Gee.Map map, TransactionController controller) throws Error { controller.begin(); Gee.MapIterator map_iter = map.map_iterator(); while (map_iter.next()) map_iter.get_key().set_master_file(map_iter.get_value()); controller.commit(); } public abstract void set_master_timestamp(FileInfo info); public static void set_many_master_timestamp(Gee.Map map, TransactionController controller) throws Error { controller.begin(); Gee.MapIterator map_iter = map.map_iterator(); while (map_iter.next()) map_iter.get_key().set_master_timestamp(map_iter.get_value()); controller.commit(); } } // // Dateable // // Dateable media may have their exposure date and time set arbitrarily. // // The set_exposure_time() method refactors the existing set_exposure_time() // from Photo to here in order to add this capability to videos. It should // fire a "metadata:exposure-time" alteration when called. public interface Dateable : MediaSource { public abstract void set_exposure_time(DateTime target_time); public abstract DateTime? get_exposure_time(); }