diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/Event.vala |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/Event.vala')
-rw-r--r-- | src/Event.vala | 924 |
1 files changed, 924 insertions, 0 deletions
diff --git a/src/Event.vala b/src/Event.vala new file mode 100644 index 0000000..ed0af76 --- /dev/null +++ b/src/Event.vala @@ -0,0 +1,924 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public class 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<ContainerSource>? get_containers_holding_source(DataSource source) { + Event? event = ((MediaSource) source).get_event(); + if (event == null) + return null; + + Gee.ArrayList<ContainerSource> list = new Gee.ArrayList<ContainerSource>(); + 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<DataObject> get_no_event_objects() { + return no_event.get_sources(); + } + + private void on_no_event_collection_altered(Gee.Iterable<DataObject>? added, + Gee.Iterable<DataObject>? 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<MediaSource> attached_sources = new Gee.ArrayList<MediaSource>(); + + 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<string> event_source_ids = + MediaCollectionRegistry.get_instance().get_source_ids_for_event_id(event_id); + Gee.ArrayList<ThumbnailView> event_thumbs = new Gee.ArrayList<ThumbnailView>(); + 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<Event> events = new Gee.ArrayList<Event>(); + Gee.ArrayList<Event> unlinked = new Gee.ArrayList<Event>(); + + Gee.ArrayList<EventRow?> 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<MediaSource> media) { + Gee.ArrayList<ThumbnailView> views = new Gee.ArrayList<ThumbnailView>(); + 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<MediaSource> media) { + Gee.ArrayList<ThumbnailView> views = new Gee.ArrayList<ThumbnailView>(); + 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<MediaSource> views_to_media(Gee.Iterable<DataObject> views) { + Gee.ArrayList<MediaSource> media = new Gee.ArrayList<MediaSource>(); + foreach (DataObject object in views) + media.add((MediaSource) ((DataView) object).get_source()); + + return media; + } + + private void on_media_added(Gee.Iterable<DataObject> added) { + Gee.Collection<MediaSource> 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<DataObject> removed) { + Gee.ArrayList<MediaSource> 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<DataObject, Alteration> 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<DataSource> sources) { + unlinking = true; + + Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>(); + Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>(); + MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) sources, photos, videos); + + try { + MediaSource.set_many_to_event(photos, null, LibraryPhoto.global.transaction_controller); + } catch (Error err) { + AppWindow.error_message("%s".printf(err.message)); + } + + try { + MediaSource.set_many_to_event(videos, null, Video.global.transaction_controller); + } catch (Error err) { + AppWindow.error_message("%s".printf(err.message)); + } + + unlinking = false; + } + + public void establish_link(DataSource source) { + relinking = true; + + ((MediaSource) source).set_event(this); + + relinking = false; + } + + public void establish_link_many(Gee.Collection<DataSource> sources) { + relinking = true; + + Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>(); + Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>(); + MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) sources, photos, videos); + + try { + MediaSource.set_many_to_event(photos, this, LibraryPhoto.global.transaction_controller); + } catch (Error err) { + AppWindow.error_message("%s".printf(err.message)); + } + + try { + MediaSource.set_many_to_event(videos, this, Video.global.transaction_controller); + } catch (Error err) { + AppWindow.error_message("%s".printf(err.message)); + } + + relinking = false; + } + + private void update_indexable_keywords() { + string[] components = new string[3]; + int i = 0; + + string? rawname = get_raw_name(); + if (rawname != null) + components[i++] = rawname; + + string? comment = get_comment(); + if (comment != null) + components[i++] = comment; + + if (i == 0) + indexable_keywords = null; + else { + components[i] = null; + indexable_keywords = prepare_indexable_string(string.joinv(" ", components)); + } + } + + public unowned string? get_indexable_keywords() { + return indexable_keywords; + } + + public bool is_in_starting_day(time_t time) { + // it's possible the Event ref is held although it's been emptied + // (such as the user removing items during an import, when events + // are being generate on-the-fly) ... return false here and let + // the caller make a new one + if (view.get_count() == 0) + return false; + + // media sources are stored in ViewCollection from earliest to latest + MediaSource earliest_media = (MediaSource) ((DataView) view.get_at(0)).get_source(); + Time earliest_tm = Time.local(earliest_media.get_exposure_time()); + + // use earliest to generate the boundary hour for that day + Time start_boundary_tm = Time(); + start_boundary_tm.second = 0; + start_boundary_tm.minute = 0; + start_boundary_tm.hour = EVENT_BOUNDARY_HOUR; + start_boundary_tm.day = earliest_tm.day; + start_boundary_tm.month = earliest_tm.month; + start_boundary_tm.year = earliest_tm.year; + start_boundary_tm.isdst = -1; + + time_t start_boundary = start_boundary_tm.mktime(); + + // if the earliest's exposure time was on the day but *before* the boundary hour, + // step it back a day to the prior day's boundary + if (earliest_tm.hour < EVENT_BOUNDARY_HOUR) + start_boundary -= TIME_T_DAY; + + time_t end_boundary = (start_boundary + TIME_T_DAY - 1); + + return time >= start_boundary && time <= end_boundary; + } + + // This method attempts to add a media source to an event in the supplied list that it would + // naturally fit into (i.e. its exposure is within the boundary day of the earliest event + // photo). Otherwise, a new Event is generated and the source is added to it and the list. + private static Event? generate_event(MediaSource media, ViewCollection events_so_far, + string? event_name, out bool new_event) { + time_t exposure_time = media.get_exposure_time(); + + if (exposure_time == 0 && event_name == null) { + debug("Skipping event assignment to %s: no exposure time and no event name", media.to_string()); + new_event = false; + + return null; + } + + int count = events_so_far.get_count(); + for (int ctr = 0; ctr < count; ctr++) { + Event event = (Event) ((EventView) events_so_far.get_at(ctr)).get_source(); + + if ((event_name != null && event.has_name() && event_name == event.get_name()) + || event.is_in_starting_day(exposure_time)) { + new_event = false; + + return event; + } + } + + // no Event so far fits the bill for this photo or video, so create a new one + try { + Event event = new Event(EventTable.get_instance().create(media.get_source_id(), null)); + if (event_name != null) + event.rename(event_name); + + events_so_far.add(new EventView(event)); + + new_event = true; + return event; + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + new_event = false; + + return null; + } + + public static void generate_single_event(MediaSource media, ViewCollection events_so_far, + string? event_name = null) { + // do not replace existing assignments + if (media.get_event() != null) + return; + + bool new_event; + Event? event = generate_event(media, events_so_far, event_name, out new_event); + if (event == null) + return; + + media.set_event(event); + + if (new_event) + global.add(event); + } + + public static void generate_many_events(Gee.Collection<MediaSource> sources, ViewCollection events_so_far) { + Gee.Collection<Event> to_add = new Gee.ArrayList<Event>(); + foreach (MediaSource media in sources) { + // do not replace existing assignments + if (media.get_event() != null) + continue; + + bool new_event; + Event? event = generate_event(media, events_so_far, null, out new_event); + if (event == null) + continue; + + media.set_event(event); + + if (new_event) + to_add.add(event); + } + + if (to_add.size > 0) + global.add_many(to_add); + } + + public EventID get_event_id() { + return event_id; + } + + public override SourceSnapshot? save_snapshot() { + return new EventSnapshot(this); + } + + public SourceProxy get_proxy() { + return new EventProxy(this); + } + + public override bool equals(DataSource? source) { + // Validate primary key is unique, which is vital to all this working + Event? event = source as Event; + if (event != null) { + if (this != event) { + assert(event_id.id != event.event_id.id); + } + } + + return base.equals(source); + } + + public override string to_string() { + return "Event [%s/%s] %s".printf(event_id.id.to_string(), get_object_id().to_string(), get_name()); + } + + public bool has_name() { + return raw_name != null && raw_name.length > 0; + } + + public override string get_name() { + if (has_name()) + return get_raw_name(); + + // if no name, pretty up the start time + string? datestring = get_formatted_daterange(); + + return !is_string_empty(datestring) ? datestring : _("Event %s").printf(event_id.id.to_string()); + } + + public string? get_formatted_daterange() { + time_t start_time = get_start_time(); + time_t end_time = get_end_time(); + + if (end_time == 0 && start_time == 0) + return null; + + if (end_time == 0 && start_time != 0) + return format_local_date(Time.local(start_time)); + + Time start = Time.local(start_time); + Time end = Time.local(end_time); + + if (start.day == end.day && start.month == end.month && start.day == end.day) + return format_local_date(Time.local(start_time)); + + return format_local_datespan(start, end); + } + + public string? get_raw_name() { + return raw_name; + } + + public override string? get_comment() { + return comment; + } + + public bool rename(string? name) { + string? new_name = prep_event_name(name); + + // Allow rename to date but it should go dynamic, so set name to "" + if (new_name == get_formatted_daterange()) { + new_name = ""; + } + + bool renamed = event_table.rename(event_id, new_name); + if (renamed) { + raw_name = new_name; + update_indexable_keywords(); + notify_altered(new Alteration.from_list("metadata:name, indexable:keywords")); + } + + return renamed; + } + + public override bool set_comment(string? comment) { + string? new_comment = MediaSource.prep_comment(comment); + + bool committed = event_table.set_comment(event_id, new_comment); + if (committed) { + this.comment = new_comment; + update_indexable_keywords(); + notify_altered(new Alteration.from_list("metadata:comment, indexable:keywords")); + } + + return committed; + } + + public time_t get_creation_time() { + return event_table.get_time_created(event_id); + } + + public override time_t get_start_time() { + // Because the ViewCollection is sorted by a DateComparator, the start time is the + // first item. However, we keep looking if it has no start time. + int count = view.get_count(); + for (int i = 0; i < count; i++) { + time_t time = ((MediaSource) (((DataView) view.get_at(i)).get_source())).get_exposure_time(); + if (time != 0) + return time; + } + + return 0; + } + + public override time_t get_end_time() { + int count = view.get_count(); + + // Because the ViewCollection is sorted by a DateComparator, the end time is the + // last item--no matter what. + if (count == 0) + return 0; + + return ((MediaSource) (((DataView) view.get_at(count - 1)).get_source())).get_exposure_time(); + } + + public override uint64 get_total_filesize() { + uint64 total = 0; + foreach (MediaSource current_source in get_media()) { + total += current_source.get_filesize(); + } + + return total; + } + + public override int get_media_count() { + return view.get_count(); + } + + public override Gee.Collection<MediaSource> get_media() { + return (Gee.Collection<MediaSource>) view.get_sources(); + } + + public void mirror_photos(ViewCollection view, CreateView mirroring_ctor) { + view.mirror(this.view, mirroring_ctor, null); + } + + private void on_primary_thumbnail_altered() { + notify_thumbnail_altered(); + } + + public MediaSource get_primary_source() { + return primary_source; + } + + public bool set_primary_source(MediaSource source) { + assert(view.has_view_for_source(source)); + + bool committed = event_table.set_primary_source_id(event_id, source.get_source_id()); + if (committed) { + // switch to the new media source + if (primary_source != null) + primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered); + + primary_source = source; + primary_source.thumbnail_altered.connect(on_primary_thumbnail_altered); + + notify_thumbnail_altered(); + } + + return committed; + } + + private void release_primary_source() { + if (primary_source == null) + return; + + primary_source.thumbnail_altered.disconnect(on_primary_thumbnail_altered); + primary_source = null; + } + + public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error { + return primary_source != null ? primary_source.get_thumbnail(scale) : null; + } + + public Gdk.Pixbuf? get_preview_pixbuf(Scaling scaling) { + try { + return get_primary_source().get_preview_pixbuf(scaling); + } catch (Error err) { + return null; + } + } + + public override void destroy() { + // stop monitoring the photos collection + view.halt_all_monitoring(); + + // remove from the database + try { + event_table.remove(event_id); + } catch (DatabaseError err) { + AppWindow.database_error(err); + } + + // mark all photos and videos for this event as now event-less + PhotoTable.get_instance().drop_event(event_id); + VideoTable.get_instance().drop_event(event_id); + + base.destroy(); + } +} |