/* 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.
*/
// namespace for future migration of AppWindow alert and other question dialogs into single
// place: http://trac.yorba.org/ticket/3452
namespace Dialogs {
public bool confirm_delete_tag(Tag tag) {
int count = tag.get_sources_count();
if (count == 0)
return true;
string msg = ngettext(
"This will remove the tag “%s” from one photo. Continue?",
"This will remove the tag “%s” from %d photos. Continue?",
count).printf(tag.get_user_visible_name(), count);
return AppWindow.negate_affirm_question(msg, _("_Cancel"), _("_Delete"),
Resources.DELETE_TAG_TITLE);
}
public bool confirm_delete_saved_search(SavedSearch search) {
string msg = _("This will remove the saved search “%s”. Continue?")
.printf(search.get_name());
return AppWindow.negate_affirm_question(msg, _("_Cancel"), _("_Delete"),
Resources.DELETE_SAVED_SEARCH_DIALOG_TITLE);
}
public bool confirm_warn_developer_changed(int number) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog.with_markup(AppWindow.get_instance(),
Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE,
"%s",
ngettext("Switching developers will undo all changes you have made to this photo in Shotwell",
"Switching developers will undo all changes you have made to these photos in Shotwell", number));
dialog.add_buttons(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
dialog.add_buttons(_("_Switch Developer"), Gtk.ResponseType.YES);
int response = dialog.run();
dialog.destroy();
return response == Gtk.ResponseType.YES;
}
#if ENABLE_FACES
public bool confirm_delete_face(Face face) {
int count = face.get_sources_count();
string msg = ngettext(
"This will remove the face “%s” from one photo. Continue?",
"This will remove the face “%s” from %d photos. Continue?",
count).printf(face.get_name(), count);
return AppWindow.negate_affirm_question(msg, _("_Cancel"), _("_Delete"),
Resources.DELETE_FACE_TITLE);
}
#endif
}
namespace ExportUI {
private static File current_export_dir = null;
public File? choose_file(string current_file_basename) {
if (current_export_dir == null)
current_export_dir = File.new_for_path(Environment.get_home_dir());
string file_chooser_title = VideoReader.is_supported_video_filename(current_file_basename) ?
_("Export Video") : _("Export Photo");
Gtk.FileChooserDialog chooser = new Gtk.FileChooserDialog(file_chooser_title,
AppWindow.get_instance(), Gtk.FileChooserAction.SAVE, Resources.CANCEL_LABEL,
Gtk.ResponseType.CANCEL, Resources.SAVE_LABEL, Gtk.ResponseType.ACCEPT, null);
chooser.set_do_overwrite_confirmation(true);
chooser.set_current_folder(current_export_dir.get_path());
chooser.set_current_name(current_file_basename);
chooser.set_local_only(false);
File file = null;
if (chooser.run() == Gtk.ResponseType.ACCEPT) {
file = File.new_for_path(chooser.get_filename());
current_export_dir = file.get_parent();
}
chooser.destroy();
return file;
}
public File? choose_dir(string? user_title = null) {
if (current_export_dir == null)
current_export_dir = File.new_for_path(Environment.get_home_dir());
if (user_title == null)
user_title = _("Export Photos");
Gtk.FileChooserDialog chooser = new Gtk.FileChooserDialog(user_title,
AppWindow.get_instance(), Gtk.FileChooserAction.SELECT_FOLDER, Resources.CANCEL_LABEL,
Gtk.ResponseType.CANCEL, Resources.OK_LABEL, Gtk.ResponseType.ACCEPT, null);
chooser.set_current_folder(current_export_dir.get_path());
chooser.set_local_only(false);
File dir = null;
if (chooser.run() == Gtk.ResponseType.ACCEPT) {
dir = File.new_for_path(chooser.get_filename());
current_export_dir = dir;
}
chooser.destroy();
return dir;
}
}
// Ticket #3023
// Attempt to replace the system error with something friendlier
// if we can't copy an image over for editing in an external tool.
public void open_external_editor_error_dialog(Error err, Photo photo) {
// Did we fail because we can't write to this directory?
if (err is IOError.PERMISSION_DENIED || err is FileError.PERM) {
// Yes - display an alternate error message here.
AppWindow.error_message(
_("Shotwell couldn’t create a file for editing this photo because you do not have permission to write to %s.").printf(photo.get_master_file().get_parent().get_path()));
} else {
// No - something else is wrong, display the error message
// the system gave us.
AppWindow.error_message(Resources.launch_editor_failed(err));
}
}
public Gtk.ResponseType export_error_dialog(File dest, bool photos_remaining) {
string message = _("Unable to export the following photo due to a file error.\n\n") +
dest.get_path();
Gtk.ResponseType response = Gtk.ResponseType.NONE;
if (photos_remaining) {
message += _("\n\nWould you like to continue exporting?");
response = AppWindow.affirm_cancel_question(message, _("Con_tinue"));
} else {
AppWindow.error_message(message);
}
return response;
}
namespace ImportUI {
private const int REPORT_FAILURE_COUNT = 4;
internal const string SAVE_RESULTS_BUTTON_NAME = _("Save Details…");
internal const string SAVE_RESULTS_FILE_CHOOSER_TITLE = _("Save Details");
internal const int SAVE_RESULTS_RESPONSE_ID = 1024;
private string? generate_import_failure_list(Gee.List failed, bool show_dest_id) {
if (failed.size == 0)
return null;
string list = "";
for (int ctr = 0; ctr < REPORT_FAILURE_COUNT && ctr < failed.size; ctr++) {
list += "%s\n".printf(show_dest_id ? failed.get(ctr).dest_identifier :
failed.get(ctr).src_identifier);
}
int remaining = failed.size - REPORT_FAILURE_COUNT;
if (remaining > 0)
list += _("(and %d more)\n").printf(remaining);
return list;
}
public class QuestionParams {
public string question;
public string yes_button;
public string no_button;
public QuestionParams(string question, string yes_button, string no_button) {
this.question = question;
this.yes_button = yes_button;
this.no_button = no_button;
}
}
public bool import_has_photos(Gee.Collection import_collection) {
foreach (BatchImportResult current_result in import_collection) {
if (current_result.file != null
&& PhotoFileFormat.get_by_file_extension(current_result.file) != PhotoFileFormat.UNKNOWN) {
return true;
}
}
return false;
}
public bool import_has_videos(Gee.Collection import_collection) {
foreach (BatchImportResult current_result in import_collection) {
if (current_result.file != null && VideoReader.is_supported_video_file(current_result.file))
return true;
}
return false;
}
public string get_media_specific_string(Gee.Collection import_collection,
string photos_msg, string videos_msg, string both_msg, string neither_msg) {
bool has_photos = import_has_photos(import_collection);
bool has_videos = import_has_videos(import_collection);
if (has_photos && has_videos)
return both_msg;
else if (has_photos)
return photos_msg;
else if (has_videos)
return videos_msg;
else
return neither_msg;
}
public string create_result_report_from_manifest(ImportManifest manifest) {
StringBuilder builder = new StringBuilder();
string header = _("Import Results Report") + " (Shotwell " + Resources.APP_VERSION + " @ " +
TimeVal().to_iso8601() + ")\n\n";
builder.append(header);
string subhead = (ngettext("Attempted to import %d file.", "Attempted to import %d files.",
manifest.all.size)).printf(manifest.all.size);
subhead += " ";
subhead += (ngettext("Of these, %d file was successfully imported.",
"Of these, %d files were successfully imported.", manifest.success.size)).printf(
manifest.success.size);
subhead += "\n\n";
builder.append(subhead);
string current_file_summary = "";
//
// Duplicates
//
if (manifest.already_imported.size > 0) {
builder.append(_("Duplicate Photos/Videos Not Imported:") + "\n\n");
foreach (BatchImportResult result in manifest.already_imported) {
current_file_summary = result.src_identifier + " " +
_("duplicates existing media item") + "\n\t" +
result.duplicate_of.get_file().get_path() + "\n\n";
builder.append(current_file_summary);
}
}
//
// Files Not Imported Due to Camera Errors
//
if (manifest.camera_failed.size > 0) {
builder.append(_("Photos/Videos Not Imported Due to Camera Errors:") + "\n\n");
foreach (BatchImportResult result in manifest.camera_failed) {
current_file_summary = result.src_identifier + "\n\t" + _("error message:") + " " +
result.errmsg + "\n\n";
builder.append(current_file_summary);
}
}
//
// Files Not Imported Because They Weren't Recognized as Photos or Videos
//
if (manifest.skipped_files.size > 0) {
builder.append(_("Files Not Imported Because They Weren’t Recognized as Photos or Videos:")
+ "\n\n");
foreach (BatchImportResult result in manifest.skipped_files) {
current_file_summary = result.src_identifier + "\n\t" + _("error message:") + " " +
result.errmsg + "\n\n";
builder.append(current_file_summary);
}
}
//
// Photos/Videos Not Imported Because They Weren't in a Format Shotwell Understands
//
if (manifest.skipped_photos.size > 0) {
builder.append(_("Photos/Videos Not Imported Because They Weren’t in a Format Shotwell Understands:")
+ "\n\n");
foreach (BatchImportResult result in manifest.skipped_photos) {
current_file_summary = result.src_identifier + "\n\t" + _("error message:") + " " +
result.errmsg + "\n\n";
builder.append(current_file_summary);
}
}
//
// Photos/Videos Not Imported Because Shotwell Couldn't Copy Them into its Library
//
if (manifest.write_failed.size > 0) {
builder.append(_("Photos/Videos Not Imported Because Shotwell Couldn’t Copy Them into its Library:")
+ "\n\n");
foreach (BatchImportResult result in manifest.write_failed) {
current_file_summary = (_("couldn’t copy %s\n\tto %s")).printf(result.src_identifier,
result.dest_identifier) + "\n\t" + _("error message:") + " " +
result.errmsg + "\n\n";
builder.append(current_file_summary);
}
}
//
// Photos/Videos Not Imported Because GDK Pixbuf Library Identified them as Corrupt
//
if (manifest.corrupt_files.size > 0) {
builder.append(_("Photos/Videos Not Imported Because Files Are Corrupt:")
+ "\n\n");
foreach (BatchImportResult result in manifest.corrupt_files) {
current_file_summary = result.src_identifier + "\n\t" + _("error message:") + " |" +
result.errmsg + "|\n\n";
builder.append(current_file_summary);
}
}
//
// Photos/Videos Not Imported for Other Reasons
//
if (manifest.failed.size > 0) {
builder.append(_("Photos/Videos Not Imported for Other Reasons:") + "\n\n");
foreach (BatchImportResult result in manifest.failed) {
current_file_summary = result.src_identifier + "\n\t" + _("error message:") + " " +
result.errmsg + "\n\n";
builder.append(current_file_summary);
}
}
return builder.str;
}
// Summarizes the contents of an import manifest in an on-screen message window. Returns
// true if the user selected the yes action, false otherwise.
public bool report_manifest(ImportManifest manifest, bool show_dest_id,
QuestionParams? question = null) {
string message = "";
if (manifest.already_imported.size > 0) {
string photos_message = (ngettext("1 duplicate photo was not imported:\n",
"%d duplicate photos were not imported:\n",
manifest.already_imported.size)).printf(manifest.already_imported.size);
string videos_message = (ngettext("1 duplicate video was not imported:\n",
"%d duplicate videos were not imported:\n",
manifest.already_imported.size)).printf(manifest.already_imported.size);
string both_message = (ngettext("1 duplicate photo/video was not imported:\n",
"%d duplicate photos/videos were not imported:\n",
manifest.already_imported.size)).printf(manifest.already_imported.size);
message += get_media_specific_string(manifest.already_imported, photos_message,
videos_message, both_message, both_message);
message += generate_import_failure_list(manifest.already_imported, show_dest_id);
}
if (manifest.failed.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo failed to import due to a file or hardware error:\n",
"%d photos failed to import due to a file or hardware error:\n",
manifest.failed.size)).printf(manifest.failed.size);
string videos_message = (ngettext("1 video failed to import due to a file or hardware error:\n",
"%d videos failed to import due to a file or hardware error:\n",
manifest.failed.size)).printf(manifest.failed.size);
string both_message = (ngettext("1 photo/video failed to import due to a file or hardware error:\n",
"%d photos/videos failed to import due to a file or hardware error:\n",
manifest.failed.size)).printf(manifest.failed.size);
string neither_message = (ngettext("1 file failed to import due to a file or hardware error:\n",
"%d files failed to import due to a file or hardware error:\n",
manifest.failed.size)).printf(manifest.failed.size);
message += get_media_specific_string(manifest.failed, photos_message, videos_message,
both_message, neither_message);
message += generate_import_failure_list(manifest.failed, show_dest_id);
}
if (manifest.write_failed.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo failed to import because the photo library folder was not writable:\n",
"%d photos failed to import because the photo library folder was not writable:\n",
manifest.write_failed.size)).printf(manifest.write_failed.size);
string videos_message = (ngettext("1 video failed to import because the photo library folder was not writable:\n",
"%d videos failed to import because the photo library folder was not writable:\n",
manifest.write_failed.size)).printf(manifest.write_failed.size);
string both_message = (ngettext("1 photo/video failed to import because the photo library folder was not writable:\n",
"%d photos/videos failed to import because the photo library folder was not writable:\n",
manifest.write_failed.size)).printf(manifest.write_failed.size);
string neither_message = (ngettext("1 file failed to import because the photo library folder was not writable:\n",
"%d files failed to import because the photo library folder was not writable:\n",
manifest.write_failed.size)).printf(manifest.write_failed.size);
message += get_media_specific_string(manifest.write_failed, photos_message, videos_message,
both_message, neither_message);
message += generate_import_failure_list(manifest.write_failed, show_dest_id);
}
if (manifest.camera_failed.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo failed to import due to a camera error:\n",
"%d photos failed to import due to a camera error:\n",
manifest.camera_failed.size)).printf(manifest.camera_failed.size);
string videos_message = (ngettext("1 video failed to import due to a camera error:\n",
"%d videos failed to import due to a camera error:\n",
manifest.camera_failed.size)).printf(manifest.camera_failed.size);
string both_message = (ngettext("1 photo/video failed to import due to a camera error:\n",
"%d photos/videos failed to import due to a camera error:\n",
manifest.camera_failed.size)).printf(manifest.camera_failed.size);
string neither_message = (ngettext("1 file failed to import due to a camera error:\n",
"%d files failed to import due to a camera error:\n",
manifest.camera_failed.size)).printf(manifest.camera_failed.size);
message += get_media_specific_string(manifest.camera_failed, photos_message, videos_message,
both_message, neither_message);
message += generate_import_failure_list(manifest.camera_failed, show_dest_id);
}
if (manifest.corrupt_files.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo failed to import because it was corrupt:\n",
"%d photos failed to import because they were corrupt:\n",
manifest.corrupt_files.size)).printf(manifest.corrupt_files.size);
string videos_message = (ngettext("1 video failed to import because it was corrupt:\n",
"%d videos failed to import because they were corrupt:\n",
manifest.corrupt_files.size)).printf(manifest.corrupt_files.size);
string both_message = (ngettext("1 photo/video failed to import because it was corrupt:\n",
"%d photos/videos failed to import because they were corrupt:\n",
manifest.corrupt_files.size)).printf(manifest.corrupt_files.size);
string neither_message = (ngettext("1 file failed to import because it was corrupt:\n",
"%d files failed to import because it was corrupt:\n",
manifest.corrupt_files.size)).printf(manifest.corrupt_files.size);
message += get_media_specific_string(manifest.corrupt_files, photos_message, videos_message,
both_message, neither_message);
message += generate_import_failure_list(manifest.corrupt_files, show_dest_id);
}
if (manifest.skipped_photos.size > 0) {
if (message.length > 0)
message += "\n";
// we have no notion of "unsupported" video files right now in Shotwell (all
// standard container formats are supported, it's just that the streams in them
// might or might not be interpretable), so this message does not need to be
// media specific
string skipped_photos_message = (ngettext("1 unsupported photo skipped:\n",
"%d unsupported photos skipped:\n", manifest.skipped_photos.size)).printf(
manifest.skipped_photos.size);
message += skipped_photos_message;
message += generate_import_failure_list(manifest.skipped_photos, show_dest_id);
}
if (manifest.skipped_files.size > 0) {
if (message.length > 0)
message += "\n";
// we have no notion of "non-video" video files right now in Shotwell, so this
// message doesn't need to be media specific
string skipped_files_message = (ngettext("1 non-image file skipped.\n",
"%d non-image files skipped.\n", manifest.skipped_files.size)).printf(
manifest.skipped_files.size);
message += skipped_files_message;
}
if (manifest.aborted.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo skipped due to user cancel:\n",
"%d photos skipped due to user cancel:\n",
manifest.aborted.size)).printf(manifest.aborted.size);
string videos_message = (ngettext("1 video skipped due to user cancel:\n",
"%d videos skipped due to user cancel:\n",
manifest.aborted.size)).printf(manifest.aborted.size);
string both_message = (ngettext("1 photo/video skipped due to user cancel:\n",
"%d photos/videos skipped due to user cancel:\n",
manifest.aborted.size)).printf(manifest.aborted.size);
string neither_message = (ngettext("1 file skipped due to user cancel:\n",
"%d file skipped due to user cancel:\n",
manifest.aborted.size)).printf(manifest.aborted.size);
message += get_media_specific_string(manifest.aborted, photos_message, videos_message,
both_message, neither_message);
message += generate_import_failure_list(manifest.aborted, show_dest_id);
}
if (manifest.success.size > 0) {
if (message.length > 0)
message += "\n";
string photos_message = (ngettext("1 photo successfully imported.\n",
"%d photos successfully imported.\n",
manifest.success.size)).printf(manifest.success.size);
string videos_message = (ngettext("1 video successfully imported.\n",
"%d videos successfully imported.\n",
manifest.success.size)).printf(manifest.success.size);
string both_message = (ngettext("1 photo/video successfully imported.\n",
"%d photos/videos successfully imported.\n",
manifest.success.size)).printf(manifest.success.size);
message += get_media_specific_string(manifest.success, photos_message, videos_message,
both_message, "");
}
int total = manifest.success.size + manifest.failed.size + manifest.camera_failed.size
+ manifest.skipped_photos.size + manifest.skipped_files.size + manifest.corrupt_files.size
+ manifest.already_imported.size + manifest.aborted.size + manifest.write_failed.size;
assert(total == manifest.all.size);
// if no media items were imported at all (i.e. an empty directory attempted), need to at least
// report that nothing was imported
if (total == 0)
message += _("No photos or videos imported.\n");
Gtk.MessageDialog dialog = null;
int dialog_response = Gtk.ResponseType.NONE;
if (question == null) {
dialog = new Gtk.MessageDialog(AppWindow.get_instance(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.INFO, Gtk.ButtonsType.NONE, "%s", message);
dialog.title = _("Import Complete");
Gtk.Widget save_results_button = dialog.add_button(ImportUI.SAVE_RESULTS_BUTTON_NAME,
ImportUI.SAVE_RESULTS_RESPONSE_ID);
save_results_button.set_visible(manifest.success.size < manifest.all.size);
Gtk.Widget ok_button = dialog.add_button(Resources.OK_LABEL, Gtk.ResponseType.OK);
dialog.set_default(ok_button);
Gtk.Window dialog_parent = (Gtk.Window) dialog.get_parent();
dialog_response = dialog.run();
dialog.destroy();
if (dialog_response == ImportUI.SAVE_RESULTS_RESPONSE_ID)
save_import_results(dialog_parent, create_result_report_from_manifest(manifest));
} else {
message += ("\n" + question.question);
dialog = new Gtk.MessageDialog(AppWindow.get_instance(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, "%s", message);
dialog.title = _("Import Complete");
Gtk.Widget save_results_button = dialog.add_button(ImportUI.SAVE_RESULTS_BUTTON_NAME,
ImportUI.SAVE_RESULTS_RESPONSE_ID);
save_results_button.set_visible(manifest.success.size < manifest.all.size);
Gtk.Widget no_button = dialog.add_button(question.no_button, Gtk.ResponseType.NO);
dialog.add_button(question.yes_button, Gtk.ResponseType.YES);
dialog.set_default(no_button);
dialog_response = dialog.run();
while (dialog_response == ImportUI.SAVE_RESULTS_RESPONSE_ID) {
save_import_results(dialog, create_result_report_from_manifest(manifest));
dialog_response = dialog.run();
}
dialog.hide();
dialog.destroy();
}
return (dialog_response == Gtk.ResponseType.YES);
}
internal void save_import_results(Gtk.Window? chooser_dialog_parent, string results_log) {
Gtk.FileChooserDialog chooser_dialog = new Gtk.FileChooserDialog(
ImportUI.SAVE_RESULTS_FILE_CHOOSER_TITLE, chooser_dialog_parent, Gtk.FileChooserAction.SAVE,
Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL, Resources.SAVE_AS_LABEL, Gtk.ResponseType.ACCEPT, null);
chooser_dialog.set_do_overwrite_confirmation(true);
chooser_dialog.set_current_folder(Environment.get_home_dir());
chooser_dialog.set_current_name("Shotwell Import Log.txt");
chooser_dialog.set_local_only(false);
int dialog_result = chooser_dialog.run();
File? chosen_file = chooser_dialog.get_file();
chooser_dialog.hide();
chooser_dialog.destroy();
if (dialog_result == Gtk.ResponseType.ACCEPT && chosen_file != null) {
try {
FileOutputStream outstream = chosen_file.replace(null, false, FileCreateFlags.NONE);
outstream.write(results_log.data);
outstream.close();
} catch (Error err) {
critical("couldn't save import results to log file %s: %s", chosen_file.get_path(),
err.message);
}
}
}
}
public abstract class TextEntryDialogMediator {
private TextEntryDialog dialog;
public TextEntryDialogMediator(string title, string label, string? initial_text = null,
Gee.Collection? completion_list = null, string? completion_delimiter = null) {
dialog = new TextEntryDialog();
dialog.setup(on_modify_validate, title, label, initial_text, completion_list, completion_delimiter);
}
protected virtual bool on_modify_validate(string text) {
return true;
}
protected string? _execute() {
return dialog.execute();
}
}
public abstract class MultiTextEntryDialogMediator {
private MultiTextEntryDialog dialog;
public MultiTextEntryDialogMediator(string title, string label, string? initial_text = null) {
dialog = new MultiTextEntryDialog();
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.
public string build_alert_body_text(string? primary_text, string? secondary_text, bool should_escape = true) {
if (should_escape) {
return "%s\n%s".printf(
guarded_markup_escape_text(primary_text), guarded_markup_escape_text(secondary_text));
}
return "%s\n%s".printf(
guarded_markup_escape_text(primary_text), secondary_text);
}
public class EventRenameDialog : TextEntryDialogMediator {
public EventRenameDialog(string? event_name) {
base (_("Rename Event"), _("Name:"), event_name);
}
public virtual string? execute() {
return Event.prep_event_name(_execute());
}
}
public class EditTitleDialog : TextEntryDialogMediator {
public EditTitleDialog(string? photo_title) {
// Dialog title
base (C_("Dialog Title", "Edit Title"),
_("Title:"), photo_title);
}
public virtual string? execute() {
return MediaSource.prep_title(_execute());
}
protected override bool on_modify_validate(string text) {
return true;
}
}
public class EditCommentDialog : MultiTextEntryDialogMediator {
public EditCommentDialog(string? comment, bool is_event = false) {
string title_tmp = (is_event)
// Dialog title
? _("Edit Event Comment")
: _("Edit Photo/Video Comment");
base(title_tmp, _("Comment:"), 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,
string user_message, int count) {
string trash_action = ngettext("Remove and _Trash File", "Remove and _Trash Files", count);
Gtk.MessageDialog dialog = new Gtk.MessageDialog(owner, Gtk.DialogFlags.MODAL,
Gtk.MessageType.WARNING, Gtk.ButtonsType.CANCEL, "%s", user_message);
dialog.add_button(_("_Remove From Library"), Gtk.ResponseType.NO);
dialog.add_button(trash_action, Gtk.ResponseType.YES);
// This dialog was previously created outright; we now 'hijack'
// dialog's old title and use it as the primary text, along with
// using the message as the secondary text.
dialog.set_markup(build_alert_body_text(title, user_message));
Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
dialog.destroy();
return result;
}
// Returns: Gtk.ResponseType.YES (delete photos), Gtk.ResponseType.NO (keep photos)
public Gtk.ResponseType remove_from_filesystem_dialog(Gtk.Window owner, string title,
string user_message) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(owner, Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, "%s", user_message);
dialog.add_button(_("_Keep"), Gtk.ResponseType.NO);
dialog.add_button(_("_Delete"), Gtk.ResponseType.YES);
dialog.set_default_response( Gtk.ResponseType.NO);
dialog.set_markup(build_alert_body_text(title, user_message));
Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
dialog.destroy();
return result;
}
public bool revert_editable_dialog(Gtk.Window owner, Gee.Collection photos) {
int count = 0;
foreach (Photo photo in photos) {
if (photo.has_editable())
count++;
}
if (count == 0)
return false;
string headline = (count == 1) ? _("Revert External Edit?") : _("Revert External Edits?");
string msg = ngettext(
"This will destroy all changes made to the external file. Continue?",
"This will destroy all changes made to %d external files. Continue?",
count).printf(count);
string action = (count == 1) ? _("Re_vert External Edit") : _("Re_vert External Edits");
Gtk.MessageDialog dialog = new Gtk.MessageDialog(owner, Gtk.DialogFlags.MODAL,
Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, "%s", msg);
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL);
dialog.add_button(action, Gtk.ResponseType.YES);
dialog.set_markup(build_alert_body_text(headline, msg));
Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
dialog.destroy();
return result == Gtk.ResponseType.YES;
}
public bool remove_offline_dialog(Gtk.Window owner, int count) {
if (count == 0)
return false;
string msg = ngettext(
"This will remove the photo from the library. Continue?",
"This will remove %d photos from the library. Continue?",
count).printf(count);
Gtk.MessageDialog dialog = new Gtk.MessageDialog(owner, Gtk.DialogFlags.MODAL,
Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, "%s", msg);
dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL);
dialog.add_button(_("_Remove"), Gtk.ResponseType.OK);
dialog.title = (count == 1) ? _("Remove Photo From Library") : _("Remove Photos From Library");
Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
dialog.destroy();
return result == Gtk.ResponseType.OK;
}
public const int MAX_OBJECTS_DISPLAYED = 3;
public void multiple_object_error_dialog(Gee.ArrayList objects, string message,
string title) {
string dialog_message = message + "\n";
//add objects
for(int i = 0; i < MAX_OBJECTS_DISPLAYED && objects.size > i; i++)
dialog_message += "\n" + objects.get(i).to_string();
int remainder = objects.size - MAX_OBJECTS_DISPLAYED;
if (remainder > 0) {
dialog_message += ngettext("\n\nAnd %d other.", "\n\nAnd %d others.",
remainder).printf(remainder);
}
Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_instance(),
Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", dialog_message);
dialog.title = title;
dialog.run();
dialog.destroy();
}
public abstract class TagsDialog : TextEntryDialogMediator {
public TagsDialog(string title, string label, string? initial_text = null) {
base (title, label, initial_text, HierarchicalTagIndex.get_global_index().get_all_tags(),
",");
}
}
public class AddTagsDialog : TagsDialog {
public AddTagsDialog() {
var title = GLib.dpgettext2 (null, "Dialog Title",
Resources.ADD_TAGS_TITLE);
base (title, _("Tags (separated by commas):"));
}
public string[]? execute() {
string? text = _execute();
if (text == null)
return null;
// only want to return null if the user chose cancel, however, on_modify_validate ensures
// that Tag.prep_tag_names won't return a zero-length array (and it never returns null)
return Tag.prep_tag_names(text.split(","));
}
protected override bool on_modify_validate(string text) {
if (text.contains(Tag.PATH_SEPARATOR_STRING))
return false;
// Can't simply call Tag.prep_tag_names().length because of this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=602208
string[] names = Tag.prep_tag_names(text.split(","));
return names.length > 0;
}
}
public class ModifyTagsDialog : TagsDialog {
public ModifyTagsDialog(MediaSource source) {
base (Resources.MODIFY_TAGS_LABEL, _("Tags (separated by commas):"),
get_initial_text(source));
}
private static string? get_initial_text(MediaSource source) {
Gee.Collection? source_tags = Tag.global.fetch_for_source(source);
if (source_tags == null)
return null;
Gee.Collection terminal_tags = Tag.get_terminal_tags(source_tags);
Gee.SortedSet tag_basenames = new Gee.TreeSet();
foreach (Tag tag in terminal_tags)
tag_basenames.add(HierarchicalTagUtilities.get_basename(tag.get_path()));
string? text = null;
foreach (string name in tag_basenames) {
if (text == null)
text = "";
else
text += ", ";
text += name;
}
return text;
}
public Gee.ArrayList? execute() {
string? text = _execute();
if (text == null)
return null;
Gee.ArrayList new_tags = new Gee.ArrayList();
// return empty list if no tags specified
if (is_string_empty(text))
return new_tags;
// break up by comma-delimiter, prep for use, and separate into list
string[] tag_names = Tag.prep_tag_names(text.split(","));
tag_names = HierarchicalTagIndex.get_global_index().get_paths_for_names_array(tag_names);
foreach (string name in tag_names)
new_tags.add(Tag.for_path(name));
return new_tags;
}
protected override bool on_modify_validate(string text) {
return (!text.contains(Tag.PATH_SEPARATOR_STRING));
}
}
// This function is used to determine whether or not files should be copied or linked when imported.
// Returns ACCEPT for copy, REJECT for link, and CANCEL for (drum-roll) cancel.
public Gtk.ResponseType copy_files_dialog() {
string msg = _("Shotwell can copy the photos into your library folder or it can import them without copying.");
Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_instance(), Gtk.DialogFlags.MODAL,
Gtk.MessageType.QUESTION, Gtk.ButtonsType.CANCEL, "%s", msg);
dialog.add_button(_("Co_py Photos"), Gtk.ResponseType.ACCEPT);
dialog.add_button(_("_Import in Place"), Gtk.ResponseType.REJECT);
dialog.title = _("Import to Library");
Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
dialog.destroy();
return result;
}
public void remove_photos_from_library(Gee.Collection photos) {
remove_from_app(photos, _("Remove From Library"),
(photos.size == 1) ? _("Removing Photo From Library") : _("Removing Photos From Library"));
}
public void remove_from_app(Gee.Collection sources, string dialog_title,
string progress_dialog_text) {
if (sources.size == 0)
return;
Gee.ArrayList photos = new Gee.ArrayList();
Gee.ArrayList