summaryrefslogtreecommitdiff
path: root/src/events
diff options
context:
space:
mode:
Diffstat (limited to 'src/events')
-rw-r--r--src/events/Branch.vala542
-rw-r--r--src/events/EventDirectoryItem.vala189
-rw-r--r--src/events/EventPage.vala162
-rw-r--r--src/events/Events.vala18
-rw-r--r--src/events/EventsDirectoryPage.vala313
-rw-r--r--src/events/mk/events.mk32
6 files changed, 1256 insertions, 0 deletions
diff --git a/src/events/Branch.vala b/src/events/Branch.vala
new file mode 100644
index 0000000..e1b5221
--- /dev/null
+++ b/src/events/Branch.vala
@@ -0,0 +1,542 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class Events.Branch : Sidebar.Branch {
+ internal static Icon open_icon;
+ internal static Icon closed_icon;
+ internal static Icon events_icon;
+ internal static Icon single_event_icon;
+ internal static Icon no_event_icon;
+
+ // NOTE: Because the comparators must be static methods (due to CompareFunc's stupid impl.)
+ // and there's an assumption that only one Events.Branch is ever created, this is a static
+ // member but it's modified by instance methods.
+ private static bool sort_ascending = false;
+
+ private Gee.HashMap<Event, Events.EventEntry> entry_map = new Gee.HashMap<
+ Event, Events.EventEntry>();
+ private Events.UndatedDirectoryEntry undated_entry = new Events.UndatedDirectoryEntry();
+ private Events.NoEventEntry no_event_entry = new Events.NoEventEntry();
+
+ public Branch() {
+ base (new Events.MasterDirectoryEntry(), Sidebar.Branch.Options.STARTUP_EXPAND_TO_FIRST_CHILD,
+ event_year_comparator);
+
+ // seed the branch
+ foreach (DataObject object in Event.global.get_all())
+ add_event((Event) object);
+
+ show_no_events(Event.global.get_no_event_objects().size > 0);
+
+ // monitor Events for future changes
+ Event.global.contents_altered.connect(on_events_added_removed);
+ Event.global.items_altered.connect(on_events_altered);
+ Event.global.no_event_collection_altered.connect(on_no_event_collection_altered);
+
+ // monitor sorting criteria (see note at sort_ascending about this)
+ Config.Facade.get_instance().events_sort_ascending_changed.connect(on_config_changed);
+ }
+
+ ~Branch() {
+ Event.global.contents_altered.disconnect(on_events_added_removed);
+ Event.global.items_altered.disconnect(on_events_altered);
+ Event.global.no_event_collection_altered.disconnect(on_no_event_collection_altered);
+
+ Config.Facade.get_instance().events_sort_ascending_changed.disconnect(on_config_changed);
+ }
+
+ internal static void init() {
+ open_icon = new ThemedIcon(Resources.ICON_FOLDER_OPEN);
+ closed_icon = new ThemedIcon(Resources.ICON_FOLDER_CLOSED);
+ events_icon = new ThemedIcon(Resources.ICON_EVENTS);
+ single_event_icon = new ThemedIcon(Resources.ICON_ONE_EVENT);
+ no_event_icon = new ThemedIcon(Resources.ICON_NO_EVENT);
+
+ sort_ascending = Config.Facade.get_instance().get_events_sort_ascending();
+ }
+
+ internal static void terminate() {
+ open_icon = null;
+ closed_icon = null;
+ events_icon = null;
+ single_event_icon = null;
+ no_event_icon = null;
+ }
+
+ public Events.MasterDirectoryEntry get_master_entry() {
+ return (Events.MasterDirectoryEntry) get_root();
+ }
+
+ private static int event_year_comparator(Sidebar.Entry a, Sidebar.Entry b) {
+ if (a == b)
+ return 0;
+
+ // The Undated and No Event entries should always appear last in the
+ // list, respectively.
+ if (a is Events.UndatedDirectoryEntry) {
+ if (b is Events.NoEventEntry)
+ return -1;
+ return 1;
+ } else if (b is Events.UndatedDirectoryEntry) {
+ if (a is Events.NoEventEntry)
+ return 1;
+ return -1;
+ }
+
+ if (a is Events.NoEventEntry)
+ return 1;
+ else if (b is Events.NoEventEntry)
+ return -1;
+
+ if (!sort_ascending) {
+ Sidebar.Entry swap = a;
+ a = b;
+ b = swap;
+ }
+
+ int result =
+ ((Events.YearDirectoryEntry) a).get_year() - ((Events.YearDirectoryEntry) b).get_year();
+ assert(result != 0);
+
+ return result;
+ }
+
+ private static int event_month_comparator(Sidebar.Entry a, Sidebar.Entry b) {
+ if (a == b)
+ return 0;
+
+ if (!sort_ascending) {
+ Sidebar.Entry swap = a;
+ a = b;
+ b = swap;
+ }
+
+ int result =
+ ((Events.MonthDirectoryEntry) a).get_month() - ((Events.MonthDirectoryEntry) b).get_month();
+ assert(result != 0);
+
+ return result;
+ }
+
+ private static int event_comparator(Sidebar.Entry a, Sidebar.Entry b) {
+ if (a == b)
+ return 0;
+
+ if (!sort_ascending) {
+ Sidebar.Entry swap = a;
+ a = b;
+ b = swap;
+ }
+
+ int64 result = ((Events.EventEntry) a).get_event().get_start_time()
+ - ((Events.EventEntry) b).get_event().get_start_time();
+
+ // to stabilize sort (events with the same start time are allowed)
+ if (result == 0) {
+ result = ((Events.EventEntry) a).get_event().get_event_id().id
+ - ((Events.EventEntry) b).get_event().get_event_id().id;
+ }
+
+ assert(result != 0);
+
+ return (result < 0) ? -1 : 1;
+ }
+
+ private static int undated_event_comparator(Sidebar.Entry a, Sidebar.Entry b) {
+ if (a == b)
+ return 0;
+
+ if (!sort_ascending) {
+ Sidebar.Entry swap = a;
+ a = b;
+ b = swap;
+ }
+
+ int ret = ((Events.EventEntry) a).get_event().get_name().collate(
+ ((Events.EventEntry) b).get_event().get_name());
+
+ if (ret == 0)
+ ret = (int) (((Events.EventEntry) b).get_event().get_instance_id() -
+ ((Events.EventEntry) a).get_event().get_instance_id());
+
+ return ret;
+ }
+
+ public Events.EventEntry? get_entry_for_event(Event event) {
+ return entry_map.get(event);
+ }
+
+ private void on_config_changed() {
+ bool value = Config.Facade.get_instance().get_events_sort_ascending();
+
+ sort_ascending = value;
+ reorder_all();
+ }
+
+ private void on_events_added_removed(Gee.Iterable<DataObject>? added,
+ Gee.Iterable<DataObject>? removed) {
+ if (added != null) {
+ foreach (DataObject object in added)
+ add_event((Event) object);
+ }
+
+ if (removed != null) {
+ foreach (DataObject object in removed)
+ remove_event((Event) object);
+ }
+ }
+
+ private void on_events_altered(Gee.Map<DataObject, Alteration> altered) {
+ foreach (DataObject object in altered.keys) {
+ Event event = (Event) object;
+ Alteration alteration = altered.get(object);
+
+ if (alteration.has_detail("metadata", "time")) {
+ // can't merely re-sort the event because it might have moved to a new month or
+ // even a new year
+ move_event(event);
+ } else if (alteration.has_detail("metadata", "name")) {
+ Events.EventEntry? entry = entry_map.get(event);
+ assert(entry != null);
+
+ entry.sidebar_name_changed(event.get_name());
+ entry.sidebar_tooltip_changed(event.get_name());
+ }
+ }
+ }
+
+ private void on_no_event_collection_altered() {
+ show_no_events(Event.global.get_no_event_objects().size > 0);
+ }
+
+ private void add_event(Event event) {
+ time_t event_time = event.get_start_time();
+ if (event_time == 0) {
+ add_undated_event(event);
+
+ return;
+ }
+
+ Time event_tm = Time.local(event_time);
+
+ Sidebar.Entry? year;
+ Sidebar.Entry? month = find_event_month(event, event_tm, out year);
+ if (month != null) {
+ graft_event(month, event, event_comparator);
+
+ return;
+ }
+
+ if (year == null) {
+ year = new Events.YearDirectoryEntry(event_tm.format(SubEventsDirectoryPage.YEAR_FORMAT),
+ event_tm);
+ graft(get_root(), year, event_month_comparator);
+ }
+
+ month = new Events.MonthDirectoryEntry(event_tm.format(SubEventsDirectoryPage.MONTH_FORMAT),
+ event_tm);
+ graft(year, month, event_comparator);
+
+ graft_event(month, event, event_comparator);
+ }
+
+ private void move_event(Event event) {
+ time_t event_time = event.get_start_time();
+ if (event_time == 0) {
+ move_to_undated_event(event);
+
+ return;
+ }
+
+ Time event_tm = Time.local(event_time);
+
+ Sidebar.Entry? year;
+ Sidebar.Entry? month = find_event_month(event, event_tm, out year);
+
+ if (year == null) {
+ year = new Events.YearDirectoryEntry(event_tm.format(SubEventsDirectoryPage.YEAR_FORMAT),
+ event_tm);
+ graft(get_root(), year, event_month_comparator);
+ }
+
+ if (month == null) {
+ month = new Events.MonthDirectoryEntry(event_tm.format(SubEventsDirectoryPage.MONTH_FORMAT),
+ event_tm);
+ graft(year, month, event_comparator);
+ }
+
+ reparent_event(event, month);
+ }
+
+ private void remove_event(Event event) {
+ // the following code works for undated events as well as dated (no need for special
+ // case, as in add_event())
+ Sidebar.Entry? entry;
+ bool removed = entry_map.unset(event, out entry);
+ assert(removed);
+
+ Sidebar.Entry? parent = get_parent(entry);
+ assert(parent != null);
+
+ prune(entry);
+
+ // prune up the tree to the root
+ while (get_child_count(parent) == 0 && parent != get_root()) {
+ Sidebar.Entry? grandparent = get_parent(parent);
+ assert(grandparent != null);
+
+ prune(parent);
+
+ parent = grandparent;
+ }
+ }
+
+ private Sidebar.Entry? find_event_month(Event event, Time event_tm, out Sidebar.Entry found_year) {
+ // find the year first
+ found_year = find_event_year(event, event_tm);
+ if (found_year == null)
+ return null;
+
+ int event_month = event_tm.month + 1;
+
+ // found the year, traverse the months
+ return find_first_child(found_year, (entry) => {
+ return ((Events.MonthDirectoryEntry) entry).get_month() == event_month;
+ });
+ }
+
+ private Sidebar.Entry? find_event_year(Event event, Time event_tm) {
+ int event_year = event_tm.year + 1900;
+
+ return find_first_child(get_root(), (entry) => {
+ if ((entry is Events.UndatedDirectoryEntry) || (entry is Events.NoEventEntry))
+ return false;
+ else
+ return ((Events.YearDirectoryEntry) entry).get_year() == event_year;
+ });
+ }
+
+ private void add_undated_event(Event event) {
+ if (!has_entry(undated_entry))
+ graft(get_root(), undated_entry, undated_event_comparator);
+
+ graft_event(undated_entry, event);
+ }
+
+ private void move_to_undated_event(Event event) {
+ if (!has_entry(undated_entry))
+ graft(get_root(), undated_entry);
+
+ reparent_event(event, undated_entry);
+ }
+
+ private void graft_event(Sidebar.Entry parent, Event event,
+ owned CompareDataFunc<Sidebar.Entry>? comparator = null) {
+ Events.EventEntry entry = new Events.EventEntry(event);
+ entry_map.set(event, entry);
+
+ graft(parent, entry, (owned) comparator);
+ }
+
+ private void reparent_event(Event event, Sidebar.Entry new_parent) {
+ Events.EventEntry? entry = entry_map.get(event);
+ assert(entry != null);
+
+ Sidebar.Entry? old_parent = get_parent(entry);
+ assert(old_parent != null);
+
+ reparent(new_parent, entry);
+
+ while (get_child_count(old_parent) == 0 && old_parent != get_root()) {
+ Sidebar.Entry? grandparent = get_parent(old_parent);
+ assert(grandparent != null);
+
+ prune(old_parent);
+
+ old_parent = grandparent;
+ }
+ }
+
+ private void show_no_events(bool show) {
+ if (show && !has_entry(no_event_entry))
+ graft(get_root(), no_event_entry);
+ else if (!show && has_entry(no_event_entry))
+ prune(no_event_entry);
+ }
+}
+
+public abstract class Events.DirectoryEntry : Sidebar.SimplePageEntry, Sidebar.ExpandableEntry {
+ public DirectoryEntry() {
+ }
+
+ public override Icon? get_sidebar_icon() {
+ return null;
+ }
+
+ public virtual Icon? get_sidebar_open_icon() {
+ return Events.Branch.open_icon;
+ }
+
+ public virtual Icon? get_sidebar_closed_icon() {
+ return Events.Branch.closed_icon;
+ }
+
+ public bool expand_on_select() {
+ return true;
+ }
+}
+
+public class Events.MasterDirectoryEntry : Events.DirectoryEntry {
+ public MasterDirectoryEntry() {
+ }
+
+ public override string get_sidebar_name() {
+ return MasterEventsDirectoryPage.NAME;
+ }
+
+ public override Icon? get_sidebar_icon() {
+ return Events.Branch.events_icon;
+ }
+
+ public override Icon? get_sidebar_open_icon() {
+ return Events.Branch.events_icon;
+ }
+
+ public override Icon? get_sidebar_closed_icon() {
+ return Events.Branch.events_icon;
+ }
+
+ protected override Page create_page() {
+ return new MasterEventsDirectoryPage();
+ }
+}
+
+public class Events.YearDirectoryEntry : Events.DirectoryEntry {
+ private string name;
+ private Time tm;
+
+ public YearDirectoryEntry(string name, Time tm) {
+ this.name = name;
+ this.tm = tm;
+ }
+
+ public override string get_sidebar_name() {
+ return name;
+ }
+
+ public int get_year() {
+ return tm.year + 1900;
+ }
+
+ protected override Page create_page() {
+ return new SubEventsDirectoryPage(SubEventsDirectoryPage.DirectoryType.YEAR, tm);
+ }
+}
+
+public class Events.MonthDirectoryEntry : Events.DirectoryEntry {
+ private string name;
+ private Time tm;
+
+ public MonthDirectoryEntry(string name, Time tm) {
+ this.name = name;
+ this.tm = tm;
+ }
+
+ public override string get_sidebar_name() {
+ return name;
+ }
+
+ public int get_year() {
+ return tm.year + 1900;
+ }
+
+ public int get_month() {
+ return tm.month + 1;
+ }
+
+ protected override Page create_page() {
+ return new SubEventsDirectoryPage(SubEventsDirectoryPage.DirectoryType.MONTH, tm);
+ }
+}
+
+public class Events.UndatedDirectoryEntry : Events.DirectoryEntry {
+ public UndatedDirectoryEntry() {
+ }
+
+ public override string get_sidebar_name() {
+ return SubEventsDirectoryPage.UNDATED_PAGE_NAME;
+ }
+
+ protected override Page create_page() {
+ return new SubEventsDirectoryPage(SubEventsDirectoryPage.DirectoryType.UNDATED,
+ Time.local(0));
+ }
+}
+
+public class Events.EventEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntry,
+ Sidebar.InternalDropTargetEntry {
+ private Event event;
+
+ public EventEntry(Event event) {
+ this.event = event;
+ }
+
+ public Event get_event() {
+ return event;
+ }
+
+ public override string get_sidebar_name() {
+ return event.get_name();
+ }
+
+ public override Icon? get_sidebar_icon() {
+ return Events.Branch.single_event_icon;
+ }
+
+ protected override Page create_page() {
+ return new EventPage(event);
+ }
+
+ public void rename(string new_name) {
+ string? prepped = Event.prep_event_name(new_name);
+ if (prepped != null)
+ AppWindow.get_command_manager().execute(new RenameEventCommand(event, prepped));
+ }
+
+ public bool internal_drop_received(Gee.List<MediaSource> media) {
+ // ugh ... some early Commands expected DataViews instead of DataSources (to make life
+ // easier for Pages) and this is one of the prices paid for that
+ Gee.ArrayList<DataView> views = new Gee.ArrayList<DataView>();
+ foreach (MediaSource media_source in media)
+ views.add(new DataView(media_source));
+
+ AppWindow.get_command_manager().execute(new SetEventCommand(views, event));
+
+ return true;
+ }
+
+ public bool internal_drop_received_arbitrary(Gtk.SelectionData data) {
+ return false;
+ }
+}
+
+public class Events.NoEventEntry : Sidebar.SimplePageEntry {
+ public NoEventEntry() {
+ }
+
+ public override string get_sidebar_name() {
+ return NoEventPage.NAME;
+ }
+
+ public override Icon? get_sidebar_icon() {
+ return Events.Branch.no_event_icon;
+ }
+
+ protected override Page create_page() {
+ return new NoEventPage();
+ }
+}
+
diff --git a/src/events/EventDirectoryItem.vala b/src/events/EventDirectoryItem.vala
new file mode 100644
index 0000000..5b2026b
--- /dev/null
+++ b/src/events/EventDirectoryItem.vala
@@ -0,0 +1,189 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+class EventDirectoryItem : CheckerboardItem {
+ private static int CROPPED_SCALE {
+ get {
+ return ThumbnailCache.Size.MEDIUM.get_scale()
+ + ((ThumbnailCache.Size.BIG.get_scale() - ThumbnailCache.Size.MEDIUM.get_scale()) / 2);
+ }
+ }
+
+ public static Scaling squared_scaling = Scaling.to_fill_viewport(Dimensions(CROPPED_SCALE,
+ CROPPED_SCALE));
+
+ public Event event;
+
+ private Gdk.Rectangle paul_lynde = Gdk.Rectangle();
+
+ public EventDirectoryItem(Event event) {
+ base (event, Dimensions(CROPPED_SCALE, CROPPED_SCALE), get_formatted_title(event), event.get_comment(), true,
+ Pango.Alignment.CENTER);
+
+ this.event = event;
+
+ // find the center square
+ paul_lynde = get_paul_lynde_rect(event.get_primary_source());
+
+ // don't display yet, but claim its dimensions
+ clear_image(Dimensions.for_rectangle(paul_lynde));
+
+ // monitor the event for changes
+ Event.global.items_altered.connect(on_events_altered);
+ }
+
+ ~EventDirectoryItem() {
+ Event.global.items_altered.disconnect(on_events_altered);
+ }
+
+ // square the photo's dimensions and locate the pixbuf's center square
+ private static Gdk.Rectangle get_paul_lynde_rect(MediaSource source) {
+ Dimensions scaled = squared_scaling.get_scaled_dimensions(source.get_dimensions());
+
+ Gdk.Rectangle paul_lynde = Gdk.Rectangle();
+ paul_lynde.x = (scaled.width - CROPPED_SCALE).clamp(0, scaled.width) / 2;
+ paul_lynde.y = (scaled.height - CROPPED_SCALE).clamp(0, scaled.height) / 2;
+ paul_lynde.width = CROPPED_SCALE;
+ paul_lynde.height = CROPPED_SCALE;
+
+ return paul_lynde;
+ }
+
+ // scale and crop the center square of the media
+ private static Gdk.Pixbuf get_paul_lynde(MediaSource media, Gdk.Rectangle paul_lynde) throws Error {
+ Gdk.Pixbuf pixbuf = media.get_preview_pixbuf(squared_scaling);
+
+ Dimensions thumbnail_dimensions = Dimensions.for_pixbuf(pixbuf);
+
+ if (thumbnail_dimensions.width > 2 * paul_lynde.width ||
+ thumbnail_dimensions.height > paul_lynde.height * 2 ) {
+ LibraryPhoto photo = (LibraryPhoto) media;
+ pixbuf = photo.get_pixbuf(squared_scaling);
+ thumbnail_dimensions = Dimensions.for_pixbuf(pixbuf);
+ }
+
+ // to catch rounding errors in the two algorithms
+ paul_lynde = clamp_rectangle(paul_lynde, thumbnail_dimensions);
+
+ // crop the center square
+ return new Gdk.Pixbuf.subpixbuf(pixbuf, paul_lynde.x, paul_lynde.y, paul_lynde.width,
+ paul_lynde.height);
+ }
+
+ private static string get_formatted_title(Event event) {
+ bool has_photos = MediaSourceCollection.has_photo(event.get_media());
+ bool has_videos = MediaSourceCollection.has_video(event.get_media());
+
+ int count = event.get_media_count();
+ string count_text = "";
+ if (has_photos && has_videos)
+ count_text = ngettext("%d Photo/Video", "%d Photos/Videos", count).printf(count);
+ else if (has_videos)
+ count_text = ngettext("%d Video", "%d Videos", count).printf(count);
+ else
+ count_text = ngettext("%d Photo", "%d Photos", count).printf(count);
+
+ string? daterange = event.get_formatted_daterange();
+ string name = event.get_name();
+
+ // if we don't have a daterange or if it's the same as name, then don't print it; otherwise
+ // print it beneath the preview photo
+ if (daterange == null || daterange == name)
+ return "<b>%s</b>\n%s".printf(guarded_markup_escape_text(name),
+ guarded_markup_escape_text(count_text));
+ else
+ return "<b>%s</b>\n%s\n%s".printf(guarded_markup_escape_text(name),
+ guarded_markup_escape_text(count_text), guarded_markup_escape_text(daterange));
+ }
+
+ public override void exposed() {
+ if (is_exposed())
+ return;
+
+ try {
+ set_image(get_paul_lynde(event.get_primary_source(), paul_lynde));
+ } catch (Error err) {
+ critical("Unable to fetch preview for %s: %s", event.to_string(), err.message);
+ }
+
+ update_comment();
+
+ base.exposed();
+ }
+
+ public override void unexposed() {
+ if (!is_exposed())
+ return;
+
+ clear_image(Dimensions.for_rectangle(paul_lynde));
+
+ base.unexposed();
+ }
+
+ private void on_events_altered(Gee.Map<DataObject, Alteration> map) {
+ update_comment();
+ if (map.has_key(event))
+ set_title(get_formatted_title(event), true, Pango.Alignment.CENTER);
+ }
+
+ protected override void thumbnail_altered() {
+ MediaSource media = event.get_primary_source();
+
+ // get new center square
+ paul_lynde = get_paul_lynde_rect(media);
+
+ if (is_exposed()) {
+ try {
+ set_image(get_paul_lynde(media, paul_lynde));
+ } catch (Error err) {
+ critical("Unable to fetch preview for %s: %s", event.to_string(), err.message);
+ }
+ } else {
+ clear_image(Dimensions.for_rectangle(paul_lynde));
+ }
+
+ base.thumbnail_altered();
+ }
+
+ protected override void paint_shadow(Cairo.Context ctx, Dimensions dimensions, Gdk.Point origin,
+ int radius, float initial_alpha) {
+ Dimensions altered = Dimensions(dimensions.width - 25, dimensions.height - 25);
+ base.paint_shadow(ctx, altered, origin, 36, initial_alpha);
+ }
+
+ protected override void paint_border(Cairo.Context ctx, Dimensions object_dimensions,
+ Gdk.Point object_origin, int border_width) {
+ Dimensions dimensions = get_border_dimensions(object_dimensions, border_width);
+ Gdk.Point origin = get_border_origin(object_origin, border_width);
+
+ draw_rounded_corners_filled(ctx, dimensions, origin, 6.0);
+ }
+
+ protected override void paint_image(Cairo.Context ctx, Gdk.Pixbuf pixbuf,
+ Gdk.Point origin) {
+ Dimensions dimensions = Dimensions.for_pixbuf(pixbuf);
+
+ if (pixbuf.get_has_alpha())
+ draw_rounded_corners_filled(ctx, dimensions, origin, 6.0);
+
+ // use rounded corners on events
+ context_rounded_corners(ctx, dimensions, origin, 6.0);
+ Gdk.cairo_set_source_pixbuf(ctx, pixbuf, origin.x, origin.y);
+ ctx.paint();
+ }
+
+ private void update_comment(bool init = false) {
+ string comment = event.get_comment();
+ if (is_string_empty(comment))
+ clear_comment();
+ else if (!init)
+ set_comment(comment);
+ else
+ set_comment("");
+ }
+}
+
+
diff --git a/src/events/EventPage.vala b/src/events/EventPage.vala
new file mode 100644
index 0000000..3d23f25
--- /dev/null
+++ b/src/events/EventPage.vala
@@ -0,0 +1,162 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class EventPage : CollectionPage {
+ private Event page_event;
+
+ public EventPage(Event page_event) {
+ base (page_event.get_name());
+
+ this.page_event = page_event;
+ page_event.mirror_photos(get_view(), create_thumbnail);
+
+ init_page_context_menu("/EventContextMenu");
+
+ Event.global.items_altered.connect(on_events_altered);
+ }
+
+ public Event get_event() {
+ return page_event;
+ }
+
+ protected override bool on_app_key_pressed(Gdk.EventKey event) {
+ // If and only if one image is selected, propagate F2 to the rest of
+ // the window, otherwise, consume it here - if we don't do this, it'll
+ // either let us re-title multiple images at the same time or
+ // spuriously highlight the event name in the sidebar for editing...
+ if (Gdk.keyval_name(event.keyval) == "F2") {
+ if (get_view().get_selected_count() != 1) {
+ return true;
+ }
+ }
+
+ return base.on_app_key_pressed(event);
+ }
+
+ ~EventPage() {
+ Event.global.items_altered.disconnect(on_events_altered);
+ get_view().halt_mirroring();
+ }
+
+ protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
+ base.init_collect_ui_filenames(ui_filenames);
+
+ ui_filenames.add("event.ui");
+ }
+
+ protected override Gtk.ActionEntry[] init_collect_action_entries() {
+ Gtk.ActionEntry[] new_actions = base.init_collect_action_entries();
+
+ Gtk.ActionEntry make_primary = { "MakePrimary", Resources.MAKE_PRIMARY,
+ TRANSLATABLE, null, TRANSLATABLE, on_make_primary };
+ make_primary.label = Resources.MAKE_KEY_PHOTO_MENU;
+ new_actions += make_primary;
+
+ Gtk.ActionEntry rename = { "Rename", null, TRANSLATABLE, null, TRANSLATABLE, on_rename };
+ rename.label = Resources.RENAME_EVENT_MENU;
+ new_actions += rename;
+
+ Gtk.ActionEntry comment = { "EditEventComment", null, TRANSLATABLE, null,
+ Resources.EDIT_EVENT_COMMENT_MENU, on_edit_comment};
+ comment.label = Resources.EDIT_EVENT_COMMENT_MENU;
+ new_actions += comment;
+
+ return new_actions;
+ }
+
+ protected override void init_actions(int selected_count, int count) {
+ base.init_actions(selected_count, count);
+ }
+
+ protected override void update_actions(int selected_count, int count) {
+ set_action_sensitive("MakePrimary", selected_count == 1);
+
+ // hide this command in CollectionPage, as it does not apply here
+ set_action_visible("CommonJumpToEvent", false);
+
+ base.update_actions(selected_count, count);
+
+ // this is always valid; if the user has right-clicked in an empty area,
+ // change the comment on the event itself.
+ set_action_sensitive("EditEventComment", true);
+ }
+
+ protected override void get_config_photos_sort(out bool sort_order, out int sort_by) {
+ Config.Facade.get_instance().get_event_photos_sort(out sort_order, out sort_by);
+ }
+
+ protected override void set_config_photos_sort(bool sort_order, int sort_by) {
+ Config.Facade.get_instance().set_event_photos_sort(sort_order, sort_by);
+ }
+
+ private void on_events_altered(Gee.Map<DataObject, Alteration> map) {
+ if (map.has_key(page_event))
+ set_page_name(page_event.get_name());
+ }
+
+ protected override void on_edit_comment() {
+ if (get_view().get_selected_count() == 0) {
+ EditCommentDialog edit_comment_dialog = new EditCommentDialog(page_event.get_comment(),
+ true);
+ string? new_comment = edit_comment_dialog.execute();
+ if (new_comment == null)
+ return;
+
+ EditEventCommentCommand command = new EditEventCommentCommand(page_event, new_comment);
+ get_command_manager().execute(command);
+ return;
+ }
+
+ base.on_edit_comment();
+ }
+
+ private void on_make_primary() {
+ if (get_view().get_selected_count() != 1)
+ return;
+
+ page_event.set_primary_source((MediaSource) get_view().get_selected_at(0).get_source());
+ }
+
+ private void on_rename() {
+ LibraryWindow.get_app().rename_event_in_sidebar(page_event);
+ }
+}
+
+public class NoEventPage : CollectionPage {
+ public const string NAME = _("No Event");
+
+ // This seems very similar to EventSourceCollection -> ViewManager
+ private class NoEventViewManager : CollectionViewManager {
+ public NoEventViewManager(NoEventPage page) {
+ base (page);
+ }
+
+ // this is not threadsafe
+ public override bool include_in_view(DataSource source) {
+ return (((MediaSource) source).get_event_id().id != EventID.INVALID) ? false :
+ base.include_in_view(source);
+ }
+ }
+
+ private static Alteration no_event_page_alteration = new Alteration("metadata", "event");
+
+ public NoEventPage() {
+ base (NAME);
+
+ ViewManager filter = new NoEventViewManager(this);
+ get_view().monitor_source_collection(LibraryPhoto.global, filter, no_event_page_alteration);
+ get_view().monitor_source_collection(Video.global, filter, no_event_page_alteration);
+ }
+
+ protected override void get_config_photos_sort(out bool sort_order, out int sort_by) {
+ Config.Facade.get_instance().get_event_photos_sort(out sort_order, out sort_by);
+ }
+
+ protected override void set_config_photos_sort(bool sort_order, int sort_by) {
+ Config.Facade.get_instance().set_event_photos_sort(sort_order, sort_by);
+ }
+}
+
diff --git a/src/events/Events.vala b/src/events/Events.vala
new file mode 100644
index 0000000..dc2b662
--- /dev/null
+++ b/src/events/Events.vala
@@ -0,0 +1,18 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace Events {
+
+public void init() throws Error {
+ Events.Branch.init();
+}
+
+public void terminate() {
+ Events.Branch.terminate();
+}
+
+}
+
diff --git a/src/events/EventsDirectoryPage.vala b/src/events/EventsDirectoryPage.vala
new file mode 100644
index 0000000..41a1ac6
--- /dev/null
+++ b/src/events/EventsDirectoryPage.vala
@@ -0,0 +1,313 @@
+/* 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 abstract class EventsDirectoryPage : CheckerboardPage {
+ public class EventDirectoryManager : ViewManager {
+ public override DataView create_view(DataSource source) {
+ return new EventDirectoryItem((Event) source);
+ }
+ }
+
+ private class EventsDirectorySearchViewFilter : SearchViewFilter {
+ public override uint get_criteria() {
+ return SearchFilterCriteria.TEXT;
+ }
+
+ public override bool predicate(DataView view) {
+ assert(view.get_source() is Event);
+ if (is_string_empty(get_search_filter()))
+ return true;
+
+ Event source = (Event) view.get_source();
+ unowned string? event_keywords = source.get_indexable_keywords();
+ if (is_string_empty(event_keywords))
+ return false;
+
+ // Return false if the word isn't found, true otherwise.
+ foreach (unowned string word in get_search_filter_words()) {
+ if (!event_keywords.contains(word))
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ private const int MIN_PHOTOS_FOR_PROGRESS_WINDOW = 50;
+
+ protected ViewManager view_manager;
+
+ private EventsDirectorySearchViewFilter search_filter = new EventsDirectorySearchViewFilter();
+
+ public EventsDirectoryPage(string page_name, ViewManager view_manager,
+ Gee.Collection<Event>? initial_events) {
+ base (page_name);
+
+ // set comparator before monitoring source collection, to prevent a re-sort
+ get_view().set_comparator(get_event_comparator(Config.Facade.get_instance().get_events_sort_ascending()),
+ event_comparator_predicate);
+ get_view().monitor_source_collection(Event.global, view_manager, null, initial_events);
+
+ get_view().set_property(Event.PROP_SHOW_COMMENTS,
+ Config.Facade.get_instance().get_display_event_comments());
+
+ init_item_context_menu("/EventsDirectoryContextMenu");
+
+ this.view_manager = view_manager;
+
+ // set up page's toolbar (used by AppWindow for layout and FullscreenWindow as a popup)
+ Gtk.Toolbar toolbar = get_toolbar();
+
+ // merge tool
+ Gtk.ToolButton merge_button = new Gtk.ToolButton.from_stock(Resources.MERGE);
+ merge_button.set_related_action(get_action("Merge"));
+
+ toolbar.insert(merge_button, -1);
+ }
+
+ ~EventsDirectoryPage() {
+ Gtk.RadioAction? action = get_action("CommonSortEventsAscending") as Gtk.RadioAction;
+ assert(action != null);
+ action.changed.disconnect(on_sort_changed);
+ }
+
+ protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
+ ui_filenames.add("events_directory.ui");
+
+ base.init_collect_ui_filenames(ui_filenames);
+ }
+
+ protected static bool event_comparator_predicate(DataObject object, Alteration alteration) {
+ return alteration.has_detail("metadata", "time");
+ }
+
+ private static int64 event_ascending_comparator(void *a, void *b) {
+ time_t start_a = ((EventDirectoryItem *) a)->event.get_start_time();
+ time_t start_b = ((EventDirectoryItem *) b)->event.get_start_time();
+
+ return start_a - start_b;
+ }
+
+ private static int64 event_descending_comparator(void *a, void *b) {
+ return event_ascending_comparator(b, a);
+ }
+
+ private static Comparator get_event_comparator(bool ascending) {
+ if (ascending)
+ return event_ascending_comparator;
+ else
+ return event_descending_comparator;
+ }
+
+ protected override Gtk.ActionEntry[] init_collect_action_entries() {
+ Gtk.ActionEntry[] actions = base.init_collect_action_entries();
+
+ Gtk.ActionEntry rename = { "Rename", null, TRANSLATABLE, "F2", TRANSLATABLE, on_rename };
+ rename.label = Resources.RENAME_EVENT_MENU;
+ actions += rename;
+
+ Gtk.ActionEntry merge = { "Merge", Resources.MERGE, TRANSLATABLE, null, Resources.MERGE_TOOLTIP,
+ on_merge };
+ merge.label = Resources.MERGE_MENU;
+ actions += merge;
+
+ Gtk.ActionEntry comment = { "EditComment", null, TRANSLATABLE, null, Resources.EDIT_COMMENT_MENU,
+ on_edit_comment };
+ comment.label = Resources.EDIT_COMMENT_MENU;
+ actions += comment;
+
+ return actions;
+ }
+
+ protected override Gtk.ToggleActionEntry[] init_collect_toggle_action_entries() {
+ Gtk.ToggleActionEntry[] toggle_actions = base.init_collect_toggle_action_entries();
+
+ Gtk.ToggleActionEntry comments = { "ViewComment", null, TRANSLATABLE, "<Ctrl><Shift>C",
+ TRANSLATABLE, on_display_comments, Config.Facade.get_instance().get_display_event_comments() };
+ comments.label = _("_Comments");
+ comments.tooltip = _("Display the comment of each event");
+ toggle_actions += comments;
+
+ return toggle_actions;
+ }
+
+ protected override void init_actions(int selected_count, int count) {
+ base.init_actions(selected_count, count);
+
+ Gtk.RadioAction? action = get_action("CommonSortEventsAscending") as Gtk.RadioAction;
+ assert(action != null);
+ action.changed.connect(on_sort_changed);
+ }
+
+ protected override void update_actions(int selected_count, int count) {
+ set_action_sensitive("Merge", selected_count > 1);
+ set_action_important("Merge", true);
+ set_action_sensitive("Rename", selected_count == 1);
+ set_action_sensitive("EditComment", selected_count == 1);
+
+ base.update_actions(selected_count, count);
+ }
+
+ protected override string get_view_empty_message() {
+ return _("No events");
+ }
+
+ protected override string get_filter_no_match_message() {
+ return _("No events found");
+ }
+
+ public override void on_item_activated(CheckerboardItem item, CheckerboardPage.Activator
+ activator, CheckerboardPage.KeyboardModifiers modifiers) {
+ EventDirectoryItem event = (EventDirectoryItem) item;
+ LibraryWindow.get_app().switch_to_event(event.event);
+ }
+
+ private void on_sort_changed(Gtk.Action action, Gtk.Action c) {
+ Gtk.RadioAction current = (Gtk.RadioAction) c;
+
+ get_view().set_comparator(
+ get_event_comparator(current.current_value == LibraryWindow.SORT_EVENTS_ORDER_ASCENDING),
+ event_comparator_predicate);
+ }
+
+ private void on_rename() {
+ // only rename one at a time
+ if (get_view().get_selected_count() != 1)
+ return;
+
+ EventDirectoryItem item = (EventDirectoryItem) get_view().get_selected_at(0);
+
+ EventRenameDialog rename_dialog = new EventRenameDialog(item.event.get_raw_name());
+ string? new_name = rename_dialog.execute();
+ if (new_name == null)
+ return;
+
+ RenameEventCommand command = new RenameEventCommand(item.event, new_name);
+ get_command_manager().execute(command);
+ }
+
+ protected void on_edit_comment() {
+ // only edit one at a time
+ if (get_view().get_selected_count() != 1)
+ return;
+
+ EventDirectoryItem item = (EventDirectoryItem) get_view().get_selected_at(0);
+
+ EditCommentDialog edit_comment_dialog = new EditCommentDialog(item.event.get_comment());
+ string? new_comment = edit_comment_dialog.execute();
+ if (new_comment == null)
+ return;
+
+ EditEventCommentCommand command = new EditEventCommentCommand(item.event, new_comment);
+ get_command_manager().execute(command);
+ }
+
+ private void on_merge() {
+ if (get_view().get_selected_count() <= 1)
+ return;
+
+ MergeEventsCommand command = new MergeEventsCommand(get_view().get_selected());
+ get_command_manager().execute(command);
+ }
+
+ private void on_display_comments(Gtk.Action action) {
+ bool display = ((Gtk.ToggleAction) action).get_active();
+
+ set_display_comments(display);
+
+ Config.Facade.get_instance().set_display_event_comments(display);
+ }
+
+ public override SearchViewFilter get_search_view_filter() {
+ return search_filter;
+ }
+}
+
+public class MasterEventsDirectoryPage : EventsDirectoryPage {
+ public const string NAME = _("Events");
+
+ public MasterEventsDirectoryPage() {
+ base (NAME, new EventDirectoryManager(), (Gee.Collection<Event>) Event.global.get_all());
+ }
+}
+
+public class SubEventsDirectoryPage : EventsDirectoryPage {
+ public enum DirectoryType {
+ YEAR,
+ MONTH,
+ UNDATED;
+ }
+
+ public const string UNDATED_PAGE_NAME = _("Undated");
+ public const string YEAR_FORMAT = _("%Y");
+ public const string MONTH_FORMAT = _("%B");
+
+ private class SubEventDirectoryManager : EventsDirectoryPage.EventDirectoryManager {
+ private int month = 0;
+ private int year = 0;
+ DirectoryType type;
+
+ public SubEventDirectoryManager(DirectoryType type, Time time) {
+ base();
+
+ if (type == DirectoryType.MONTH)
+ month = time.month;
+ this.type = type;
+ year = time.year;
+ }
+
+ public override bool include_in_view(DataSource source) {
+ if (!base.include_in_view(source))
+ return false;
+
+ EventSource event = (EventSource) source;
+ Time event_time = Time.local(event.get_start_time());
+ if (event_time.year == year) {
+ if (type == DirectoryType.MONTH) {
+ return (event_time.month == month);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public int get_month() {
+ return month;
+ }
+
+ public int get_year() {
+ return year;
+ }
+
+ public DirectoryType get_event_directory_type() {
+ return type;
+ }
+ }
+
+ public SubEventsDirectoryPage(DirectoryType type, Time time) {
+ string page_name;
+ if (type == SubEventsDirectoryPage.DirectoryType.UNDATED) {
+ page_name = UNDATED_PAGE_NAME;
+ } else {
+ page_name = time.format((type == DirectoryType.YEAR) ? YEAR_FORMAT : MONTH_FORMAT);
+ }
+
+ base(page_name, new SubEventDirectoryManager(type, time), null);
+ }
+
+ public int get_month() {
+ return ((SubEventDirectoryManager) view_manager).get_month();
+ }
+
+ public int get_year() {
+ return ((SubEventDirectoryManager) view_manager).get_year();
+ }
+
+ public DirectoryType get_event_directory_type() {
+ return ((SubEventDirectoryManager) view_manager).get_event_directory_type();
+ }
+}
+
diff --git a/src/events/mk/events.mk b/src/events/mk/events.mk
new file mode 100644
index 0000000..d09fc0f
--- /dev/null
+++ b/src/events/mk/events.mk
@@ -0,0 +1,32 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Events
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := events
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ Branch.vala \
+ EventsDirectoryPage.vala \
+ EventPage.vala \
+ EventDirectoryItem.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES := \
+ Sidebar
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+