/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
private abstract class Properties : Gtk.Box {
protected Gtk.Grid grid = new Gtk.Grid();
protected uint line_count = 0;
protected Properties() {
Object(orientation: Gtk.Orientation.VERTICAL, homogeneous : false);
grid.row_spacing = 6;
grid.column_spacing = 12;
pack_start(grid, false, false, 0);
}
protected void add_line(string label_text, string info_text, bool multi_line = false, string? href = null) {
Gtk.Label label = new Gtk.Label("");
Gtk.Widget info;
label.set_justify(Gtk.Justification.RIGHT);
label.get_style_context().add_class("dim-label");
label.set_markup(GLib.Markup.printf_escaped("%s", label_text));
if (multi_line) {
Gtk.ScrolledWindow info_scroll = new Gtk.ScrolledWindow(null, null);
info_scroll.shadow_type = Gtk.ShadowType.NONE;
Gtk.TextView view = new Gtk.TextView();
// by default TextView widgets have a white background, which
// makes sense during editing. In this instance we only *show*
// the content and thus want that the parent's background color
// is inherited to the TextView
view.get_style_context().add_class("shotwell-static");
view.set_wrap_mode(Gtk.WrapMode.WORD);
view.set_cursor_visible(false);
view.set_editable(false);
view.buffer.text = is_string_empty(info_text) ? "" : info_text;
view.hexpand = true;
info_scroll.add(view);
label.halign = Gtk.Align.END;
label.valign = Gtk.Align.START;
info = (Gtk.Widget) info_scroll;
} else {
Gtk.Label info_label = new Gtk.Label("");
if (!is_string_empty(info_text)) {
info_label.set_tooltip_text(info_text);
}
if (href == null) {
info_label.set_text(is_string_empty(info_text) ? "" : info_text);
} else {
info_label.set_markup("%s".printf(href, Markup.escape_text(info_text)));
}
info_label.set_ellipsize(Pango.EllipsizeMode.END);
info_label.halign = Gtk.Align.START;
info_label.valign = Gtk.Align.FILL;
info_label.hexpand = false;
info_label.vexpand = false;
info_label.set_justify(Gtk.Justification.LEFT);
info_label.set_selectable(true);
label.halign = Gtk.Align.END;
label.valign = Gtk.Align.FILL;
info = (Gtk.Widget) info_label;
}
grid.attach(label, 0, (int) line_count, 1, 1);
if (multi_line) {
grid.attach(info, 1, (int) line_count, 1, 3);
} else {
grid.attach(info, 1, (int) line_count, 1, 1);
}
line_count++;
}
protected string get_prettyprint_time(DateTime time) {
string timestring = time.format(Resources.get_hh_mm_format_string());
if (timestring[0] == '0')
timestring = timestring.substring(1, -1);
return timestring;
}
protected string get_prettyprint_time_with_seconds(DateTime time) {
string timestring = time.format(Resources.get_hh_mm_ss_format_string());
if (timestring[0] == '0')
timestring = timestring.substring(1, -1);
return timestring;
}
protected string get_prettyprint_date(DateTime date) {
string date_string = null;
var today = new DateTime.now_local();
if (date.get_day_of_year() == today.get_day_of_year() && date.get_year() == today.get_year()) {
date_string = _("Today");
} else if (date.get_day_of_year() == (today.get_day_of_year() - 1) && date.get_year() == today.get_year()) {
date_string = _("Yesterday");
} else {
date_string = format_local_date(date);
}
return date_string;
}
protected virtual void get_single_properties(DataView view) {
}
protected virtual void get_multiple_properties(Gee.Iterable? iter) {
}
protected virtual void get_properties(Page current_page) {
ViewCollection view = current_page.get_view();
if (view == null)
return;
// summarize selected items, if none selected, summarize all
int count = view.get_selected_count();
Gee.Iterable iter = null;
if (count != 0) {
iter = view.get_selected();
} else {
count = view.get_count();
iter = (Gee.Iterable) view.get_all();
}
if (iter == null || count == 0)
return;
if (count == 1) {
foreach (DataView item in iter) {
get_single_properties(item);
break;
}
} else {
get_multiple_properties(iter);
}
}
protected virtual void clear_properties() {
foreach (Gtk.Widget child in grid.get_children())
grid.remove(child);
line_count = 0;
}
public void update_properties(Page page) {
clear_properties();
internal_update_properties(page);
show_all();
}
public virtual void internal_update_properties(Page page) {
get_properties(page);
}
}
private class BasicProperties : Properties {
private string title;
private DateTime? start_time = new DateTime.now_utc();
private DateTime? end_time = new DateTime.now_utc();
private Dimensions dimensions;
private int photo_count;
private int event_count;
private int video_count;
private string exposure;
private string aperture;
private string iso;
private double clip_duration;
private string raw_developer;
private string raw_assoc;
public BasicProperties() {
base();
}
protected override void clear_properties() {
base.clear_properties();
title = "";
start_time = null;
end_time = null;
dimensions = Dimensions(0,0);
photo_count = -1;
event_count = -1;
video_count = -1;
exposure = "";
aperture = "";
iso = "";
clip_duration = 0.0;
raw_developer = "";
raw_assoc = "";
}
protected override void get_single_properties(DataView view) {
base.get_single_properties(view);
DataSource source = view.get_source();
title = source.get_name();
if (source is PhotoSource || source is PhotoImportSource) {
start_time = (source is PhotoSource) ? ((PhotoSource) source).get_exposure_time() :
((PhotoImportSource) source).get_exposure_time();
end_time = start_time;
PhotoMetadata? metadata = (source is PhotoSource) ? ((PhotoSource) source).get_metadata() :
((PhotoImportSource) source).get_metadata();
if (metadata != null) {
exposure = metadata.get_exposure_string();
if (exposure == null)
exposure = "";
aperture = metadata.get_aperture_string(true);
if (aperture == null)
aperture = "";
iso = metadata.get_iso_string();
if (iso == null)
iso = "";
dimensions = (metadata.get_pixel_dimensions() != null) ?
metadata.get_orientation().rotate_dimensions(metadata.get_pixel_dimensions()) :
Dimensions(0, 0);
}
if (source is PhotoSource)
dimensions = ((PhotoSource) source).get_dimensions();
if (source is Photo && ((Photo) source).get_master_file_format() == PhotoFileFormat.RAW) {
Photo photo = source as Photo;
raw_developer = photo.get_raw_developer().get_label();
raw_assoc = photo.is_raw_developer_available(RawDeveloper.CAMERA) ? _("RAW+JPEG") : "";
}
} else if (source is EventSource) {
EventSource event_source = (EventSource) source;
start_time = event_source.get_start_time();
end_time = event_source.get_end_time();
int event_photo_count;
int event_video_count;
MediaSourceCollection.count_media(event_source.get_media(), out event_photo_count,
out event_video_count);
photo_count = event_photo_count;
video_count = event_video_count;
} else if (source is VideoSource || source is VideoImportSource) {
if (source is VideoSource) {
Video video = (Video) source;
clip_duration = video.get_clip_duration();
if (video.get_is_interpretable())
dimensions = video.get_frame_dimensions();
start_time = video.get_exposure_time();
} else {
start_time = ((VideoImportSource) source).get_exposure_time();
}
end_time = start_time;
}
}
protected override void get_multiple_properties(Gee.Iterable? iter) {
base.get_multiple_properties(iter);
photo_count = 0;
video_count = 0;
foreach (DataView view in iter) {
DataSource source = view.get_source();
if (source is PhotoSource || source is PhotoImportSource) {
var exposure_time = (source is PhotoSource) ?
((PhotoSource) source).get_exposure_time() :
((PhotoImportSource) source).get_exposure_time();
if (exposure_time != null) {
if (start_time == null || exposure_time.compare(start_time) < 0)
start_time = exposure_time;
if (end_time == null || exposure_time.compare(end_time) > 0)
end_time = exposure_time;
}
photo_count++;
} else if (source is EventSource) {
EventSource event_source = (EventSource) source;
if (event_count == -1)
event_count = 0;
if ((start_time == null || event_source.get_start_time().compare(start_time) < 0) &&
event_source.get_start_time() != null ) {
start_time = event_source.get_start_time();
}
if ((end_time == null || event_source.get_end_time().compare(end_time) > 0) &&
event_source.get_end_time() != null ) {
end_time = event_source.get_end_time();
} else if (end_time == null || event_source.get_start_time().compare(end_time) > 0) {
end_time = event_source.get_start_time();
}
int event_photo_count;
int event_video_count;
MediaSourceCollection.count_media(event_source.get_media(), out event_photo_count,
out event_video_count);
photo_count += event_photo_count;
video_count += event_video_count;
event_count++;
} else if (source is VideoSource || source is VideoImportSource) {
var exposure_time = (source is VideoSource) ?
((VideoSource) source).get_exposure_time() :
((VideoImportSource) source).get_exposure_time();
if (exposure_time != null) {
if (start_time == null || exposure_time.compare(start_time) < 0)
start_time = exposure_time;
if (end_time == null || exposure_time.compare(end_time) > 0)
end_time = exposure_time;
}
video_count++;
}
}
}
protected override void get_properties(Page current_page) {
base.get_properties(current_page);
if (end_time == null)
end_time = start_time;
if (start_time == null)
start_time = end_time;
}
protected override void internal_update_properties(Page page) {
base.internal_update_properties(page);
// display the title if a Tag page
if (title == "" && page is TagPage)
title = ((TagPage) page).get_tag().get_user_visible_name();
if (title != "")
add_line(_("Title:"), guarded_markup_escape_text(title));
if (photo_count >= 0 || video_count >= 0) {
string label = _("Items:");
if (event_count >= 0) {
string event_num_string = (ngettext("%d Event", "%d Events", event_count)).printf(
event_count);
add_line(label, event_num_string);
label = "";
}
string photo_num_string = (ngettext("%d Photo", "%d Photos", photo_count)).printf(
photo_count);
string video_num_string = (ngettext("%d Video", "%d Videos", video_count)).printf(
video_count);
if (photo_count == 0 && video_count > 0) {
add_line(label, video_num_string);
return;
}
add_line(label, photo_num_string);
if (video_count > 0)
add_line("", video_num_string);
}
if (start_time != null) {
string start_date = get_prettyprint_date(start_time.to_local());
string start_time = get_prettyprint_time(start_time.to_local());
string end_date = get_prettyprint_date(end_time.to_local());
string end_time = get_prettyprint_time(end_time.to_local());
if (start_date == end_date) {
// display only one date if start and end are the same
add_line(_("Date:"), start_date);
if (start_time == end_time) {
// display only one time if start and end are the same
add_line(_("Time:"), start_time);
} else {
// display time range
add_line(_("From:"), start_time);
add_line(_("To:"), end_time);
}
} else {
// display date range
add_line(_("From:"), start_date);
add_line(_("To:"), end_date);
}
}
if (dimensions.has_area()) {
string label = _("Size:");
if (dimensions.has_area()) {
add_line(label, "%d × %d".printf(dimensions.width, dimensions.height));
label = "";
}
}
if (clip_duration > 0.0) {
add_line(_("Duration:"), _("%.1f seconds").printf(clip_duration));
}
if (raw_developer != "") {
add_line(_("Developer:"), raw_developer);
}
// RAW+JPEG flag.
if (raw_assoc != "")
add_line("", raw_assoc);
if (exposure != "" || aperture != "" || iso != "") {
string line = null;
// attempt to put exposure and aperture on the same line
if (exposure != "")
line = exposure;
if (aperture != "") {
if (line != null)
line += ", " + aperture;
else
line = aperture;
}
// if not both available but ISO is, add it to the first line
if ((exposure == "" || aperture == "") && iso != "") {
if (line != null)
line += ", " + "ISO " + iso;
else
line = "ISO " + iso;
add_line(_("Exposure:"), line);
} else {
// fit both on the top line, emit and move on
if (line != null)
add_line(_("Exposure:"), line);
// emit ISO on a second unadorned line
if (iso != "") {
if (line != null)
add_line("","ISO " + iso);
else
add_line(_("Exposure:"), "ISO " + iso);
}
}
}
}
}
private class ExtendedProperties : Properties {
private const string NO_VALUE = "";
// Photo stuff
private string file_path;
private uint64 filesize;
private Dimensions? original_dim;
private string camera_make;
private string camera_model;
private string flash;
private string focal_length;
private double gps_lat;
private string gps_lat_ref;
private double gps_long;
private string gps_long_ref;
private double gps_alt;
private string artist;
private string copyright;
private string software;
private string exposure_bias;
private string exposure_date;
private string exposure_time;
private bool is_raw;
private string? development_path;
private const string OSM_LINK_TEMPLATE = "https://www.openstreetmap.org/?mlat=%1$f&mlon=%2$f#map=16/%1$f/%2$f";
public ExtendedProperties() {
base();
grid.row_spacing = 6;
}
// Event stuff
// nothing here which is not already shown in the BasicProperties but
// comments, which are common, see below
// common stuff
private string comment;
protected override void clear_properties() {
base.clear_properties();
file_path = "";
development_path = "";
is_raw = false;
filesize = 0;
original_dim = Dimensions(0, 0);
camera_make = "";
camera_model = "";
flash = "";
focal_length = "";
gps_lat = -1;
gps_lat_ref = "";
gps_long = -1;
gps_long_ref = "";
artist = "";
copyright = "";
software = "";
exposure_bias = "";
exposure_date = "";
exposure_time = "";
comment = "";
}
protected override void get_single_properties(DataView view) {
base.get_single_properties(view);
DataSource source = view.get_source();
if (source == null)
return;
if (source is MediaSource) {
MediaSource media = (MediaSource) source;
file_path = Filename.display_name(media.get_master_file().get_path());
development_path = Filename.display_name(media.get_file().get_path());
filesize = media.get_master_filesize();
// as of right now, all extended properties other than filesize, filepath & comment aren't
// applicable to non-photo media types, so if the current media source isn't a photo,
// just do a short-circuit return
Photo photo = media as Photo;
if (photo == null)
return;
PhotoMetadata? metadata;
try {
// For some raw files, the developments may not contain metadata (please
// see the comment about cameras generating 'crazy' exif segments in
// Photo.develop_photo() for why), and so we'll want to display what was
// in the original raw file instead.
metadata = photo.get_master_metadata();
} catch (Error e) {
metadata = photo.get_metadata();
}
if (metadata == null)
return;
// Fix up any timestamp weirdness.
//
// If the exposure date wasn't properly set (the most likely cause of this
// is a raw with a metadataless development), use the one from the photo
// row.
if (metadata.get_exposure_date_time() == null)
metadata.set_exposure_date_time(new MetadataDateTime(photo.get_timestamp()));
is_raw = (photo.get_master_file_format() == PhotoFileFormat.RAW);
original_dim = metadata.get_pixel_dimensions();
camera_make = metadata.get_camera_make();
camera_model = metadata.get_camera_model();
flash = metadata.get_flash_string();
focal_length = metadata.get_focal_length_string();
metadata.get_gps(out gps_long, out gps_long_ref, out gps_lat, out gps_lat_ref, out gps_alt);
artist = metadata.get_artist();
copyright = metadata.get_copyright();
software = metadata.get_software();
exposure_bias = metadata.get_exposure_bias();
DateTime exposure_time_obj = metadata.get_exposure_date_time().get_timestamp();
exposure_date = get_prettyprint_date(exposure_time_obj.to_local());
exposure_time = get_prettyprint_time_with_seconds(exposure_time_obj.to_local());
comment = media.get_comment();
} else if (source is EventSource) {
Event event = (Event) source;
comment = event.get_comment();
}
}
public override void internal_update_properties(Page page) {
base.internal_update_properties(page);
if (page is EventsDirectoryPage) {
// nothing special to be done for now for Events
} else {
add_line(_("Location:"), (file_path != "" && file_path != null) ?
file_path.replace("&", "&") : NO_VALUE);
add_line(_("File size:"), (filesize > 0) ?
format_size((int64) filesize) : NO_VALUE);
if (is_raw)
add_line(_("Current Development:"), development_path);
add_line(_("Original dimensions:"), (original_dim != null && original_dim.has_area()) ?
"%d × %d".printf(original_dim.width, original_dim.height) : NO_VALUE);
add_line(_("Camera make:"), (camera_make != "" && camera_make != null) ?
camera_make : NO_VALUE);
add_line(_("Camera model:"), (camera_model != "" && camera_model != null) ?
camera_model : NO_VALUE);
add_line(_("Flash:"), (flash != "" && flash != null) ? flash : NO_VALUE);
add_line(_("Focal length:"), (focal_length != "" && focal_length != null) ?
focal_length : NO_VALUE);
add_line(_("Exposure date:"), (exposure_date != "" && exposure_date != null) ?
exposure_date : NO_VALUE);
add_line(_("Exposure time:"), (exposure_time != "" && exposure_time != null) ?
exposure_time : NO_VALUE);
add_line(_("Exposure bias:"), (exposure_bias != "" && exposure_bias != null) ? exposure_bias : NO_VALUE);
string? osm_link = null;
if (gps_lat != -1 && gps_lat_ref != "" && gps_long != -1 && gps_long_ref != "") {
var old_locale = Intl.setlocale(LocaleCategory.NUMERIC, "C");
osm_link = OSM_LINK_TEMPLATE.printf(gps_lat, gps_long);
Intl.setlocale(LocaleCategory.NUMERIC, old_locale);
}
add_line(_("GPS latitude:"), (gps_lat != -1 && gps_lat_ref != "" &&
gps_lat_ref != null) ? "%f °%s".printf(gps_lat, gps_lat_ref) : NO_VALUE, false, osm_link);
add_line(_("GPS longitude:"), (gps_long != -1 && gps_long_ref != "" &&
gps_long_ref != null) ? "%f °%s".printf(gps_long, gps_long_ref) : NO_VALUE, false, osm_link);
add_line(_("Artist:"), (artist != "" && artist != null) ? Markup.escape_text(artist) : NO_VALUE);
add_line(_("Copyright:"), (copyright != "" && copyright != null) ? copyright : NO_VALUE);
add_line(_("Software:"), (software != "" && software != null) ? software : NO_VALUE);
}
bool has_comment = (comment != "" && comment != null);
add_line(_("Comment:"), has_comment ? comment : NO_VALUE, has_comment);
}
}