/* 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. */ public class EventSourceCollection : ContainerSourceCollection { public signal void no_event_collection_altered(); private ViewCollection no_event; private class NoEventViewManager : ViewManager { public override bool include_in_view(DataSource source) { // Note: this is not threadsafe return (((MediaSource) source).get_event_id().id != EventID.INVALID) ? false : base.include_in_view(source); } public override DataView create_view(DataSource source) { return new ThumbnailView((MediaSource) source); } } public EventSourceCollection() { base(Event.TYPENAME, "EventSourceCollection", get_event_key); attach_collection(LibraryPhoto.global); attach_collection(Video.global); } public void init() { no_event = new ViewCollection("No Event View Collection"); NoEventViewManager view_manager = new NoEventViewManager(); Alteration filter_alteration = new Alteration("metadata", "event"); no_event.monitor_source_collection(LibraryPhoto.global, view_manager, filter_alteration); no_event.monitor_source_collection(Video.global, view_manager, filter_alteration); no_event.contents_altered.connect(on_no_event_collection_altered); } public override bool holds_type_of_source(DataSource source) { return source is Event; } private static int64 get_event_key(DataSource source) { Event event = (Event) source; EventID event_id = event.get_event_id(); return event_id.id; } public Event? fetch(EventID event_id) { return (Event) fetch_by_key(event_id.id); } protected override Gee.Collection? get_containers_holding_source(DataSource source) { Event? event = ((MediaSource) source).get_event(); if (event == null) return null; Gee.ArrayList list = new Gee.ArrayList(); list.add(event); return list; } protected override ContainerSource? convert_backlink_to_container(SourceBacklink backlink) { EventID event_id = EventID(backlink.instance_id); Event? event = fetch(event_id); if (event != null) return event; foreach (ContainerSource container in get_holding_tank()) { if (((Event) container).get_event_id().id == event_id.id) return container; } return null; } public Gee.Collection get_no_event_objects() { return no_event.get_sources(); } private void on_no_event_collection_altered(Gee.Iterable? added, Gee.Iterable? removed) { no_event_collection_altered(); } } public class Event : EventSource, ContainerSource, Proxyable, Indexable { public const string TYPENAME = "event"; // SHOW_COMMENTS (bool) public const string PROP_SHOW_COMMENTS = "show-comments"; // In 24-hour time. public const int EVENT_BOUNDARY_HOUR = 4; private const time_t TIME_T_DAY = 24 * 60 * 60; private class EventSnapshot : SourceSnapshot { private EventRow row; private MediaSource primary_source; private Gee.ArrayList attached_sources = new Gee.ArrayList(); public EventSnapshot(Event event) { // save current state of event row = EventTable.get_instance().get_row(event.get_event_id()); primary_source = event.get_primary_source(); // stash all the media sources in the event ... these are not used when reconstituting // the event, but need to know when they're destroyed, as that means the event cannot // be restored foreach (MediaSource source in event.get_media()) attached_sources.add(source); LibraryPhoto.global.item_destroyed.connect(on_attached_source_destroyed); Video.global.item_destroyed.connect(on_attached_source_destroyed); } ~EventSnapshot() { LibraryPhoto.global.item_destroyed.disconnect(on_attached_source_destroyed); Video.global.item_destroyed.disconnect(on_attached_source_destroyed); } public EventRow get_row() { return row; } public override void notify_broken() { row = new EventRow(); primary_source = null; attached_sources.clear(); base.notify_broken(); } private void on_attached_source_destroyed(DataSource source) { MediaSource media_source = (MediaSource) source; // if one of the media sources in the event goes away, reconstitution is impossible if (media_source != null && primary_source.equals(media_source)) notify_broken(); else if (attached_sources.contains(media_source)) notify_broken(); } } private class EventProxy : SourceProxy { public EventProxy(Event event) { base (event); } public override DataSource reconstitute(int64 object_id, SourceSnapshot snapshot) { EventSnapshot event_snapshot = snapshot as EventSnapshot; assert(event_snapshot != null); return Event.reconstitute(object_id, event_snapshot.get_row()); } } public static EventSourceCollection global = null; private static EventTable event_table = null; private EventID event_id; private string? raw_name; private MediaSource primary_source; private ViewCollection view; private bool unlinking = false; private bool relinking = false; private string? indexable_keywords = null; private string? comment = null; private Event(EventRow event_row, int64 object_id = INVALID_OBJECT_ID) { base (object_id); // normalize user text event_row.name = prep_event_name(event_row.name); this.event_id = event_row.event_id; this.raw_name = event_row.name; this.comment = event_row.comment; Gee.Collection event_source_ids = MediaCollectionRegistry.get_instance().get_source_ids_for_event_id(event_id); Gee.ArrayList event_thumbs = new Gee.ArrayList(); foreach (string current_source_id in event_source_ids) { MediaSource? media = MediaCollectionRegistry.get_instance().fetch_media(current_source_id); if (media != null) event_thumbs.add(new ThumbnailView(media)); } view = new ViewCollection("ViewCollection for Event %s".printf(event_id.id.to_string())); view.set_comparator(view_comparator, view_comparator_predicate); view.add_many(event_thumbs); // need to do this manually here because only want to monitor ViewCollection contents after // initial batch has been added, but need to keep EventSourceCollection apprised if (event_thumbs.size > 0) { global.notify_container_contents_added(this, event_thumbs, false); global.notify_container_contents_altered(this, event_thumbs, false, null, false); } // get the primary source for monitoring; if not available, use the first unrejected // source in the event primary_source = MediaCollectionRegistry.get_instance().fetch_media(event_row.primary_source_id); if (primary_source == null && view.get_count() > 0) { primary_source = (MediaSource) ((DataView) view.get_first_unrejected()).get_source(); event_table.set_primary_source_id(event_id, primary_source.get_source_id()); } // watch the primary source to reflect thumbnail changes if (primary_source != null) primary_source.thumbnail_altered.connect(on_primary_thumbnail_altered); // watch for for addition, removal, and alteration of photos and videos view.items_added.connect(on_media_added); view.items_removed.connect(on_media_removed); view.items_altered.connect(on_media_altered); // because we're no longer using source monitoring (for performance reasons), need to watch // for media destruction (but not removal, which is handled automatically in any case) LibraryPhoto.global.item_destroyed.connect(on_media_destroyed); Video.global.item_destroyed.connect(on_media_destroyed); update_indexable_keywords(); } ~Event() { if (primary_source != null) primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered); view.items_altered.disconnect(on_media_altered); view.items_removed.disconnect(on_media_removed); view.items_added.disconnect(on_media_added); LibraryPhoto.global.item_destroyed.disconnect(on_media_destroyed); Video.global.item_destroyed.disconnect(on_media_destroyed); } public override string get_typename() { return TYPENAME; } public override int64 get_instance_id() { return get_event_id().id; } public override string get_representative_id() { return (primary_source != null) ? primary_source.get_source_id() : get_source_id(); } public override PhotoFileFormat get_preferred_thumbnail_format() { return (primary_source != null) ? primary_source.get_preferred_thumbnail_format() : PhotoFileFormat.get_system_default_format(); } public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error { return (primary_source != null) ? primary_source.create_thumbnail(scale) : null; } public static void init(ProgressMonitor? monitor = null) { event_table = EventTable.get_instance(); global = new EventSourceCollection(); global.init(); // add all events to the global collection Gee.ArrayList events = new Gee.ArrayList(); Gee.ArrayList unlinked = new Gee.ArrayList(); Gee.ArrayList event_rows = event_table.get_events(); int count = event_rows.size; for (int ctr = 0; ctr < count; ctr++) { Event event = new Event(event_rows[ctr]); if (monitor != null) monitor(ctr, count); if (event.get_media_count() != 0) { events.add(event); continue; } // TODO: If event has no backlinks, destroy (empty Event stored in database) ... this // is expensive to check at startup time, however, should happen in background or // during a "clean" operation event.rehydrate_backlinks(global, null); unlinked.add(event); } global.add_many(events); global.init_add_many_unlinked(unlinked); } public static void terminate() { } private static int64 view_comparator(void *a, void *b) { return ((MediaSource) ((ThumbnailView *) a)->get_source()).get_exposure_time() - ((MediaSource) ((ThumbnailView *) b)->get_source()).get_exposure_time() ; } private static bool view_comparator_predicate(DataObject object, Alteration alteration) { return alteration.has_detail("metadata", "exposure-time"); } public static string? prep_event_name(string? name) { // Ticket #3218 - we tell prepare_input_text to // allow empty strings, and if the rest of the app sees // one, it already knows to rename it to // one of the default event names. return prepare_input_text(name, PrepareInputTextOptions.NORMALIZE | PrepareInputTextOptions.VALIDATE | PrepareInputTextOptions.INVALID_IS_NULL | PrepareInputTextOptions.STRIP | PrepareInputTextOptions.STRIP_CRLF, DEFAULT_USER_TEXT_INPUT_LENGTH); } // This is used by MediaSource to notify Event when it's joined. Don't use this to manually attach a // a photo or video to an Event, use MediaSource.set_event(). public void attach(MediaSource source) { view.add(new ThumbnailView(source)); } public void attach_many(Gee.Collection media) { Gee.ArrayList views = new Gee.ArrayList(); foreach (MediaSource current_source in media) views.add(new ThumbnailView(current_source)); view.add_many(views); } // This is used by internally by Photos and Videos to notify their parent Event as to when // they're leaving. Don't use this manually to detach a MediaSource; instead use // MediaSource.set_event( ) public void detach(MediaSource source) { view.remove_marked(view.mark(view.get_view_for_source(source))); } public void detach_many(Gee.Collection media) { Gee.ArrayList views = new Gee.ArrayList(); foreach (MediaSource current_source in media) { ThumbnailView? view = (ThumbnailView?) view.get_view_for_source(current_source); if (view != null) views.add(view); } view.remove_marked(view.mark_many(views)); } // TODO: A preferred way to do this is for ContainerSource to have an abstract interface for // obtaining the DataCollection in the ContainerSource of all the media objects. Then, // ContinerSource could offer this helper class. public bool contains_media_type(string media_type) { foreach (MediaSource media in get_media()) { if (media.get_typename() == media_type) return true; } return false; } private Gee.ArrayList views_to_media(Gee.Iterable views) { Gee.ArrayList media = new Gee.ArrayList(); foreach (DataObject object in views) media.add((MediaSource) ((DataView) object).get_source()); return media; } private void on_media_added(Gee.Iterable added) { Gee.Collection media = views_to_media(added); global.notify_container_contents_added(this, media, relinking); global.notify_container_contents_altered(this, media, relinking, null, false); notify_altered(new Alteration.from_list("contents:added, metadata:time")); } // Event needs to know whenever a media source is removed from the system to update the event private void on_media_removed(Gee.Iterable removed) { Gee.ArrayList media = views_to_media(removed); global.notify_container_contents_removed(this, media, unlinking); global.notify_container_contents_altered(this, null, false, media, unlinking); // update primary source if it's been removed (and there's one to take its place) foreach (MediaSource current_source in media) { if (current_source == primary_source) { if (get_media_count() > 0) set_primary_source((MediaSource) view.get_first_unrejected().get_source()); else release_primary_source(); break; } } // evaporate event if no more media in it; do not touch thereafter if (get_media_count() == 0) { global.evaporate(this); // as it's possible (highly likely, in fact) that all refs to the Event object have // gone out of scope now, do NOT touch this, but exit immediately return; } notify_altered(new Alteration.from_list("contents:removed, metadata:time")); } private void on_media_destroyed(DataSource source) { ThumbnailView? thumbnail_view = (ThumbnailView) view.get_view_for_source(source); if (thumbnail_view != null) view.remove_marked(view.mark(thumbnail_view)); } public override void notify_relinking(SourceCollection sources) { assert(get_media_count() > 0); // If the primary source was lost in the unlink, reestablish it now. if (primary_source == null) set_primary_source((MediaSource) view.get_first_unrejected().get_source()); base.notify_relinking(sources); } /** @brief This gets called when one or more media items inside this * event gets modified in some fashion. If the media item's date changes * and the event was previously undated, the name of the event needs to * change as well; all of that happens automatically in here. * * In addition, if the _rating_ of one or more media items has changed, * the thumbnail of this event may need to change, as the primary * image may have been rejected and should not be the thumbnail anymore. */ private void on_media_altered(Gee.Map items) { bool should_remake_thumb = false; foreach (Alteration alteration in items.values) { if (alteration.has_detail("metadata", "exposure-time")) { string alt_list = "metadata:time"; if(!has_name()) alt_list += (", metadata:name"); notify_altered(new Alteration.from_list(alt_list)); break; } if (alteration.has_detail("metadata", "rating")) should_remake_thumb = true; } if (should_remake_thumb) { // check whether we actually need to remake this thumbnail... if ((get_primary_source() == null) || (get_primary_source().get_rating() == Rating.REJECTED)) { // yes, rejected - drop it and get a new one... set_primary_source((MediaSource) view.get_first_unrejected().get_source()); } // ...otherwise, if the primary source wasn't rejected, just leave it alone. } } // This creates an empty event with a primary source. NOTE: This does not add the source to // the event. That must be done manually. public static Event? create_empty_event(MediaSource source) { try { Event event = new Event(EventTable.get_instance().create(source.get_source_id(), null)); global.add(event); debug("Created empty event %s", event.to_string()); return event; } catch (DatabaseError err) { AppWindow.database_error(err); return null; } } // This will create an event using the fields supplied in EventRow. The event_id is ignored. private static Event reconstitute(int64 object_id, EventRow row) { row.event_id = EventTable.get_instance().create_from_row(row); Event event = new Event(row, object_id); global.add(event); assert(global.contains(event)); debug("Reconstituted event %s", event.to_string()); return event; } public bool has_links() { return (LibraryPhoto.global.has_backlink(get_backlink()) || Video.global.has_backlink(get_backlink())); } public SourceBacklink get_backlink() { return new SourceBacklink.from_source(this); } public void break_link(DataSource source) { unlinking = true; ((MediaSource) source).set_event(null); unlinking = false; } public void break_link_many(Gee.Collection sources) { unlinking = true; Gee.ArrayList photos = new Gee.ArrayList(); Gee.ArrayList