diff options
Diffstat (limited to 'src/events')
-rw-r--r-- | src/events/Branch.vala | 542 | ||||
-rw-r--r-- | src/events/EventDirectoryItem.vala | 189 | ||||
-rw-r--r-- | src/events/EventPage.vala | 162 | ||||
-rw-r--r-- | src/events/Events.vala | 18 | ||||
-rw-r--r-- | src/events/EventsDirectoryPage.vala | 313 | ||||
-rw-r--r-- | src/events/mk/events.mk | 32 |
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 + |