Commit f21921ce authored by Norbert Preining's avatar Norbert Preining Committed by Lucas Beeler

Implements a major new feature: the ability to add comments to photos. Closes #1573.

parent 0fb5d20c
......@@ -41,6 +41,12 @@
<description>True if photo titles are to be displayed beneath thumbnails in collection views, false otherwise.</description>
</key>
<key name="display-photo-comments" type="b">
<default>false</default>
<summary>display photo comments</summary>
<description>True if photo comments are to be displayed beneath thumbnails in collection views, false otherwise.</description>
</key>
<key name="display-photo-tags" type="b">
<default>true</default>
<summary>display photo tags</summary>
......
......@@ -99,6 +99,8 @@ public abstract class CheckerboardItem : ThumbnailView {
// Collection properties CheckerboardItem understands
// SHOW_TITLES (bool)
public const string PROP_SHOW_TITLES = "show-titles";
// SHOW_COMMENTS (bool)
public const string PROP_SHOW_COMMENTS = "show-comments";
// SHOW_SUBTITLES (bool)
public const string PROP_SHOW_SUBTITLES = "show-subtitles";
......@@ -120,6 +122,8 @@ public abstract class CheckerboardItem : ThumbnailView {
private bool exposure = false;
private CheckerboardItemText? title = null;
private bool title_visible = true;
private CheckerboardItemText? comment = null;
private bool comment_visible = true;
private CheckerboardItemText? subtitle = null;
private bool subtitle_visible = false;
private Gdk.Pixbuf pixbuf = null;
......@@ -130,12 +134,20 @@ public abstract class CheckerboardItem : ThumbnailView {
private int row = -1;
private int horizontal_trinket_offset = 0;
public CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title,
public CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, string? comment,
bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
base(source);
pixbuf_dim = initial_pixbuf_dim;
this.title = new CheckerboardItemText(title, alignment, marked_up);
// on the checkboard page we display the comment in
// one line, i.e., replacing all newlines with spaces.
// that means that the display will contain "..." if the comment
// is too long.
// warning: changes here have to be done in set_comment, too!
if (comment != null)
this.comment = new CheckerboardItemText(comment.replace("\n", " "), alignment,
marked_up);
// Don't calculate size here, wait for the item to be assigned to a ViewCollection
// (notify_membership_changed) and calculate when the collection's property settings
......@@ -150,6 +162,10 @@ public abstract class CheckerboardItem : ThumbnailView {
return (title != null) ? title.get_text() : "";
}
public string get_comment() {
return (comment != null) ? comment.get_text() : "";
}
public void set_title(string text, bool marked_up = false,
Pango.Alignment alignment = Pango.Alignment.LEFT) {
if (title != null && title.is_set_to(text, marked_up, alignment))
......@@ -185,6 +201,42 @@ public abstract class CheckerboardItem : ThumbnailView {
notify_view_altered();
}
public void set_comment(string text, bool marked_up = false,
Pango.Alignment alignment = Pango.Alignment.LEFT) {
if (comment != null && comment.is_set_to(text, marked_up, alignment))
return;
comment = new CheckerboardItemText(text.replace("\n", " "), alignment, marked_up);
if (comment_visible) {
recalc_size("set_comment");
notify_view_altered();
}
}
public void clear_comment() {
if (comment == null)
return;
comment = null;
if (comment_visible) {
recalc_size("clear_comment");
notify_view_altered();
}
}
private void set_comment_visible(bool visible) {
if (comment_visible == visible)
return;
comment_visible = visible;
recalc_size("set_comment_visible");
notify_view_altered();
}
public string get_subtitle() {
return (subtitle != null) ? subtitle.get_text() : "";
}
......@@ -226,6 +278,7 @@ public abstract class CheckerboardItem : ThumbnailView {
protected override void notify_membership_changed(DataCollection? collection) {
bool title_visible = (bool) get_collection_property(PROP_SHOW_TITLES, true);
bool comment_visible = (bool) get_collection_property(PROP_SHOW_COMMENTS, true);
bool subtitle_visible = (bool) get_collection_property(PROP_SHOW_SUBTITLES, false);
bool altered = false;
......@@ -234,6 +287,11 @@ public abstract class CheckerboardItem : ThumbnailView {
altered = true;
}
if (this.comment_visible != comment_visible) {
this.comment_visible = comment_visible;
altered = true;
}
if (this.subtitle_visible != subtitle_visible) {
this.subtitle_visible = subtitle_visible;
altered = true;
......@@ -253,6 +311,10 @@ public abstract class CheckerboardItem : ThumbnailView {
set_title_visible((bool) val);
break;
case PROP_SHOW_COMMENTS:
set_comment_visible((bool) val);
break;
case PROP_SHOW_SUBTITLES:
set_subtitle_visible((bool) val);
break;
......@@ -278,6 +340,9 @@ public abstract class CheckerboardItem : ThumbnailView {
if (title != null)
title.clear_pango_layout();
if (comment != null)
comment.clear_pango_layout();
if (subtitle != null)
subtitle.clear_pango_layout();
}
......@@ -328,6 +393,8 @@ public abstract class CheckerboardItem : ThumbnailView {
// only add in the text heights if they're displayed
int title_height = (title != null && title_visible)
? title.get_height() + LABEL_PADDING : 0;
int comment_height = (comment != null && comment_visible)
? comment.get_height() + LABEL_PADDING : 0;
int subtitle_height = (subtitle != null && subtitle_visible)
? subtitle.get_height() + LABEL_PADDING : 0;
......@@ -338,11 +405,12 @@ public abstract class CheckerboardItem : ThumbnailView {
// height is frame width (two sides) + frame padding (two sides) + height of pixbuf
// + height of text + label padding (between pixbuf and text)
requisition.height = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2)
+ pixbuf_dim.height + title_height + subtitle_height;
+ pixbuf_dim.height + title_height + comment_height + subtitle_height;
#if TRACE_REFLOW_ITEMS
debug("recalc_size %s: %s title_height=%d subtitle_height=%d requisition=%s",
get_source().get_name(), reason, title_height, subtitle_height, requisition.to_string());
debug("recalc_size %s: %s title_height=%d comment_height=%d subtitle_height=%d requisition=%s",
get_source().get_name(), reason, title_height, comment_height, subtitle_height,
requisition.to_string());
#endif
if (!requisition.approx_equals(old_requisition)) {
......@@ -524,6 +592,18 @@ public abstract class CheckerboardItem : ThumbnailView {
text_y += title.get_height() + LABEL_PADDING;
}
if (comment != null && comment_visible) {
comment.allocation.x = allocation.x + FRAME_WIDTH;
comment.allocation.y = text_y;
comment.allocation.width = pixbuf_dim.width;
comment.allocation.height = comment.get_height();
ctx.move_to(comment.allocation.x, comment.allocation.y);
Pango.cairo_show_layout(ctx, comment.get_pango_layout(pixbuf_dim.width));
text_y += comment.get_height() + LABEL_PADDING;
}
if (subtitle != null && subtitle_visible) {
subtitle.allocation.x = allocation.x + FRAME_WIDTH;
subtitle.allocation.y = text_y;
......@@ -654,6 +734,9 @@ public abstract class CheckerboardItem : ThumbnailView {
if (title != null && title_visible && coord_in_rectangle(x, y, title.allocation))
return query_tooltip_on_text(title, tooltip);
if (comment != null && comment_visible && coord_in_rectangle(x, y, comment.allocation))
return query_tooltip_on_text(comment, tooltip);
if (subtitle != null && subtitle_visible && coord_in_rectangle(x, y, subtitle.allocation))
return query_tooltip_on_text(subtitle, tooltip);
......
......@@ -579,6 +579,26 @@ public class EditTitleCommand : SingleDataSourceCommand {
}
}
public class EditCommentCommand : SingleDataSourceCommand {
private string new_comment;
private string? old_comment;
public EditCommentCommand(MediaSource source, string new_comment) {
base(source, Resources.EDIT_COMMENT_LABEL, "");
this.new_comment = new_comment;
old_comment = source.get_comment();
}
public override void execute() {
((MediaSource) source).set_comment(new_comment);
}
public override void undo() {
((MediaSource) source).set_comment(old_comment);
}
}
public class EditMultipleTitlesCommand : MultipleDataSourceAtOnceCommand {
public string new_title;
public Gee.HashMap<MediaSource, string?> old_titles = new Gee.HashMap<MediaSource, string?>();
......@@ -602,6 +622,29 @@ public class EditMultipleTitlesCommand : MultipleDataSourceAtOnceCommand {
}
}
public class EditMultipleCommentsCommand : MultipleDataSourceAtOnceCommand {
public string new_comment;
public Gee.HashMap<MediaSource, string?> old_comments = new Gee.HashMap<MediaSource, string?>();
public EditMultipleCommentsCommand(Gee.Collection<MediaSource> media_sources, string new_comment) {
base (media_sources, Resources.EDIT_COMMENT_LABEL, "");
this.new_comment = new_comment;
foreach (MediaSource media in media_sources)
old_comments.set(media, media.get_comment());
}
public override void execute_on_all(Gee.Collection<DataSource> sources) {
foreach (DataSource source in sources)
((MediaSource) source).set_comment(new_comment);
}
public override void undo_on_all(Gee.Collection<DataSource> sources) {
foreach (DataSource source in sources)
((MediaSource) source).set_comment(old_comments.get((MediaSource) source));
}
}
public class RenameEventCommand : SimpleProxyableCommand {
private string new_name;
private string? old_name;
......
......@@ -734,6 +734,27 @@ public abstract class TextEntryDialogMediator {
}
}
public abstract class MultiTextEntryDialogMediator {
private MultiTextEntryDialog dialog;
public MultiTextEntryDialogMediator(string title, string label, string? initial_text = null) {
Gtk.Builder builder = AppWindow.create_builder();
dialog = new MultiTextEntryDialog();
dialog.get_content_area().add((Gtk.Box) builder.get_object("dialog-vbox4"));
dialog.set_builder(builder);
dialog.setup(on_modify_validate, title, label, initial_text);
}
protected virtual bool on_modify_validate(string text) {
return true;
}
protected string? _execute() {
return dialog.execute();
}
}
// This method takes primary and secondary texts and returns ready-to-use pango markup
// for a HIG-compliant alert dialog. Please see
// http://library.gnome.org/devel/hig-book/2.32/windows-alert.html.en for details.
......@@ -970,6 +991,60 @@ public class TextEntryDialog : Gtk.Dialog {
}
}
public class MultiTextEntryDialog : Gtk.Dialog {
public delegate bool OnModifyValidateType(string text);
private unowned OnModifyValidateType on_modify_validate;
private Gtk.TextView entry;
private Gtk.Builder builder;
private Gtk.Button button1;
private Gtk.Button button2;
private Gtk.ButtonBox action_area_box;
public void set_builder(Gtk.Builder builder) {
this.builder = builder;
}
public void setup(OnModifyValidateType? modify_validate, string title, string label, string? initial_text) {
set_title(title);
set_resizable(true);
set_default_size(500,300);
set_parent_window(AppWindow.get_instance().get_parent_window());
set_transient_for(AppWindow.get_instance());
on_modify_validate = modify_validate;
Gtk.Label name_label = builder.get_object("label9") as Gtk.Label;
name_label.set_text(label);
entry = builder.get_object("textview1") as Gtk.TextView;
entry.buffer = new Gtk.TextBuffer(null);
entry.buffer.text = (initial_text != null ? initial_text : "");
entry.grab_focus();
action_area_box = (Gtk.ButtonBox) get_action_area();
action_area_box.set_layout(Gtk.ButtonBoxStyle.END);
button1 = (Gtk.Button) add_button(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL);
button2 = (Gtk.Button) add_button(Gtk.Stock.SAVE, Gtk.ResponseType.OK);
set_has_resize_grip(true);
}
public string? execute() {
string? text = null;
show_all();
if (run() == Gtk.ResponseType.OK)
text = entry.buffer.text;
destroy();
return text;
}
}
public class EventRenameDialog : TextEntryDialogMediator {
public EventRenameDialog(string? event_name) {
base (_("Rename Event"), _("Name:"), event_name);
......@@ -994,6 +1069,20 @@ public class EditTitleDialog : TextEntryDialogMediator {
}
}
public class EditCommentDialog : MultiTextEntryDialogMediator {
public EditCommentDialog(string? photo_comment) {
base (_("Edit Comment"), _("Comment:"), photo_comment);
}
public virtual string? execute() {
return MediaSource.prep_comment(_execute());
}
protected override bool on_modify_validate(string text) {
return true;
}
}
// Returns: Gtk.ResponseType.YES (trash photos), Gtk.ResponseType.NO (only remove photos) and
// Gtk.ResponseType.CANCEL.
public Gtk.ResponseType remove_from_library_dialog(Gtk.Window owner, string title,
......
......@@ -80,9 +80,10 @@ public abstract class MediaSource : ThumbnailSource, Indexable {
}
private void update_indexable_keywords() {
string[] indexables = new string[2];
string[] indexables = new string[3];
indexables[0] = get_title();
indexables[1] = get_basename();
indexables[2] = get_comment();
indexable_keywords = prepare_indexable_strings(indexables);
}
......@@ -157,12 +158,19 @@ public abstract class MediaSource : ThumbnailSource, Indexable {
public abstract BackingFileState[] get_backing_files_state();
public abstract string? get_title();
public abstract string? get_comment();
public abstract void set_title(string? title);
public abstract void set_comment(string? comment);
public static string? prep_title(string? title) {
return prepare_input_text(title,
PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.EMPTY_IS_NULL, DEFAULT_USER_TEXT_INPUT_LENGTH);
}
public static string? prep_comment(string? comment) {
return prepare_input_text(comment,
PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.STRIP_CRLF & ~PrepareInputTextOptions.EMPTY_IS_NULL, DEFAULT_USER_TEXT_INPUT_LENGTH);
}
public abstract Rating get_rating();
public abstract void set_rating(Rating rating);
......
......@@ -13,6 +13,8 @@ public abstract class MediaMetadata {
public abstract MetadataDateTime? get_creation_date_time();
public abstract string? get_title();
public abstract string? get_comment();
}
public struct MetadataRational {
......
......@@ -12,9 +12,9 @@ public class MediaSourceItem : CheckerboardItem {
// preserve the same constructor arguments and semantics as CheckerboardItem so that we're
// a drop-in replacement
public MediaSourceItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title,
bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
base(source, initial_pixbuf_dim, title, marked_up, alignment);
public MediaSourceItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title,
string? comment, bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
base(source, initial_pixbuf_dim, title, comment, marked_up, alignment);
if (basis_sprocket_pixbuf == null)
basis_sprocket_pixbuf = Resources.load_icon("sprocket.png", 0);
}
......@@ -239,6 +239,8 @@ public abstract class MediaPage : CheckerboardPage {
get_view().freeze_notifications();
get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES,
Config.Facade.get_instance().get_display_photo_titles());
get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS,
Config.Facade.get_instance().get_display_photo_comments());
get_view().set_property(Thumbnail.PROP_SHOW_TAGS,
Config.Facade.get_instance().get_display_photo_tags());
get_view().set_property(Thumbnail.PROP_SIZE, get_thumb_size());
......@@ -388,6 +390,11 @@ public abstract class MediaPage : CheckerboardPage {
edit_title.label = Resources.EDIT_TITLE_MENU;
actions += edit_title;
Gtk.ActionEntry edit_comment = { "EditComment", null, TRANSLATABLE, "F3", TRANSLATABLE,
on_edit_comment };
edit_comment.label = Resources.EDIT_COMMENT_MENU;
actions += edit_comment;
Gtk.ActionEntry sort_photos = { "SortPhotos", null, TRANSLATABLE, null, null, null };
sort_photos.label = _("Sort _Photos");
actions += sort_photos;
......@@ -430,6 +437,12 @@ public abstract class MediaPage : CheckerboardPage {
titles.tooltip = _("Display the title of each photo");
toggle_actions += titles;
Gtk.ToggleActionEntry comments = { "ViewComment", null, TRANSLATABLE, "<Ctrl><Shift>C",
TRANSLATABLE, on_display_comments, Config.Facade.get_instance().get_display_photo_comments() };
comments.label = _("_Comments");
comments.tooltip = _("Display the comment of each photo");
toggle_actions += comments;
Gtk.ToggleActionEntry ratings = { "ViewRatings", null, TRANSLATABLE, "<Ctrl><Shift>N",
TRANSLATABLE, on_display_ratings, Config.Facade.get_instance().get_display_photo_ratings() };
ratings.label = Resources.VIEW_RATINGS_MENU;
......@@ -497,6 +510,7 @@ public abstract class MediaPage : CheckerboardPage {
protected override void update_actions(int selected_count, int count) {
set_action_sensitive("Export", selected_count > 0);
set_action_sensitive("EditTitle", selected_count > 0);
set_action_sensitive("EditComment", selected_count > 0);
set_action_sensitive("IncreaseSize", get_thumb_size() < Thumbnail.MAX_SCALE);
set_action_sensitive("DecreaseSize", get_thumb_size() > Thumbnail.MIN_SCALE);
set_action_sensitive("RemoveFromLibrary", selected_count > 0);
......@@ -799,6 +813,7 @@ public abstract class MediaPage : CheckerboardPage {
// set display options to match Configuration toggles (which can change while switched away)
get_view().freeze_notifications();
set_display_titles(Config.Facade.get_instance().get_display_photo_titles());
set_display_comments(Config.Facade.get_instance().get_display_photo_comments());
set_display_ratings(Config.Facade.get_instance().get_display_photo_ratings());
set_display_tags(Config.Facade.get_instance().get_display_photo_tags());
get_view().thaw_notifications();
......@@ -1016,6 +1031,18 @@ public abstract class MediaPage : CheckerboardPage {
get_command_manager().execute(new EditMultipleTitlesCommand(media_sources, new_title));
}
protected virtual void on_edit_comment() {
if (get_view().get_selected_count() == 0)
return;
Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources();
EditCommentDialog edit_comment_dialog = new EditCommentDialog(media_sources[0].get_comment());
string? new_comment = edit_comment_dialog.execute();
if (new_comment != null)
get_command_manager().execute(new EditMultipleCommentsCommand(media_sources, new_comment));
}
protected virtual void on_display_titles(Gtk.Action action) {
bool display = ((Gtk.ToggleAction) action).get_active();
......@@ -1024,6 +1051,14 @@ public abstract class MediaPage : CheckerboardPage {
Config.Facade.get_instance().set_display_photo_titles(display);
}
protected virtual void on_display_comments(Gtk.Action action) {
bool display = ((Gtk.ToggleAction) action).get_active();
set_display_comments(display);
Config.Facade.get_instance().set_display_photo_comments(display);
}
protected virtual void on_display_ratings(Gtk.Action action) {
bool display = ((Gtk.ToggleAction) action).get_active();
......@@ -1098,6 +1133,14 @@ public abstract class MediaPage : CheckerboardPage {
action.set_active(display);
}
protected override void set_display_comments(bool display) {
base.set_display_comments(display);
Gtk.ToggleAction? action = get_action("ViewComment") as Gtk.ToggleAction;
if (action != null)
action.set_active(display);
}
private Gtk.RadioAction sort_by_title_action() {
Gtk.RadioAction action = (Gtk.RadioAction) get_action("SortByTitle");
assert(action != null);
......
......@@ -15,7 +15,7 @@ public class MetadataWriter : Object {
public const uint COMMIT_DELAY_MSEC = 3000;
public const uint COMMIT_SPACING_MSEC = 50;
private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "rating", "exposure-time" };
private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "comment", "rating", "exposure-time" };
private class CommitJob : BackgroundJob {
public LibraryPhoto photo;
......@@ -82,6 +82,13 @@ public class MetadataWriter : Object {
changed = true;
}
// comment
string? current_comment = photo.get_comment();
if (current_comment != metadata.get_comment()) {
metadata.set_comment(current_comment);
changed = true;
}
// rating
Rating current_rating = photo.get_rating();
if (current_rating != metadata.get_rating()) {
......
......@@ -1870,6 +1870,12 @@ public abstract class CheckerboardPage : Page {
get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display);
get_view().thaw_notifications();
}
protected virtual void set_display_comments(bool display) {
get_view().freeze_notifications();
get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display);
get_view().thaw_notifications();
}
}
public abstract class SinglePhotoPage : Page {
......
......@@ -375,6 +375,7 @@ public abstract class Photo : PhotoSource, Dateable {
// normalize user text
this.row.title = prep_title(this.row.title);
this.row.comment = prep_comment(this.row.comment);
// don't need to lock the struct in the constructor (and to do so would hurt startup
// time)
......@@ -1084,6 +1085,7 @@ public abstract class Photo : PhotoSource, Dateable {
Orientation orientation = Orientation.TOP_LEFT;
time_t exposure_time = 0;
string title = "";
string comment = "";
Rating rating = Rating.UNRATED;
#if TRACE_MD5
......@@ -1098,6 +1100,7 @@ public abstract class Photo : PhotoSource, Dateable {
orientation = detected.metadata.get_orientation();
title = detected.metadata.get_title();
comment = detected.metadata.get_comment();
params.keywords = detected.metadata.get_keywords();
rating = detected.metadata.get_rating();
}
......@@ -1132,6 +1135,7 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.flags = 0;
params.row.master.file_format = detected.file_format;
params.row.title = title;
params.row.comment = comment;
params.row.rating = rating;
if (params.thumbnails != null) {
......@@ -1171,6 +1175,7 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.flags = 0;
params.row.master.file_format = PhotoFileFormat.JFIF;
params.row.title = null;
params.row.comment = null;
params.row.rating = Rating.UNRATED;
PhotoFileInterrogator interrogator = new PhotoFileInterrogator(params.file, params.sniffer_options);
......@@ -1331,6 +1336,9 @@ public abstract class Photo : PhotoSource, Dateable {
if (updated_row.title != detected.metadata.get_title())
list += "metadata:name";
if (updated_row.comment != detected.metadata.get_comment())
list += "metadata:comment";
if (updated_row.rating != detected.metadata.get_rating())
list += "metadata:rating";
}
......@@ -1349,6 +1357,7 @@ public abstract class Photo : PhotoSource, Dateable {
updated_row.exposure_time = date_time.get_timestamp();
updated_row.title = detected.metadata.get_title();
updated_row.comment = detected.metadata.get_comment();
updated_row.rating = detected.metadata.get_rating();
}
......@@ -1457,6 +1466,7 @@ public abstract class Photo : PhotoSource, Dateable {
if (reimport_state.metadata != null) {
set_title(reimport_state.metadata.get_title());
set_comment(reimport_state.metadata.get_comment());
set_rating(reimport_state.metadata.get_rating());
apply_user_metadata_for_reimport(reimport_state.metadata);
}
......@@ -2197,6 +2207,12 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
public override string? get_comment() {
lock (row) {
return row.comment;
}
}
public override void set_title(string? title) {
string? new_title = prep_title(title);
......@@ -2214,6 +2230,23 @@ public abstract class Photo : PhotoSource, Dateable {
notify_altered(new Alteration("metadata", "name"));
}
public override void set_comment(string? comment) {
string? new_comment = prep_comment(comment);
bool committed = false;
lock (row) {
if (new_comment == row.comment)
return;
committed = PhotoTable.get_instance().set_comment(row.photo_id, new_comment);
if (committed)
row.comment = new_comment;
}
if (committed)
notify_altered(new Alteration("metadata", "comment"));
}
public void set_import_id(ImportID import_id) {
DatabaseError dberr = null;
lock (row) {
......@@ -2259,6 +2292,34 @@ public abstract class Photo : PhotoSource, Dateable {
file_exif_updated();
}
public void set_comment_persistent(string? comment) throws Error {
PhotoFileReader source = get_source_reader();
// Try to write to backing file
if (!source.get_file_format().can_write_metadata()) {
warning("No photo file writer available for %s", source.get_filepath());
set_comment(comment);
return;
}
PhotoMetadata metadata = source.read_metadata();
metadata.set_comment(comment);
PhotoFileMetadataWriter writer = source.create_metadata_writer();
LibraryMonitor.blacklist_file(source.get_file(), "Photo.set_persistent_comment");
try {
writer.write_metadata(metadata);
} finally {
LibraryMonitor.unblacklist_file(source.get_file());
}
set_comment(comment);
file_exif_updated();
}
public void set_exposure_time(time_t time) {
bool committed;
lock (row) {
......@@ -2551,11 +2612,13 @@ public abstract class Photo : PhotoSource, Dateable {
public bool has_alterations() {
MetadataDateTime? date_time = null;
string? title = null;
string? comment = null;
PhotoMetadata? metadata = get_metadata();
if (metadata != null) {
date_time = metadata.get_exposure_date_time();
title = metadata.get_title();
comment = metadata.get_comment();
}
// Does this photo contain any date/time info?
......@@ -2574,8 +2637,10 @@ public abstract class Photo : PhotoSource, Dateable {
return row.transformations != null
|| row.orientation != backing_photo_row.original_orientation
|| (date_time != null && row.exposure_time != date_time.get_timestamp())
|| (get_comment() != comment)
|| (get_title() != title);
}
}
public PhotoTransformationState save_transformation_state() {
......@@ -3350,7 +3415,8 @@ public abstract class Photo : PhotoSource, Dateable {
// If asking for an full-sized file and there are no alterations (transformations or EXIF)
// *and* this is a copy of the original backing *and* there's no user metadata or title *and* metadata should be exported, then done
if (!has_alterations() && is_master && !has_user_generated_metadata() && (get_title() == null) && export_metadata)
if (!has_alterations() && is_master && !has_user_generated_metadata() &&
(get_title() == null) && (get_comment() == null) && export_metadata)
return true;
// copy over relevant metadata if possible, otherwise generate new metadata
......@@ -3368,6 +3434,7 @@ public abstract class Photo : PhotoSource, Dateable {
if(export_metadata) {
//set metadata
metadata.set_title(get_title());
metadata.set_comment(get_comment());