Commit 096727a8 authored by Allison Barlow's avatar Allison Barlow

added display basic photo info feature for single selection

parent df5c5c2e
......@@ -48,7 +48,8 @@ SRC_FILES = \
Queryable.vala \
LibraryWindow.vala \
CameraTable.vala \
DirectWindow.vala
DirectWindow.vala \
Properties.vala
VAPI_FILES = \
libexif.vapi \
......
......@@ -163,6 +163,14 @@ class SlideshowPage : SinglePhotoPage {
return (base.key_press_event != null) ? base.key_press_event(event) : true;
}
public override int get_queryable_count() {
return 1;
}
public override int get_selected_queryable_count() {
return get_queryable_count();
}
public override Gee.Iterable<Queryable>? get_queryables() {
Gee.ArrayList<LibraryPhoto> photo_array_list = new Gee.ArrayList<LibraryPhoto>();
photo_array_list.add(thumbnail.get_photo());
......@@ -593,6 +601,9 @@ public class CollectionPage : CheckerboardPage {
}
private void on_thumbnail_altered(LibraryPhoto photo) {
// TODO: use a different signal: e.g. contents_changed or photo_altered
notify_selection_changed(get_selected_queryable_count());
// the thumbnail is only going to reload a low-quality interp, so schedule improval
schedule_thumbnail_improval();
......
......@@ -29,12 +29,12 @@ public class DirectoryItem : LayoutItem, EventSource {
}
public time_t get_end_time() {
return (new EventTable()).get_start_time(event_id);
return (new EventTable()).get_end_time(event_id);
}
public Gee.Iterable<PhotoSource> get_photos() {
Gee.ArrayList<PhotoID?> photo_ids = (new PhotoTable()).get_event_photos(event_id);
Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
Gee.ArrayList<PhotoSource> photos = new Gee.ArrayList<PhotoSource>();
foreach (PhotoID photo_id in photo_ids) {
photos.add(LibraryPhoto.fetch(photo_id));
}
......@@ -125,7 +125,7 @@ public class EventsDirectoryPage : CheckerboardPage {
return null;
}
public void add_event(EventID event_id) {
DirectoryItem item = new DirectoryItem(event_id, event_table);
add_item(item);
......@@ -173,7 +173,8 @@ public class EventPage : CollectionPage {
}
protected override void on_photos_menu() {
set_item_sensitive("/CollectionMenuBar/PhotosMenu/MakePrimary", get_selected_count() == 1);
set_item_sensitive("/CollectionMenuBar/PhotosMenu/MakePrimary",
get_selected_count() == 1);
base.on_photos_menu();
}
......
......@@ -73,6 +73,23 @@ namespace Exif {
return false;
}
private Exif.Entry? find_entry(Exif.Data exif, Exif.Ifd ifd, Exif.Tag tag, Exif.Format format) {
assert(exif != null);
Exif.Content content = exif.ifd[(int) ifd];
assert(content != null);
Exif.Entry entry = content.get_entry(tag);
if (entry == null)
return null;
assert(entry.format == format);
if ((format != Exif.Format.ASCII) && (format != Exif.Format.UNDEFINED))
assert(entry.size == format.get_size());
return entry;
}
private Exif.Entry? find_entry_multiformat(Exif.Data exif, Exif.Ifd ifd, Exif.Tag tag,
Exif.Format format1, Exif.Format format2) {
assert(exif != null);
......@@ -91,6 +108,28 @@ namespace Exif {
return entry;
}
public Orientation get_orientation(Exif.Data exif) {
Exif.Entry entry = find_entry(exif, Exif.Ifd.ZERO, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null)
return Orientation.TOP_LEFT;
int o = Exif.Convert.get_short(entry.data, exif.get_byte_order());
if (o < (int) Orientation.MIN || o > (int) Orientation.MAX)
return Orientation.TOP_LEFT;
return (Orientation) o;
}
public void set_orientation(ref Exif.Data exif, Orientation orientation) {
Exif.Entry entry = find_first_entry(exif, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null) {
// TODO: Need a fall-back here
error("Unable to set orientation: no entry found");
}
Exif.Convert.set_short(entry.data, exif.get_byte_order(), orientation);
}
public bool get_dimensions(Exif.Data exif, out Dimensions dim) {
Exif.Entry width = find_entry_multiformat(exif, Exif.Ifd.EXIF,
Exif.Tag.PIXEL_X_DIMENSION, Exif.Format.SHORT, Exif.Format.LONG);
......@@ -238,27 +277,13 @@ public class PhotoExif {
public Orientation get_orientation() {
update();
Exif.Entry entry = find_entry(Exif.Ifd.ZERO, Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null)
return Orientation.TOP_LEFT;
int o = Exif.Convert.get_short(entry.data, exif.get_byte_order());
if (o < (int) Orientation.MIN || o > (int) Orientation.MAX)
return Orientation.TOP_LEFT;
return (Orientation) o;
return Exif.get_orientation(exif);
}
public void set_orientation(Orientation orientation) {
update();
Exif.Entry entry = find_first_entry(Exif.Tag.ORIENTATION, Exif.Format.SHORT);
if (entry == null) {
// TODO: Need a fall-back here
error("Unable to set orientation: no entry found");
}
Exif.Convert.set_short(entry.data, exif.get_byte_order(), orientation);
Exif.set_orientation(ref exif, orientation);
}
public bool get_dimensions(out Dimensions dim) {
......@@ -276,7 +301,7 @@ public class PhotoExif {
public string? get_datetime() {
update();
Exif.Entry datetime = find_entry(Exif.Ifd.EXIF, Exif.Tag.DATE_TIME_ORIGINAL, Exif.Format.ASCII);
Exif.Entry datetime = Exif.find_entry(exif, Exif.Ifd.EXIF, Exif.Tag.DATE_TIME_ORIGINAL, Exif.Format.ASCII);
if (datetime == null)
return null;
......@@ -335,31 +360,6 @@ public class PhotoExif {
exif.fix();
}
private Exif.Entry? find_entry(Exif.Ifd ifd, Exif.Tag tag, Exif.Format format) {
assert(exif != null);
Exif.Content content = exif.ifd[(int) ifd];
assert(content != null);
Exif.Entry entry = content.get_entry(tag);
if (entry == null)
return null;
assert(entry.format == format);
if ((format != Exif.Format.ASCII) && (format != Exif.Format.UNDEFINED))
assert(entry.size == format.get_size());
return entry;
}
private Exif.Entry? find_first_entry(Exif.Tag tag, Exif.Format format) {
assert(exif != null);
return Exif.find_first_entry(exif, tag, format);
}
public void commit() throws Error {
if (exif == null)
return;
......
......@@ -55,9 +55,13 @@ class ImportPreview : LayoutItem, PhotoSource {
public Dimensions get_dimensions() {
Dimensions dimensions;
if (!Exif.get_dimensions(exif, out dimensions))
Orientation orientation = Exif.get_orientation(exif);
if (!Exif.get_dimensions(exif, out dimensions)) {
dimensions = Dimensions(0,0);
return dimensions;
}
return orientation.rotate_dimensions(dimensions);
}
public Exif.Data? get_exif() {
......@@ -871,6 +875,14 @@ public class ImportQueuePage : SinglePhotoPage {
}
}
public override int get_queryable_count() {
return 1;
}
public override int get_selected_queryable_count() {
return get_queryable_count();
}
public override Gee.Iterable<Queryable>? get_queryables() {
return null;
}
......
......@@ -32,6 +32,10 @@ public class LibraryWindow : AppWindow {
on_file_import },
{ "CommonSortEvents", null, "Sort _Events", null, null, on_sort_events }
};
private const Gtk.ToggleActionEntry[] COMMON_TOGGLE_ACTIONS = {
{ "CommonDisplayBasicProperties", null, "Basic _Information", "<Ctrl><Shift>I", "Display basic information of the selection", on_display_basic_properties, false }
};
private const Gtk.RadioActionEntry[] COMMON_SORT_EVENTS_ORDER_ACTIONS = {
{ "CommonSortEventsAscending", Gtk.STOCK_SORT_ASCENDING, "_Ascending", null,
......@@ -44,6 +48,9 @@ public class LibraryWindow : AppWindow {
private string import_dir = Environment.get_home_dir();
private Gtk.VPaned sidebar_paned = new Gtk.VPaned();
private Gtk.Frame bottom_frame = new Gtk.Frame(null);
private class FileImportJob : BatchImportJob {
private File file_or_dir;
private bool copy_to_library;
......@@ -110,6 +117,8 @@ public class LibraryWindow : AppWindow {
private Sidebar sidebar = new Sidebar();
private SidebarMarker cameras_marker = null;
private BasicProperties basic_properties = new BasicProperties();
private Gtk.Notebook notebook = new Gtk.Notebook();
private Gtk.Box layout = new Gtk.VBox(false, 0);
......@@ -163,7 +172,20 @@ public class LibraryWindow : AppWindow {
foreach (DiscoveredCamera camera in CameraTable.get_instance().get_cameras())
add_camera_page(camera);
}
public override void show_all() {
base.show_all();
Gtk.ToggleAction basic_properties_action =
(Gtk.ToggleAction) current_page.common_action_group.get_action(
"CommonDisplayBasicProperties");
assert(basic_properties_action != null);
if (!basic_properties_action.get_active()) {
bottom_frame.hide();
}
}
public static LibraryWindow get_app() {
assert(instance is LibraryWindow);
......@@ -220,6 +242,7 @@ public class LibraryWindow : AppWindow {
base.add_common_actions(action_group);
action_group.add_actions(COMMON_LIBRARY_ACTIONS, this);
action_group.add_toggle_actions(COMMON_TOGGLE_ACTIONS, this);
action_group.add_radio_actions(COMMON_SORT_EVENTS_ORDER_ACTIONS, SORT_EVENTS_ORDER_ASCENDING,
on_events_sort_changed);
}
......@@ -327,6 +350,18 @@ public class LibraryWindow : AppWindow {
// the events directory page needs to know about this
events_directory_page.notify_sort_changed(events_sort);
}
private void on_display_basic_properties(Gtk.Action action) {
bool display = ((Gtk.ToggleAction) action).get_active();
if (display) {
bottom_frame.show();
} else {
if (sidebar_paned.child2 != null) {
bottom_frame.hide();
}
}
}
public void enqueue_batch_import(BatchImport batch_import) {
if (import_queue_page == null) {
......@@ -595,8 +630,8 @@ public class LibraryWindow : AppWindow {
}
private void add_camera_page(DiscoveredCamera camera) {
ImportPage page = new ImportPage(camera.gcamera, camera.uri);
ImportPage page = new ImportPage(camera.gcamera, camera.uri);
// create the Cameras row if this is the first one
if (cameras_marker == null)
cameras_marker = sidebar.insert_grouping_after(events_directory_page.get_marker(),
......@@ -728,10 +763,21 @@ public class LibraryWindow : AppWindow {
scrolled_sidebar.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
scrolled_sidebar.add(sidebar);
// divy the sidebar up into selection tree list and properties
Gtk.Frame top_frame = new Gtk.Frame(null);
top_frame.add(scrolled_sidebar);
top_frame.set_shadow_type(Gtk.ShadowType.IN);
bottom_frame.add(basic_properties);
bottom_frame.set_shadow_type(Gtk.ShadowType.IN);
sidebar_paned.pack1(top_frame, true, false);
sidebar_paned.pack2(bottom_frame, false, false);
sidebar_paned.set_position(1000);
// layout the selection tree to the left of the collection/toolbar box with an adjustable
// gutter between them, framed for presentation
Gtk.Frame left_frame = new Gtk.Frame(null);
left_frame.add(scrolled_sidebar);
left_frame.add(sidebar_paned);
left_frame.set_shadow_type(Gtk.ShadowType.IN);
Gtk.Frame right_frame = new Gtk.Frame(null);
......@@ -755,11 +801,27 @@ public class LibraryWindow : AppWindow {
public void switch_to_page(Page page) {
if (page == current_page)
return;
if (current_page != null) {
current_page.switching_from();
remove_accel_group(current_page.ui.get_accel_group());
// carry over menubar toggle activity between pages
Gtk.ToggleAction old_action =
(Gtk.ToggleAction) current_page.common_action_group.get_action(
"CommonDisplayBasicProperties");
assert(old_action != null);
Gtk.ToggleAction new_action =
(Gtk.ToggleAction) page.common_action_group.get_action(
"CommonDisplayBasicProperties");
assert(new_action != null);
new_action.set_active(old_action.get_active());
// unsubscribe to the basic properties display signal (new page subscribes below)
current_page.selection_changed -= on_selection_changed;
}
int pos = get_notebook_pos(page);
......@@ -784,6 +846,11 @@ public class LibraryWindow : AppWindow {
page.show_all();
page.switched_to();
on_selection_changed();
// subscribe to this signal for each event page so basic properties display will update
current_page.selection_changed += on_selection_changed;
}
private bool is_page_selected(Page page, Gtk.TreePath path) {
......@@ -846,6 +913,10 @@ public class LibraryWindow : AppWindow {
// this signal
Idle.add(focus_on_current_page);
}
private void on_selection_changed() {
basic_properties.update_properties(current_page);
}
public void mounted_camera_shell_notification(string uri) {
debug("mount point reported: %s", uri);
......
......@@ -48,6 +48,8 @@ public abstract class Page : Gtk.ScrolledWindow {
private Gtk.Widget event_source = null;
private bool dnd_enabled = false;
private bool in_view = false;
public signal void selection_changed();
public Page(string page_name) {
this.page_name = page_name;
......@@ -55,6 +57,14 @@ public abstract class Page : Gtk.ScrolledWindow {
set_flags(Gtk.WidgetFlags.CAN_FOCUS);
}
protected virtual void on_selection_changed(int count) {
}
protected void notify_selection_changed(int count) {
on_selection_changed(count);
selection_changed();
}
public string get_page_name() {
return page_name;
}
......@@ -407,8 +417,12 @@ public abstract class Page : Gtk.ScrolledWindow {
return on_motion(event, x, y, mask);
}
public abstract int get_queryable_count();
public abstract Gee.Iterable<Queryable>? get_queryables();
public abstract int get_selected_queryable_count();
public abstract Gee.Iterable<Queryable>? get_selected_queryables();
}
......@@ -446,9 +460,6 @@ public abstract class CheckerboardPage : Page {
context_menu = (Gtk.Menu) ui.get_widget(path);
}
protected virtual void on_selection_changed(int count) {
}
public virtual Gtk.Menu? get_context_menu() {
return context_menu;
}
......@@ -500,18 +511,40 @@ public abstract class CheckerboardPage : Page {
public Gee.Iterable<LayoutItem> get_items() {
return layout.items;
}
public Gee.Iterable<LayoutItem> get_selected() {
return selected_items;
}
public override int get_queryable_count() {
return get_count();
}
public override Gee.Iterable<Queryable>? get_queryables() {
return get_items();
}
public override int get_selected_queryable_count() {
return get_selected_count();
}
public override Gee.Iterable<Queryable>? get_selected_queryables() {
return get_selected();
}
public void add_item(LayoutItem item) {
layout.add_item(item);
}
public void remove_item(LayoutItem item) {
int count = selected_items.size;
selected_items.remove(item);
layout.remove_item(item);
if (count != selected_items.size) {
notify_selection_changed(selected_items.size);
}
}
public int remove_selected() {
......@@ -521,16 +554,25 @@ public abstract class CheckerboardPage : Page {
layout.remove_item(item);
selected_items.clear();
if (count != selected_items.size) {
notify_selection_changed(selected_items.size);
}
return count;
}
public int remove_all() {
int count = layout.items.size;
int selection_count = selected_items.size;
layout.clear();
selected_items.clear();
if (selection_count != selected_items.size) {
notify_selection_changed(selected_items.size);
}
return count;
}
......@@ -549,7 +591,7 @@ public abstract class CheckerboardPage : Page {
}
if (changed)
on_selection_changed(selected_items.size);
notify_selection_changed(selected_items.size);
}
public void unselect_all() {
......@@ -563,7 +605,7 @@ public abstract class CheckerboardPage : Page {
selected_items.clear();
on_selection_changed(0);
notify_selection_changed(0);
}
public void unselect_all_but(LayoutItem exception) {
......@@ -585,7 +627,7 @@ public abstract class CheckerboardPage : Page {
selected_items.add(exception);
if (changed)
on_selection_changed(1);
notify_selection_changed(1);
}
public void select(LayoutItem item) {
......@@ -595,7 +637,7 @@ public abstract class CheckerboardPage : Page {
item.select();
selected_items.add(item);
on_selection_changed(selected_items.size);
notify_selection_changed(selected_items.size);
}
}
......@@ -606,7 +648,7 @@ public abstract class CheckerboardPage : Page {
item.unselect();
selected_items.remove(item);
on_selection_changed(selected_items.size);
notify_selection_changed(selected_items.size);
}
}
......@@ -619,13 +661,13 @@ public abstract class CheckerboardPage : Page {
selected_items.remove(item);
}
on_selection_changed(selected_items.size);
notify_selection_changed(selected_items.size);
}
public int get_selected_count() {
return selected_items.size;
}
protected override bool key_press_event(Gdk.EventKey event) {
bool handled = true;
switch (Gdk.keyval_name(event.keyval)) {
......@@ -1164,14 +1206,6 @@ public abstract class CheckerboardPage : Page {
select(item);
}
}
public override Gee.Iterable<Queryable>? get_queryables() {
return get_selected();
}
public override Gee.Iterable<Queryable>? get_selected_queryables() {
return get_items();
}
}
public abstract class SinglePhotoPage : Page {
......
......@@ -168,13 +168,15 @@ public abstract class EditingHostPage : SinglePhotoPage {
photo.altered += on_photo_altered;
set_page_name(photo.get_name());
quick_update_pixbuf();
update_ui();
// signal the photo has been replaced
photo_replaced(old_photo, photo);
notify_selection_changed(1);
}
private void quick_update_pixbuf() {
......@@ -186,7 +188,6 @@ public abstract class EditingHostPage : SinglePhotoPage {
private bool update_pixbuf() {
set_pixbuf(photo.get_pixbuf(TransformablePhoto.SCREEN));
return false;
}
......@@ -345,6 +346,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
}
private void on_photo_altered(TransformablePhoto p) {
// TODO: use a different signal: e.g. contents_changed or photo_altered
notify_selection_changed(1);
quick_update_pixbuf();
update_ui();
......@@ -635,6 +639,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
public override Gee.Iterable<Queryable>? get_queryables() {
Gee.ArrayList<PhotoSource> photo_array_list = new Gee.ArrayList<PhotoSource>();
photo_array_list.add(photo);
return photo_array_list;
}
......@@ -642,6 +647,14 @@ public abstract class EditingHostPage : SinglePhotoPage {
public override Gee.Iterable<Queryable>? get_selected_queryables() {
return get_queryables();
}
public override int get_queryable_count() {
return 1;
}
public override int get_selected_queryable_count() {
return get_queryable_count();
}
}
public class LibraryPhotoPage : EditingHostPage {
......
/* Copyright 2009 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
private class BasicProperties : Gtk.HBox {
private Gtk.Label label = new Gtk.Label("");
private Gtk.Label info = new Gtk.Label("");
private string title;
private time_t start_time = time_t();
private time_t end_time = time_t();
private Dimensions dimensions;
private uint64 filesize;
private int photo_count;
public BasicProperties() {
label.set_justify(Gtk.Justification.RIGHT);
label.set_alignment(0, (float) 5e-1);
info.set_alignment(0, (float) 5e-1);
pack_start(label, false, false, 3);
pack_start(info, true, true, 3);
info.set_ellipsize(Pango.EllipsizeMode.END);
}
private string get_prettyprint_time(Time time) {
return "%d:%02d %s".printf((time.hour-1)%12+1, time.minute,
time.hour < 12 ? "AM" : "PM");
}
private string get_prettyprint_date(Time time) {
return time.format("%a %b") + " %d, %d".printf(time.day, 1900 + time.year);
}
private void set_text(string new_label, string new_info) {
label.set_text(new_label);
info.set_text(new_info);
}
public void clear_properties() {
title = "";
start_time = time_t();
end_time = time_t();
dimensions = Dimensions(0,0);
filesize = 0;
photo_count = -1;
}
public void update_properties(Page current_page) {
clear_properties();
get_properties(current_page);
string basic_properties_labels = "";
string basic_properties_info = "";
if (title != "") {
basic_properties_labels += "Title:";
basic_properties_info += title;
}
if (photo_count >= 0) {
basic_properties_labels += "\nItems:";
basic_properties_info += "\n%d".printf(photo_count);
basic_properties_info += photo_count == 1 ? " Photo" : " Photos";
}
if (start_time != time_t()) {
string start_date = get_prettyprint_date(Time.local(start_time));
string start_time = get_prettyprint_time(Time.local(start_time));
string end_date = get_prettyprint_date(Time.local(end_time));
string end_time = get_prettyprint_time(Time.local(end_time));
if (start_date == end_date) {
basic_properties_labels += "\nDate:";
basic_properties_info += "\n" + start_date;
if (start_date == end_date && start_time == end_time) {
basic_properties_labels += "\nTime:";
basic_properties_info += "\n" + start_time;
} else {
basic_properties_labels += "\nTime:\n";
basic_properties_info += "\n" + start_time + " to\n" + end_time;
}
} else {
basic_properties_labels += "\nDate:\n";
basic_properties_info += "\n" + start_date + " to\n" + end_date;
}
}
if (filesize > 0 || (dimensions.width != 0 && dimensions.height != 0)) {
basic_properties_labels += "\nSize:";
if (filesize > 0 && (dimensions.width != 0 && dimensions.height != 0)) {
basic_properties_labels += "\n";
}
if (dimensions.width != 0 && dimensions.height != 0) {
basic_properties_info += "\n%d x %d".printf(dimensions.width, dimensions.height);
}
if (filesize > 0) {
basic_properties_info += "\n" + format_size_for_display((int64) filesize);
}
}
set_text(basic_properties_labels, basic_properties_info);
}
private void get_properties(Page current_page) {
int count = current_page.get_selected_queryable_count();
Gee.Iterable<Queryable>? queryables = current_page.get_selected_queryables();
if (queryables == null)
return;
if (count == 1) {
foreach (Queryable queryable in queryables) {
title = queryable.get_name();