/* 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;
}
}
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;
}
public class ExportDialog : Gtk.Dialog {
public const int DEFAULT_SCALE = 1200;
// "Unmodified" and "Current," though they appear in the "Format:" popup menu, really
// aren't formats so much as they are operating modes that determine specific formats.
// Hereafter we'll refer to these as "special formats."
public const int NUM_SPECIAL_FORMATS = 2;
public const string UNMODIFIED_FORMAT_LABEL = _("Unmodified");
public const string CURRENT_FORMAT_LABEL = _("Current");
public const ScaleConstraint[] CONSTRAINT_ARRAY = { ScaleConstraint.ORIGINAL,
ScaleConstraint.DIMENSIONS, ScaleConstraint.WIDTH, ScaleConstraint.HEIGHT };
public const Jpeg.Quality[] QUALITY_ARRAY = { Jpeg.Quality.LOW, Jpeg.Quality.MEDIUM,
Jpeg.Quality.HIGH, Jpeg.Quality.MAXIMUM };
private static ScaleConstraint current_constraint = ScaleConstraint.ORIGINAL;
private static ExportFormatParameters current_parameters = ExportFormatParameters.current();
private static int current_scale = DEFAULT_SCALE;
private Gtk.Grid table = new Gtk.Grid();
private Gtk.ComboBoxText quality_combo;
private Gtk.ComboBoxText constraint_combo;
private Gtk.ComboBoxText format_combo;
private Gtk.Switch export_metadata;
private Gee.ArrayList format_options = new Gee.ArrayList();
private Gtk.Entry pixels_entry;
private Gtk.Widget ok_button;
private bool in_insert = false;
public ExportDialog(string title) {
bool use_header;
Gtk.Settings.get_default ().get ("gtk-dialogs-use-header", out use_header);
Object (use_header_bar: use_header ? 1 : 0);
this.title = title;
resizable = false;
//get information about the export settings out of our config backend
Config.Facade config = Config.Facade.get_instance();
current_parameters.mode = config.get_export_export_format_mode(); //ExportFormatMode
current_parameters.specified_format = config.get_export_photo_file_format(); //PhotoFileFormat
current_parameters.quality = config.get_export_quality(); //quality
current_parameters.export_metadata = config.get_export_export_metadata(); //export metadata
current_constraint = config.get_export_constraint(); //constraint
current_scale = config.get_export_scale(); //scale
quality_combo = new Gtk.ComboBoxText();
int ctr = 0;
foreach (Jpeg.Quality quality in QUALITY_ARRAY) {
quality_combo.append_text(quality.to_string());
if (quality == current_parameters.quality)
quality_combo.set_active(ctr);
ctr++;
}
constraint_combo = new Gtk.ComboBoxText();
ctr = 0;
foreach (ScaleConstraint constraint in CONSTRAINT_ARRAY) {
constraint_combo.append_text(constraint.to_string());
if (constraint == current_constraint)
constraint_combo.set_active(ctr);
ctr++;
}
format_combo = new Gtk.ComboBoxText();
format_add_option(UNMODIFIED_FORMAT_LABEL);
format_add_option(CURRENT_FORMAT_LABEL);
foreach (PhotoFileFormat format in PhotoFileFormat.get_writeable()) {
format_add_option(format.get_properties().get_user_visible_name());
}
pixels_entry = new Gtk.Entry();
pixels_entry.set_max_length(6);
pixels_entry.set_text("%d".printf(current_scale));
// register after preparation to avoid signals during init
constraint_combo.changed.connect(on_constraint_changed);
format_combo.changed.connect(on_format_changed);
pixels_entry.changed.connect(on_pixels_changed);
pixels_entry.insert_text.connect(on_pixels_insert_text);
pixels_entry.activate.connect(on_activate);
// layout controls
add_label(_("_Format:"), 0, 0, format_combo);
add_control(format_combo, 1, 0);
add_label(_("_Quality:"), 0, 1, quality_combo);
add_control(quality_combo, 1, 1);
add_label(_("_Scaling constraint:"), 0, 2, constraint_combo);
add_control(constraint_combo, 1, 2);
add_label(_("_Pixels:"), 0, 3, pixels_entry);
add_control(pixels_entry, 1, 3);
export_metadata = new Gtk.Switch ();
add_label(_("Export _metadata:"), 0, 4, export_metadata);
add_control(export_metadata, 1, 4);
export_metadata.active = true;
export_metadata.halign = Gtk.Align.START;
table.set_row_spacing(6);
table.set_column_spacing(12);
table.set_border_width(18);
((Gtk.Box) get_content_area()).add(table);
// add buttons to action area
add_button(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
ok_button = add_button(Resources.OK_LABEL, Gtk.ResponseType.OK);
set_default_response(Gtk.ResponseType.OK);
ok_button.set_can_default(true);
ok_button.has_default = true;
set_default(ok_button);
if (current_constraint == ScaleConstraint.ORIGINAL) {
pixels_entry.sensitive = false;
quality_combo.sensitive = false;
}
ok_button.grab_focus();
}
private void format_add_option(string format_name) {
format_options.add(format_name);
format_combo.append_text(format_name);
}
private void format_set_active_text(string text) {
int selection_ticker = 0;
foreach (string current_text in format_options) {
if (current_text == text) {
format_combo.set_active(selection_ticker);
return;
}
selection_ticker++;
}
error("format_set_active_text: text '%s' isn't in combo box", text);
}
private PhotoFileFormat get_specified_format() {
int index = format_combo.get_active();
if (index < NUM_SPECIAL_FORMATS)
index = NUM_SPECIAL_FORMATS;
index -= NUM_SPECIAL_FORMATS;
PhotoFileFormat[] writeable_formats = PhotoFileFormat.get_writeable();
return writeable_formats[index];
}
private string get_label_for_parameters(ExportFormatParameters params) {
switch(params.mode) {
case ExportFormatMode.UNMODIFIED:
return UNMODIFIED_FORMAT_LABEL;
case ExportFormatMode.CURRENT:
return CURRENT_FORMAT_LABEL;
case ExportFormatMode.SPECIFIED:
return params.specified_format.get_properties().get_user_visible_name();
default:
error("get_label_for_parameters: unrecognized export format mode");
}
}
// unlike other parameters, which should be persisted across dialog executions, the
// format parameters must be set each time the dialog is executed -- this is why
// it's passed qualified as ref and not as out
public bool execute(out int scale, out ScaleConstraint constraint,
ref ExportFormatParameters parameters) {
show_all();
// if the export format mode isn't set to last (i.e., don't use the persisted settings),
// reset the scale constraint to original size
if (parameters.mode != ExportFormatMode.LAST) {
current_constraint = constraint = ScaleConstraint.ORIGINAL;
constraint_combo.set_active(0);
}
if (parameters.mode == ExportFormatMode.LAST)
parameters = current_parameters;
else if (parameters.mode == ExportFormatMode.SPECIFIED && !parameters.specified_format.can_write())
parameters.specified_format = PhotoFileFormat.get_system_default_format();
format_set_active_text(get_label_for_parameters(parameters));
on_format_changed();
bool ok = (run() == Gtk.ResponseType.OK);
if (ok) {
int index = constraint_combo.get_active();
assert(index >= 0);
constraint = CONSTRAINT_ARRAY[index];
current_constraint = constraint;
scale = int.parse(pixels_entry.get_text());
if (constraint != ScaleConstraint.ORIGINAL)
assert(scale > 0);
current_scale = scale;
parameters.export_metadata = export_metadata.sensitive ? export_metadata.active : false;
if (format_combo.get_active_text() == UNMODIFIED_FORMAT_LABEL) {
parameters.mode = current_parameters.mode = ExportFormatMode.UNMODIFIED;
} else if (format_combo.get_active_text() == CURRENT_FORMAT_LABEL) {
parameters.mode = current_parameters.mode = ExportFormatMode.CURRENT;
} else {
parameters.mode = current_parameters.mode = ExportFormatMode.SPECIFIED;
parameters.specified_format = current_parameters.specified_format = get_specified_format();
if (current_parameters.specified_format == PhotoFileFormat.JFIF)
parameters.quality = current_parameters.quality = QUALITY_ARRAY[quality_combo.get_active()];
}
//save current settings in config backend for reusing later
Config.Facade config = Config.Facade.get_instance();
config.set_export_export_format_mode(current_parameters.mode); //ExportFormatMode
config.set_export_photo_file_format(current_parameters.specified_format); //PhotoFileFormat
config.set_export_quality(current_parameters.quality); //quality
config.set_export_export_metadata(current_parameters.export_metadata); //export metadata
config.set_export_constraint(current_constraint); //constraint
config.set_export_scale(current_scale); //scale
} else {
scale = 0;
constraint = ScaleConstraint.ORIGINAL;
}
destroy();
return ok;
}
private void add_label(string text, int x, int y, Gtk.Widget? widget = null) {
Gtk.Label new_label = new Gtk.Label.with_mnemonic(text);
new_label.halign = Gtk.Align.END;
new_label.valign = Gtk.Align.CENTER;
new_label.set_use_underline(true);
if (widget != null)
new_label.set_mnemonic_widget(widget);
table.attach(new_label, x, y, 1, 1);
}
private void add_control(Gtk.Widget widget, int x, int y) {
widget.halign = Gtk.Align.FILL;
widget.valign = Gtk.Align.CENTER;
widget.hexpand = true;
widget.vexpand = true;
table.attach(widget, x, y, 1, 1);
}
private void on_constraint_changed() {
bool original = CONSTRAINT_ARRAY[constraint_combo.get_active()] == ScaleConstraint.ORIGINAL;
bool jpeg = format_combo.get_active_text() ==
PhotoFileFormat.JFIF.get_properties().get_user_visible_name();
pixels_entry.sensitive = !original;
quality_combo.sensitive = !original && jpeg;
if (original)
ok_button.sensitive = true;
else
on_pixels_changed();
}
private void on_format_changed() {
bool original = CONSTRAINT_ARRAY[constraint_combo.get_active()] == ScaleConstraint.ORIGINAL;
if (format_combo.get_active_text() == UNMODIFIED_FORMAT_LABEL) {
// if the user wishes to export the media unmodified, then we just copy the original
// files, so parameterizing size, quality, etc. is impossible -- these are all
// just as they are in the original file. In this case, we set the scale constraint to
// original and lock out all the controls
constraint_combo.set_active(0); /* 0 == original size */
constraint_combo.set_sensitive(false);
quality_combo.set_sensitive(false);
pixels_entry.sensitive = false;
export_metadata.active = false;
export_metadata.sensitive = false;
} else if (format_combo.get_active_text() == CURRENT_FORMAT_LABEL) {
// if the user wishes to export the media in its current format, we allow sizing but
// not JPEG quality customization, because in a batch of many photos, it's not
// guaranteed that all of them will be JPEGs or RAWs that get converted to JPEGs. Some
// could be PNGs, and PNG has no notion of quality. So lock out the quality control.
// If the user wants to set JPEG quality, he or she can explicitly specify the JPEG
// format.
constraint_combo.set_sensitive(true);
quality_combo.set_sensitive(false);
pixels_entry.sensitive = !original;
export_metadata.sensitive = true;
} else {
// if the user has chosen a specific format, then allow JPEG quality customization if
// the format is JPEG and the user is re-sizing the image, otherwise, disallow JPEG
// quality customization; always allow scaling.
constraint_combo.set_sensitive(true);
bool jpeg = get_specified_format() == PhotoFileFormat.JFIF;
quality_combo.sensitive = !original && jpeg;
export_metadata.sensitive = true;
}
}
private void on_activate() {
response(Gtk.ResponseType.OK);
}
private void on_pixels_changed() {
ok_button.sensitive = (pixels_entry.get_text_length() > 0) && (int.parse(pixels_entry.get_text()) > 0);
}
private void on_pixels_insert_text(string text, int length, ref int position) {
// This is necessary because SignalHandler.block_by_func() is not properly bound
if (in_insert)
return;
in_insert = true;
if (length == -1)
length = (int) text.length;
// only permit numeric text
string new_text = "";
for (int ctr = 0; ctr < length; ctr++) {
if (text[ctr].isdigit()) {
new_text += ((char) text[ctr]).to_string();
}
}
if (new_text.length > 0)
pixels_entry.insert_text(new_text, (int) new_text.length, ref position);
Signal.stop_emission_by_name(pixels_entry, "insert-text");
in_insert = false;
}
}
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) {
Gtk.Builder builder = AppWindow.create_builder();
dialog = new TextEntryDialog();
dialog.get_content_area().add((Gtk.Box) builder.get_object("dialog-vbox2"));
dialog.set_builder(builder);
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) {
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.
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);
}
// Entry completion for values separated by separators (e.g. comma in the case of tags)
// Partly inspired by the class of the same name in gtkmm-utils by Marko Anastasov
public class EntryMultiCompletion : Gtk.EntryCompletion {
private string delimiter;
public EntryMultiCompletion(Gee.Collection completion_list, string? delimiter) {
assert(delimiter == null || delimiter.length == 1);
this.delimiter = delimiter;
set_model(create_completion_store(completion_list));
set_text_column(0);
set_match_func(match_func);
}
private static Gtk.ListStore create_completion_store(Gee.Collection completion_list) {
Gtk.ListStore completion_store = new Gtk.ListStore(1, typeof(string));
Gtk.TreeIter store_iter;
Gee.Iterator completion_iter = completion_list.iterator();
while (completion_iter.next()) {
completion_store.append(out store_iter);
completion_store.set(store_iter, 0, completion_iter.get(), -1);
}
return completion_store;
}
private bool match_func(Gtk.EntryCompletion completion, string key, Gtk.TreeIter iter) {
Gtk.TreeModel model = completion.get_model();
string possible_match;
model.get(iter, 0, out possible_match);
// Normalize key and possible matches to allow comparison of non-ASCII characters.
// Use a "COMPOSE" normalization to allow comparison to the position value returned by
// Gtk.Entry, i.e. one character=one position. Using the default normalization a character
// like "é" or "ö" would have a length of two.
possible_match = possible_match.casefold().normalize(-1, NormalizeMode.ALL_COMPOSE);
string normed_key = key.normalize(-1, NormalizeMode.ALL_COMPOSE);
if (delimiter == null) {
return possible_match.has_prefix(normed_key.strip());
} else {
if (normed_key.contains(delimiter)) {
// check whether cursor is before last delimiter
int offset = normed_key.char_count(normed_key.last_index_of_char(delimiter[0]));
int position = ((Gtk.Entry) get_entry()).get_position();
if (position <= offset)
return false; // TODO: Autocompletion for tags not last in list
}
string last_part = get_last_part(normed_key.strip(), delimiter);
if (last_part.length == 0)
return false; // need at least one character to show matches
return possible_match.has_prefix(last_part.strip());
}
}
public override bool match_selected(Gtk.TreeModel model, Gtk.TreeIter iter) {
string match;
model.get(iter, 0, out match);
Gtk.Entry entry = (Gtk.Entry)get_entry();
string old_text = entry.get_text().normalize(-1, NormalizeMode.ALL_COMPOSE);
if (old_text.length > 0) {
if (old_text.contains(delimiter)) {
old_text = old_text.substring(0, old_text.last_index_of_char(delimiter[0]) + 1) + (delimiter != " " ? " " : "");
} else
old_text = "";
}
string new_text = old_text + match + delimiter + (delimiter != " " ? " " : "");
entry.set_text(new_text);
entry.set_position((int) new_text.length);
return true;
}
// Find last string after any delimiter
private static string get_last_part(string s, string delimiter) {
string[] split = s.split(delimiter);
if((split != null) && (split[0] != null)) {
return split[split.length - 1];
} else {
return "";
}
}
}
public abstract class SetBackgroundDialog {
protected Gtk.Dialog dialog;
protected Gtk.CheckButton desktop_background_button;
protected Gtk.CheckButton screensaver_button;
protected Gtk.Button ok_button;
// the checkbuttons themselves are initialized to these values
protected bool desktop = true;
protected bool screensaver = false;
public SetBackgroundDialog(Gtk.Builder builder) {
dialog = builder.get_object("dialog1") as Gtk.Dialog;
dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG);
dialog.set_parent_window(AppWindow.get_instance().get_parent_window());
dialog.set_transient_for(AppWindow.get_instance());
dialog.set_default_response(Gtk.ResponseType.OK);
desktop_background_button = builder.get_object("desktop_background_checkbox") as Gtk.CheckButton;
desktop_background_button.active = desktop;
desktop_background_button.toggled.connect(on_checkbox_clicked);
screensaver_button = builder.get_object("screensaver_checkbox") as Gtk.CheckButton;
screensaver_button.active = screensaver;
screensaver_button.toggled.connect(on_checkbox_clicked);
ok_button = builder.get_object("ok_button") as Gtk.Button;
}
protected void on_checkbox_clicked() {
desktop = desktop_background_button.active;
screensaver = screensaver_button.active;
if (!desktop && !screensaver) {
ok_button.sensitive = false;
} else {
ok_button.sensitive = true;
}
}
protected bool execute_base() {
dialog.show_all();
bool result = dialog.run() == Gtk.ResponseType.OK;
dialog.destroy();
return result;
}
}
public class SetBackgroundPhotoDialog : SetBackgroundDialog {
public SetBackgroundPhotoDialog() {
Gtk.Builder builder = AppWindow.create_builder("set_background_dialog.ui", this);
base(builder);
}
public bool execute(out bool desktop_background, out bool screensaver) {
bool result = execute_base();
desktop_background = this.desktop;
screensaver = this.screensaver;
return result;
}
}
public class SetBackgroundSlideshowDialog : SetBackgroundDialog {
private Gtk.Label delay_value_label;
private Gtk.Scale delay_scale;
private int delay_value = 0;
public SetBackgroundSlideshowDialog() {
Gtk.Builder builder = AppWindow.create_builder("set_background_slideshow_dialog.ui", this);
base(builder);
delay_value_label = builder.get_object("delay_value_label") as Gtk.Label;
delay_scale = builder.get_object("delay_scale") as Gtk.Scale;
delay_scale.value_changed.connect(on_delay_scale_value_changed);
delay_scale.adjustment.value = 50;
}
private void on_delay_scale_value_changed() {
double value = delay_scale.adjustment.value;
// f(x)=x^5 allows to have fine-grained values (seconds) to the left
// and very coarse-grained values (hours) to the right of the slider.
// We limit maximum value to 1 day and minimum to 5 seconds.
delay_value = (int) (Math.pow(value, 5) / Math.pow(90, 5) * 60 * 60 * 24 + 5);
// convert to text and remove fractions from values > 1 minute
string text;
if (delay_value < 60) {
text = ngettext("%d second", "%d seconds", delay_value).printf(delay_value);
} else if (delay_value < 60 * 60) {
int minutes = delay_value / 60;
text = ngettext("%d minute", "%d minutes", minutes).printf(minutes);
delay_value = minutes * 60;
} else if (delay_value < 60 * 60 * 24) {
int hours = delay_value / (60 * 60);
text = ngettext("%d hour", "%d hours", hours).printf(hours);
delay_value = hours * (60 * 60);
} else {
text = _("1 day");
delay_value = 60 * 60 * 24;
}
delay_value_label.label = text;
}
public bool execute(out int delay_value, out bool desktop_background, out bool screensaver) {
bool result = execute_base();
delay_value = this.delay_value;
desktop_background = this.desktop;
screensaver = this.screensaver;
return result;
}
}
public class TextEntryDialog : Gtk.Dialog {
public delegate bool OnModifyValidateType(string text);
private unowned OnModifyValidateType on_modify_validate;
private Gtk.Entry entry;
private Gtk.Builder builder;
private Gtk.Button button1;
private Gtk.Button button2;
public TextEntryDialog() {
bool use_header;
Gtk.Settings.get_default ().get ("gtk-dialogs-use-header", out use_header);
Object (use_header_bar: use_header ? 1 : 0);
}
public void set_builder(Gtk.Builder builder) {
this.builder = builder;
}
public void setup(OnModifyValidateType? modify_validate, string title, string label,
string? initial_text, Gee.Collection? completion_list, string? completion_delimiter) {
set_title(title);
set_resizable(true);
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("label") as Gtk.Label;
name_label.set_text(label);
entry = builder.get_object("entry") as Gtk.Entry;
entry.set_text(initial_text != null ? initial_text : "");
entry.grab_focus();
entry.changed.connect(on_entry_changed);
button1 = (Gtk.Button) add_button(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
button2 = (Gtk.Button) add_button(Resources.SAVE_LABEL, Gtk.ResponseType.OK);
set_default_response(Gtk.ResponseType.OK);
if (completion_list != null) { // Textfield with autocompletion
EntryMultiCompletion completion = new EntryMultiCompletion(completion_list,
completion_delimiter);
entry.set_completion(completion);
}
set_default_response(Gtk.ResponseType.OK);
}
public string? execute() {
string? text = null;
// validate entry to start with
set_response_sensitive(Gtk.ResponseType.OK, on_modify_validate(entry.get_text()));
show_all();
if (run() == Gtk.ResponseType.OK)
text = entry.get_text();
entry.changed.disconnect(on_entry_changed);
destroy();
return text;
}
public void on_entry_changed() {
set_response_sensitive(Gtk.ResponseType.OK, on_modify_validate(entry.get_text()));
}
}
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;
public MultiTextEntryDialog() {
bool use_header;
Gtk.Settings.get_default ().get ("gtk-dialogs-use-header", out use_header);
Object (use_header_bar: use_header ? 1 : 0);
}
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;
entry = builder.get_object("textview1") as Gtk.TextView;
entry.set_wrap_mode (Gtk.WrapMode.WORD);
entry.buffer = new Gtk.TextBuffer(null);
entry.buffer.text = (initial_text != null ? initial_text : "");
entry.grab_focus();
button1 = (Gtk.Button) add_button(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
button2 = (Gtk.Button) add_button(Resources.SAVE_LABEL, Gtk.ResponseType.OK);
set_default_response(Gtk.ResponseType.OK);
}
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);
}
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 class ProgressDialog : Gtk.Window {
private Gtk.ProgressBar progress_bar = new Gtk.ProgressBar();
private Gtk.Button cancel_button = null;
private Cancellable cancellable;
private uint64 last_count = uint64.MAX;
private int update_every = 1;
private int minimum_on_screen_time_msec = 500;
private ulong time_started;
#if UNITY_SUPPORT
UnityProgressBar uniprobar = UnityProgressBar.get_instance();
#endif
public ProgressDialog(Gtk.Window? owner, string text, Cancellable? cancellable = null) {
this.cancellable = cancellable;
set_title(text);
set_resizable(false);
if (owner != null)
set_transient_for(owner);
set_modal(true);
set_type_hint(Gdk.WindowTypeHint.DIALOG);
progress_bar.set_size_request(300, -1);
progress_bar.set_show_text(true);
Gtk.Box vbox_bar = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
vbox_bar.pack_start(progress_bar, true, false, 0);
if (cancellable != null) {
cancel_button = new Gtk.Button.with_mnemonic(Resources.CANCEL_LABEL);
cancel_button.clicked.connect(on_cancel);
delete_event.connect(on_window_closed);
}
Gtk.Box hbox = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
hbox.pack_start(vbox_bar, true, false, 0);
if (cancel_button != null)
hbox.pack_end(cancel_button, false, false, 0);
Gtk.Label primary_text_label = new Gtk.Label("");
primary_text_label.set_markup("%s".printf(text));
primary_text_label.xalign = 0.0f;
primary_text_label.yalign = 0.5f;
Gtk.Box vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 12);
vbox.pack_start(primary_text_label, false, false, 0);
vbox.pack_start(hbox, true, false, 0);
vbox.halign = Gtk.Align.CENTER;
vbox.valign = Gtk.Align.CENTER;
vbox.hexpand = true;
vbox.vexpand = true;
vbox.margin_start = 12;
vbox.margin_end = 12;
vbox.margin_top = 12;
vbox.margin_bottom = 12;
add(vbox);
time_started = now_ms();
}
public override void realize() {
base.realize();
// if unable to cancel the progress bar, remove the close button
if (cancellable == null)
get_window().set_functions(Gdk.WMFunction.MOVE);
}
public void update_display_every(int update_every) {
assert(update_every >= 1);
this.update_every = update_every;
}
public void set_minimum_on_screen_time_msec(int minimum_on_screen_time_msec) {
this.minimum_on_screen_time_msec = minimum_on_screen_time_msec;
}
public void set_fraction(int current, int total) {
set_percentage((double) current / (double) total);
}
public void set_percentage(double pct) {
pct = pct.clamp(0.0, 1.0);
maybe_show_all(pct);
progress_bar.set_fraction(pct);
progress_bar.set_text(_("%d%%").printf((int) (pct * 100.0)));
#if UNITY_SUPPORT
//UnityProgressBar: set progress
uniprobar.set_progress(pct);
#endif
}
public void set_status(string text) {
progress_bar.set_text(text);
#if UNITY_SUPPORT
//UnityProgressBar: try to draw progress bar
uniprobar.set_visible(true);
#endif
show_all();
}
// This can be used as a ProgressMonitor delegate.
public bool monitor(uint64 count, uint64 total, bool do_event_loop = true) {
if ((last_count == uint64.MAX) || (count - last_count) >= update_every) {
set_percentage((double) count / (double) total);
last_count = count;
}
bool keep_going = (cancellable != null) ? !cancellable.is_cancelled() : true;
// TODO: get rid of this. non-trivial, as some progress-monitor operations are blocking
// and need to allow the event loop to spin
//
// Important: Since it's possible the progress dialog might be destroyed inside this call,
// avoid referring to "this" afterwards at all costs (in case all refs have been dropped)
if (do_event_loop)
spin_event_loop();
return keep_going;
}
public new void close() {
#if UNITY_SUPPORT
//UnityProgressBar: reset
uniprobar.reset();
#endif
hide();
destroy();
}
private bool on_window_closed() {
on_cancel();
return false; // return false so that the system handler will remove the window from
// the screen
}
private void on_cancel() {
if (cancellable != null)
cancellable.cancel();
cancel_button.sensitive = false;
}
private void maybe_show_all(double pct) {
// Appear only after a while because some jobs may take only a
// fraction of second to complete so there's no point in showing progress.
if (!this.visible && now_ms() - time_started > minimum_on_screen_time_msec) {
// calculate percents completed in one ms
double pps = pct * 100.0 / minimum_on_screen_time_msec;
// calculate [very rough] estimate of time to complete in ms
double ttc = 100.0 / pps;
// If there is still more work to do for at least MINIMUM_ON_SCREEN_TIME_MSEC,
// finally display the dialog.
if (ttc > minimum_on_screen_time_msec) {
#if UNITY_SUPPORT
//UnityProgressBar: try to draw progress bar
uniprobar.set_visible(true);
#endif
show_all();
spin_event_loop();
}
}
}
}
public class AdjustDateTimeDialog : Gtk.Dialog {
private const int64 SECONDS_IN_DAY = 60 * 60 * 24;
private const int64 SECONDS_IN_HOUR = 60 * 60;
private const int64 SECONDS_IN_MINUTE = 60;
private const int YEAR_OFFSET = 1900;
private bool no_original_time = false;
private const int CALENDAR_THUMBNAIL_SCALE = 1;
time_t original_time;
Gtk.Label original_time_label;
Gtk.Calendar calendar;
Gtk.SpinButton hour;
Gtk.SpinButton minute;
Gtk.SpinButton second;
Gtk.ComboBoxText system;
Gtk.RadioButton relativity_radio_button;
Gtk.RadioButton batch_radio_button;
Gtk.CheckButton modify_originals_check_button;
Gtk.Label notification;
private enum TimeSystem {
AM,
PM,
24HR;
}
TimeSystem previous_time_system;
public AdjustDateTimeDialog(Dateable source, int photo_count, bool display_options = true,
bool contains_video = false, bool only_video = false) {
assert(source != null);
bool use_header;
Gtk.Settings.get_default ().get ("gtk-dialogs-use-header", out use_header);
Object(use_header_bar: use_header ? 1 : 0);
set_modal(true);
set_resizable(false);
set_transient_for(AppWindow.get_instance());
add_buttons(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL,
Resources.OK_LABEL, Gtk.ResponseType.OK);
set_title(Resources.ADJUST_DATE_TIME_LABEL);
calendar = new Gtk.Calendar();
calendar.day_selected.connect(on_time_changed);
calendar.month_changed.connect(on_time_changed);
calendar.next_year.connect(on_time_changed);
calendar.prev_year.connect(on_time_changed);
if (Config.Facade.get_instance().get_use_24_hour_time())
hour = new Gtk.SpinButton.with_range(0, 23, 1);
else
hour = new Gtk.SpinButton.with_range(1, 12, 1);
hour.output.connect(on_spin_button_output);
hour.set_width_chars(2);
hour.set_max_width_chars(2);
minute = new Gtk.SpinButton.with_range(0, 59, 1);
minute.set_width_chars(2);
minute.set_max_width_chars(2);
minute.output.connect(on_spin_button_output);
second = new Gtk.SpinButton.with_range(0, 59, 1);
second.set_width_chars(2);
second.set_max_width_chars(2);
second.output.connect(on_spin_button_output);
system = new Gtk.ComboBoxText();
system.append_text(_("AM"));
system.append_text(_("PM"));
system.append_text(_("24 Hr"));
system.changed.connect(on_time_system_changed);
Gtk.Box clock = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
clock.pack_start(hour, false, false, 0);
clock.pack_start(new Gtk.Label(":"), false, false, 0); // internationalize?
clock.pack_start(minute, false, false, 0);
clock.pack_start(new Gtk.Label(":"), false, false, 0);
clock.pack_start(second, false, false, 0);
clock.pack_start(system, false, false, 0);
set_default_response(Gtk.ResponseType.OK);
relativity_radio_button = new Gtk.RadioButton.with_mnemonic(null,
_("_Shift photos/videos by the same amount"));
relativity_radio_button.set_active(Config.Facade.get_instance().get_keep_relativity());
relativity_radio_button.sensitive = display_options && photo_count > 1;
batch_radio_button = new Gtk.RadioButton.with_mnemonic(relativity_radio_button.get_group(),
_("Set _all photos/videos to this time"));
batch_radio_button.set_active(!Config.Facade.get_instance().get_keep_relativity());
batch_radio_button.sensitive = display_options && photo_count > 1;
batch_radio_button.toggled.connect(on_time_changed);
if (contains_video) {
var text = ngettext ("_Modify original photo file", "_Modify original photo files",
photo_count);
modify_originals_check_button = new Gtk.CheckButton.with_mnemonic(text);
} else {
var text = ngettext ("_Modify original file", "_Modify original files", photo_count);
modify_originals_check_button = new Gtk.CheckButton.with_mnemonic(text);
}
modify_originals_check_button.set_active(Config.Facade.get_instance().get_commit_metadata_to_masters() &&
display_options);
modify_originals_check_button.sensitive = (!only_video) &&
(!Config.Facade.get_instance().get_commit_metadata_to_masters() && display_options);
Gtk.Box time_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 5);
time_content.pack_start(calendar, true, false, 0);
time_content.pack_start(clock, true, false, 0);
if (display_options) {
time_content.pack_start(relativity_radio_button, true, false, 0);
time_content.pack_start(batch_radio_button, true, false, 0);
time_content.pack_start(modify_originals_check_button, true, false, 0);
}
Gdk.Pixbuf preview = null;
try {
// Instead of calling get_pixbuf() here, we use the thumbnail instead;
// this was needed for Videos, since they don't support get_pixbuf().
preview = source.get_thumbnail(CALENDAR_THUMBNAIL_SCALE);
} catch (Error err) {
warning("Unable to fetch preview for %s", source.to_string());
}
Gtk.Box image_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
image_content.set_valign(Gtk.Align.START);
image_content.set_homogeneous(true);
Gtk.Image image = (preview != null) ? new Gtk.Image.from_pixbuf(preview) : new Gtk.Image();
original_time_label = new Gtk.Label(null);
image_content.pack_start(image, true, false, 0);
image_content.pack_start(original_time_label, true, false, 0);
Gtk.Box hbox = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 10);
hbox.set_border_width(3);
hbox.pack_start(image_content, true, false, 0);
hbox.pack_start(time_content, true, false, 0);
hbox.halign = Gtk.Align.CENTER;
hbox.valign = Gtk.Align.CENTER;
hbox.hexpand = false;
hbox.vexpand = false;
((Gtk.Box) get_content_area()).pack_start(hbox, true, false, 0);
notification = new Gtk.Label("");
notification.set_line_wrap(true);
notification.set_justify(Gtk.Justification.CENTER);
((Gtk.Box) get_content_area()).pack_start(notification, true, true, 0);
original_time = source.get_exposure_time();
if (original_time == 0) {
original_time = time_t();
no_original_time = true;
}
set_time(Time.local(original_time));
set_original_time_label(Config.Facade.get_instance().get_use_24_hour_time());
}
private void set_time(Time time) {
calendar.select_month(time.month, time.year + YEAR_OFFSET);
calendar.select_day(time.day);
if (Config.Facade.get_instance().get_use_24_hour_time()) {
system.set_active(TimeSystem.24HR);
hour.set_value(time.hour);
} else {
int AMPM_hour = time.hour % 12;
hour.set_value((AMPM_hour == 0) ? 12 : AMPM_hour);
system.set_active((time.hour >= 12) ? TimeSystem.PM : TimeSystem.AM);
}
minute.set_value(time.minute);
second.set_value(time.second);
previous_time_system = (TimeSystem) system.get_active();
}
private void set_original_time_label(bool use_24_hr_format) {
if (no_original_time)
return;
original_time_label.set_text(_("Original: ") +
Time.local(original_time).format(use_24_hr_format ? _("%m/%d/%Y, %H:%M:%S") :
_("%m/%d/%Y, %I:%M:%S %p")));
}
private time_t get_time() {
Time time = Time();
time.second = (int) second.get_value();
time.minute = (int) minute.get_value();
// convert to 24 hr
int hour = (int) hour.get_value();
time.hour = (hour == 12 && system.get_active() != TimeSystem.24HR) ? 0 : hour;
time.hour += ((system.get_active() == TimeSystem.PM) ? 12 : 0);
uint year, month, day;
calendar.get_date(out year, out month, out day);
time.year = ((int) year) - YEAR_OFFSET;
time.month = (int) month;
time.day = (int) day;
time.isdst = -1;
return time.mktime();
}
public bool execute(out int64 time_shift, out bool keep_relativity,
out bool modify_originals) {
show_all();
bool response = false;
if (run() == Gtk.ResponseType.OK) {
if (no_original_time)
time_shift = (int64) get_time();
else
time_shift = (int64) (get_time() - original_time);
keep_relativity = relativity_radio_button.get_active();
if (relativity_radio_button.sensitive)
Config.Facade.get_instance().set_keep_relativity(keep_relativity);
modify_originals = modify_originals_check_button.get_active();
if (modify_originals_check_button.sensitive)
Config.Facade.get_instance().set_modify_originals(modify_originals);
response = true;
} else {
time_shift = 0;
keep_relativity = true;
modify_originals = false;
}
destroy();
return response;
}
private bool on_spin_button_output(Gtk.SpinButton button) {
button.set_text("%02d".printf((int) button.get_value()));
on_time_changed();
return true;
}
private void on_time_changed() {
int64 time_shift = ((int64) get_time() - (int64) original_time);
previous_time_system = (TimeSystem) system.get_active();
if (time_shift == 0 || no_original_time || (batch_radio_button.get_active() &&
batch_radio_button.sensitive)) {
notification.hide();
} else {
bool forward = time_shift > 0;
int days, hours, minutes, seconds;
time_shift = time_shift.abs();
days = (int) (time_shift / SECONDS_IN_DAY);
time_shift = time_shift % SECONDS_IN_DAY;
hours = (int) (time_shift / SECONDS_IN_HOUR);
time_shift = time_shift % SECONDS_IN_HOUR;
minutes = (int) (time_shift / SECONDS_IN_MINUTE);
seconds = (int) (time_shift % SECONDS_IN_MINUTE);
string shift_status = (forward) ?
_("Exposure time will be shifted forward by\n%d %s, %d %s, %d %s, and %d %s.") :
_("Exposure time will be shifted backward by\n%d %s, %d %s, %d %s, and %d %s.");
notification.set_text(shift_status.printf(days, ngettext("day", "days", days),
hours, ngettext("hour", "hours", hours), minutes,
ngettext("minute", "minutes", minutes), seconds,
ngettext("second", "seconds", seconds)));
notification.show();
}
}
private void on_time_system_changed() {
if (previous_time_system == system.get_active())
return;
Config.Facade.get_instance().set_use_24_hour_time(system.get_active() == TimeSystem.24HR);
if (system.get_active() == TimeSystem.24HR) {
int time = (hour.get_value() == 12.0) ? 0 : (int) hour.get_value();
time = time + ((previous_time_system == TimeSystem.PM) ? 12 : 0);
hour.set_range(0, 23);
set_original_time_label(true);
hour.set_value(time);
} else {
int AMPM_hour = ((int) hour.get_value()) % 12;
hour.set_range(1, 12);
set_original_time_label(false);
hour.set_value((AMPM_hour == 0) ? 12 : AMPM_hour);
}
on_time_changed();
}
}
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));
}
}
public interface WelcomeServiceEntry : GLib.Object {
public abstract string get_service_name();
public abstract void execute();
}
public class WelcomeDialog : Gtk.Dialog {
Gtk.CheckButton hide_button;
Gtk.CheckButton? system_pictures_import_check = null;
Gtk.CheckButton[] external_import_checks = new Gtk.CheckButton[0];
WelcomeServiceEntry[] external_import_entries = new WelcomeServiceEntry[0];
Gtk.Label secondary_text;
Gtk.Label instruction_header;
Gtk.Box import_content;
Gtk.Box import_action_checkbox_packer;
Gtk.Box external_import_action_checkbox_packer;
Spit.DataImports.WelcomeImportMetaHost import_meta_host;
bool import_content_already_installed = false;
bool ok_clicked = false;
public WelcomeDialog(Gtk.Window owner) {
import_meta_host = new Spit.DataImports.WelcomeImportMetaHost(this);
bool show_system_pictures_import = is_system_pictures_import_possible();
Gtk.Widget ok_button = add_button(Resources.OK_LABEL, Gtk.ResponseType.OK);
set_title(_("Welcome!"));
set_resizable(false);
set_type_hint(Gdk.WindowTypeHint.DIALOG);
set_transient_for(owner);
Gtk.Label primary_text = new Gtk.Label("");
primary_text.set_markup(
"%s".printf(_("Welcome to Shotwell!")));
primary_text.xalign = 0.0f;
primary_text.yalign = 0.5f;
secondary_text = new Gtk.Label("");
secondary_text.set_markup("%s".printf(
_("To get started, import photos in any of these ways:")));
secondary_text.xalign = 0.0f;
secondary_text.yalign = 0.5f;
var image = new Gtk.Image.from_icon_name ("shotwell", Gtk.IconSize.DIALOG);
Gtk.Box header_text = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
header_text.pack_start(primary_text, false, false, 5);
header_text.pack_start(secondary_text, false, false, 0);
Gtk.Box header_content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
header_content.pack_start(image, false, false, 0);
header_content.pack_start(header_text, false, false, 0);
Gtk.Label instructions = new Gtk.Label("");
string indent_prefix = " "; // we can't tell what the indent prefix is going to be so assume we need one
string arrow_glyph = (get_direction() == Gtk.TextDirection.RTL) ? "◂" : "▸";
instructions.set_markup(((indent_prefix + "• %s\n") + (indent_prefix + "• %s\n")
+ (indent_prefix + "• %s")).printf(
_("Choose File %s Import From Folder").printf(arrow_glyph),
_("Drag and drop photos onto the Shotwell window"),
_("Connect a camera to your computer and import")));
instructions.xalign = 0.0f;
instructions.yalign = 0.5f;
import_action_checkbox_packer = new Gtk.Box(Gtk.Orientation.VERTICAL, 2);
external_import_action_checkbox_packer = new Gtk.Box(Gtk.Orientation.VERTICAL, 2);
import_action_checkbox_packer.add(external_import_action_checkbox_packer);
if (show_system_pictures_import) {
system_pictures_import_check = new Gtk.CheckButton.with_mnemonic(
_("_Import photos from your %s folder").printf(
get_display_pathname(AppDirs.get_import_dir())));
import_action_checkbox_packer.add(system_pictures_import_check);
system_pictures_import_check.set_active(true);
}
instruction_header = new Gtk.Label(
_("You can also import photos in any of these ways:"));
instruction_header.xalign = 0.0f;
instruction_header.yalign = 0.5f;
instruction_header.set_margin_top(20);
Gtk.Box content = new Gtk.Box(Gtk.Orientation.VERTICAL, 16);
content.pack_start(header_content, true, true, 0);
import_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 2);
content.add(import_content);
content.pack_start(instructions, false, false, 0);
hide_button = new Gtk.CheckButton.with_mnemonic(_("_Don’t show this message again"));
hide_button.set_active(true);
content.pack_start(hide_button, false, false, 6);
content.halign = Gtk.Align.FILL;
content.valign = Gtk.Align.FILL;
content.hexpand = false;
content.vexpand = false;
content.margin_top = 12;
content.margin_bottom = 0;
content.margin_start = 12;
content.margin_end = 12;
((Gtk.Box) get_content_area()).pack_start(content, false, false, 0);
ok_button.grab_focus();
install_import_content();
import_meta_host.start();
}
private void install_import_content() {
if (
(external_import_checks.length > 0 || system_pictures_import_check != null) &&
(import_content_already_installed == false)
) {
secondary_text.set_markup("");
import_content.add(import_action_checkbox_packer);
import_content.add(instruction_header);
import_content_already_installed = true;
}
}
public void install_service_entry(WelcomeServiceEntry entry) {
debug("WelcomeDialog: Installing service entry for %s".printf(entry.get_service_name()));
external_import_entries += entry;
Gtk.CheckButton entry_check = new Gtk.CheckButton.with_label(
_("Import photos from your %s library").printf(entry.get_service_name()));
external_import_checks += entry_check;
entry_check.set_active(true);
external_import_action_checkbox_packer.add(entry_check);
install_import_content();
}
/**
* Connected to the 'response' signal. This is part of a workaround
* for the fact that run()-ning this dialog can interfere with displaying
* images from a camera; please see #4997 for details.
*/
private void on_dismiss(int resp) {
if (resp == Gtk.ResponseType.OK) {
ok_clicked = true;
}
hide();
Gtk.main_quit();
}
public bool execute(out WelcomeServiceEntry[] selected_import_entries, out bool do_system_pictures_import) {
// it's unsafe to call run() here - it interferes with displaying
// images from a camera - so we process the dialog ourselves.
response.connect(on_dismiss);
show_all();
show();
// this will block the thread we're in until a matching call
// to main_quit() is encountered; this happens when either the window
// is closed or OK is clicked.
Gtk.main();
// at this point, the inner main loop will have been exited.
// we've got the response, so we don't need this signal anymore.
response.disconnect(on_dismiss);
bool ok = ok_clicked;
bool show_dialog = true;
if (ok)
show_dialog = !hide_button.get_active();
// Use a temporary variable as += cannot be used on parameters
WelcomeServiceEntry[] result = new WelcomeServiceEntry[0];
for (int i = 0; i < external_import_entries.length; i++) {
if (external_import_checks[i].get_active() == true)
result += external_import_entries[i];
}
selected_import_entries = result;
do_system_pictures_import =
(system_pictures_import_check != null) ? system_pictures_import_check.get_active() : false;
destroy();
return show_dialog;
}
private static bool is_system_pictures_import_possible() {
File system_pictures = AppDirs.get_import_dir();
if (!system_pictures.query_exists(null))
return false;
if (!(system_pictures.query_file_type(FileQueryInfoFlags.NONE, null) == FileType.DIRECTORY))
return false;
try {
FileEnumerator syspics_child_enum = system_pictures.enumerate_children("standard::*",
FileQueryInfoFlags.NONE, null);
return (syspics_child_enum.next_file(null) != null);
} catch (Error e) {
return false;
}
}
}
public class PreferencesDialog {
private class PathFormat {
public PathFormat(string name, string? pattern) {
this.name = name;
this.pattern = pattern;
}
public string name;
public string? pattern;
}
private static PreferencesDialog preferences_dialog;
private Gtk.Dialog dialog;
private Gtk.Builder builder;
private Gtk.Adjustment bg_color_adjustment;
private Gtk.Scale bg_color_slider;
private Gtk.ComboBox photo_editor_combo;
private Gtk.ComboBox raw_editor_combo;
private SortedList external_raw_apps;
private SortedList external_photo_apps;
private Gtk.FileChooserButton library_dir_button;
private Gtk.ComboBoxText dir_pattern_combo;
private Gtk.Entry dir_pattern_entry;
private Gtk.Label dir_pattern_example;
private bool allow_closing = false;
private string? lib_dir = null;
private Gee.ArrayList path_formats = new Gee.ArrayList();
private GLib.DateTime example_date = new GLib.DateTime.local(2009, 3, 10, 18, 16, 11);
private Gtk.CheckButton lowercase;
private Plugins.ManifestWidgetMediator plugins_mediator = new Plugins.ManifestWidgetMediator();
private Gtk.ComboBoxText default_raw_developer_combo;
private PreferencesDialog() {
builder = AppWindow.create_builder();
dialog = builder.get_object("preferences_dialog") as Gtk.Dialog;
bool use_header;
Gtk.Settings.get_default ().get ("gtk-dialogs-use-header", out use_header);
if (!use_header) {
Gtk.Widget null_titlebar = null;
dialog.set_titlebar (null_titlebar);
}
dialog.set_parent_window(AppWindow.get_instance().get_parent_window());
dialog.set_transient_for(AppWindow.get_instance());
dialog.delete_event.connect(on_delete);
dialog.response.connect(on_close);
bg_color_adjustment = builder.get_object("bg_color_adjustment") as Gtk.Adjustment;
bg_color_adjustment.set_value(bg_color_adjustment.get_upper() -
(Config.Facade.get_instance().get_bg_color().red * 65535.0));
bg_color_adjustment.value_changed.connect(on_value_changed);
bg_color_slider = builder.get_object("bg_color_slider") as Gtk.Scale;
bg_color_slider.button_press_event.connect(on_bg_color_reset);
library_dir_button = builder.get_object("library_dir_button") as Gtk.FileChooserButton;
photo_editor_combo = builder.get_object("external_photo_editor_combo") as Gtk.ComboBox;
raw_editor_combo = builder.get_object("external_raw_editor_combo") as Gtk.ComboBox;
Gtk.Label pattern_help = builder.get_object("pattern_help") as Gtk.Label;
// Ticket #3162 - Move dir pattern blurb into Gnome help.
// Because specifying a particular snippet of the help requires
// us to know where its located, we can't hardcode a URL anymore;
// instead, we ask for the help path, and if we find it, we tell
// yelp to read from there, otherwise, we read from system-wide.
string help_path = Resources.get_help_path();
if (help_path == null) {
// We're installed system-wide, so use the system help.
pattern_help.set_markup("" + _("(Help)") + "");
} else {
// We're being run from the build directory; we'll have to handle clicks to this
// link manually ourselves, due to a limitation of help: URIs.
pattern_help.set_markup("" + _("(Help)") + "");
pattern_help.activate_link.connect(on_local_pattern_help);
}
dir_pattern_combo = builder.get_object("dir choser") as Gtk.ComboBoxText;
dir_pattern_entry = builder.get_object("dir_pattern_entry") as Gtk.Entry;
dir_pattern_example = builder.get_object("dynamic example") as Gtk.Label;
add_to_dir_formats(_("Year%sMonth%sDay").printf(Path.DIR_SEPARATOR_S, Path.DIR_SEPARATOR_S),
"%Y" + Path.DIR_SEPARATOR_S + "%m" + Path.DIR_SEPARATOR_S + "%d");
add_to_dir_formats(_("Year%sMonth").printf(Path.DIR_SEPARATOR_S), "%Y" +
Path.DIR_SEPARATOR_S + "%m");
add_to_dir_formats(_("Year%sMonth-Day").printf(Path.DIR_SEPARATOR_S),
"%Y" + Path.DIR_SEPARATOR_S + "%m-%d");
add_to_dir_formats(_("Year-Month-Day"), "%Y-%m-%d");
add_to_dir_formats(_("Custom"), null); // Custom must always be last.
dir_pattern_combo.changed.connect(on_dir_pattern_combo_changed);
dir_pattern_entry.changed.connect(on_dir_pattern_entry_changed);
(builder.get_object("dir_structure_label") as Gtk.Label).set_mnemonic_widget(dir_pattern_combo);
lowercase = builder.get_object("lowercase") as Gtk.CheckButton;
lowercase.toggled.connect(on_lowercase_toggled);
var notebook = builder.get_object("preferences-notebook") as Gtk.Notebook;
(notebook.get_nth_page (2) as Gtk.Container).add (plugins_mediator.widget);
populate_preference_options();
photo_editor_combo.changed.connect(on_photo_editor_changed);
raw_editor_combo.changed.connect(on_raw_editor_changed);
Gtk.CheckButton auto_import_button = builder.get_object("autoimport") as Gtk.CheckButton;
auto_import_button.set_active(Config.Facade.get_instance().get_auto_import_from_library());
Gtk.CheckButton commit_metadata_button = builder.get_object("write_metadata") as Gtk.CheckButton;
commit_metadata_button.set_active(Config.Facade.get_instance().get_commit_metadata_to_masters());
default_raw_developer_combo = builder.get_object("default_raw_developer") as Gtk.ComboBoxText;
default_raw_developer_combo.append_text(RawDeveloper.CAMERA.get_label());
default_raw_developer_combo.append_text(RawDeveloper.SHOTWELL.get_label());
set_raw_developer_combo(Config.Facade.get_instance().get_default_raw_developer());
default_raw_developer_combo.changed.connect(on_default_raw_developer_changed);
dialog.map_event.connect(map_event);
}
public void populate_preference_options() {
populate_app_combo_box(photo_editor_combo, PhotoFileFormat.get_editable_mime_types(),
Config.Facade.get_instance().get_external_photo_app(), out external_photo_apps);
populate_app_combo_box(raw_editor_combo, PhotoFileFormat.RAW.get_mime_types(),
Config.Facade.get_instance().get_external_raw_app(), out external_raw_apps);
setup_dir_pattern(dir_pattern_combo, dir_pattern_entry);
lowercase.set_active(Config.Facade.get_instance().get_use_lowercase_filenames());
}
// Ticket #3162, part II - if we're not yet installed, then we have to manually launch
// the help viewer and specify the full path to the subsection we want...
private bool on_local_pattern_help(string ignore) {
try {
Resources.launch_help(AppWindow.get_instance().get_screen(), "other-files.page");
} catch (Error e) {
message("Unable to launch help: %s", e.message);
}
return true;
}
private void populate_app_combo_box(Gtk.ComboBox combo_box, string[] mime_types,
string current_app_executable, out SortedList external_apps) {
// get list of all applications for the given mime types
assert(mime_types.length != 0);
external_apps = DesktopIntegration.get_apps_for_mime_types(mime_types);
if (external_apps.size == 0)
return;
// populate application ComboBox with app names and icons
Gtk.CellRendererPixbuf pixbuf_renderer = new Gtk.CellRendererPixbuf();
Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
combo_box.clear();
combo_box.pack_start(pixbuf_renderer, false);
combo_box.pack_start(text_renderer, false);
combo_box.add_attribute(pixbuf_renderer, "pixbuf", 0);
combo_box.add_attribute(text_renderer, "text", 1);
// TODO: need more space between icons and text
Gtk.ListStore combo_store = new Gtk.ListStore(2, typeof(Gdk.Pixbuf), typeof(string));
Gtk.TreeIter iter;
int current_app = -1;
foreach (AppInfo app in external_apps) {
combo_store.append(out iter);
Icon app_icon = app.get_icon();
try {
if (app_icon is FileIcon) {
combo_store.set_value(iter, 0, scale_pixbuf(new Gdk.Pixbuf.from_file(
((FileIcon) app_icon).get_file().get_path()), Resources.DEFAULT_ICON_SCALE,
Gdk.InterpType.BILINEAR, false));
} else if (app_icon is ThemedIcon) {
Gdk.Pixbuf icon_pixbuf =
Gtk.IconTheme.get_default().load_icon(((ThemedIcon) app_icon).get_names()[0],
Resources.DEFAULT_ICON_SCALE, Gtk.IconLookupFlags.FORCE_SIZE);
combo_store.set_value(iter, 0, icon_pixbuf);
}
} catch (GLib.Error error) {
warning("Error loading icon pixbuf: " + error.message);
}
combo_store.set_value(iter, 1, app.get_name());
if (app.get_commandline() == current_app_executable)
current_app = external_apps.index_of(app);
}
// TODO: allow users to choose unlisted applications like Nautilus's "Open with -> Other Application..."
combo_box.set_model(combo_store);
if (current_app != -1)
combo_box.set_active(current_app);
}
private void setup_dir_pattern(Gtk.ComboBox combo_box, Gtk.Entry entry) {
string? pattern = Config.Facade.get_instance().get_directory_pattern();
bool found = false;
if (null != pattern) {
// Locate pre-built text.
int i = 0;
foreach (PathFormat pf in path_formats) {
if (pf.pattern == pattern) {
combo_box.set_active(i);
found = true;
break;
}
i++;
}
} else {
// Custom path.
string? s = Config.Facade.get_instance().get_directory_pattern_custom();
if (!is_string_empty(s)) {
combo_box.set_active(path_formats.size - 1); // Assume "custom" is last.
found = true;
}
}
if (!found) {
combo_box.set_active(0);
}
on_dir_pattern_combo_changed();
}
public static void show() {
if (preferences_dialog == null)
preferences_dialog = new PreferencesDialog();
preferences_dialog.populate_preference_options();
preferences_dialog.dialog.show_all();
preferences_dialog.library_dir_button.set_current_folder(AppDirs.get_import_dir().get_path());
// Ticket #3001: Cause the dialog to become active if the user chooses 'Preferences'
// from the menus a second time.
preferences_dialog.dialog.present();
}
// For items that should only be committed when the dialog is closed, not as soon as the change
// is made.
private void commit_on_close() {
Config.Facade.get_instance().commit_bg_color();
Gtk.CheckButton? autoimport = builder.get_object("autoimport") as Gtk.CheckButton;
if (autoimport != null)
Config.Facade.get_instance().set_auto_import_from_library(autoimport.active);
Gtk.CheckButton? commit_metadata = builder.get_object("write_metadata") as Gtk.CheckButton;
if (commit_metadata != null)
Config.Facade.get_instance().set_commit_metadata_to_masters(commit_metadata.active);
if (lib_dir != null)
AppDirs.set_import_dir(lib_dir);
PathFormat pf = path_formats.get(dir_pattern_combo.get_active());
if (null == pf.pattern) {
Config.Facade.get_instance().set_directory_pattern_custom(dir_pattern_entry.text);
Config.Facade.get_instance().set_directory_pattern(null);
} else {
Config.Facade.get_instance().set_directory_pattern(pf.pattern);
}
}
private bool on_delete() {
if (!get_allow_closing())
return true;
commit_on_close();
return dialog.hide_on_delete(); //prevent widgets from getting destroyed
}
private void on_close() {
if (!get_allow_closing())
return;
dialog.hide();
commit_on_close();
}
private void on_value_changed() {
set_background_color((double)(bg_color_adjustment.get_upper() -
bg_color_adjustment.get_value()) / 65535.0);
}
private bool on_bg_color_reset(Gdk.EventButton event) {
if (event.button == 1 && event.type == Gdk.EventType.BUTTON_PRESS
&& has_only_key_modifier(event.state, Gdk.ModifierType.CONTROL_MASK)) {
// Left Mouse Button and CTRL pressed
bg_color_slider.set_value(bg_color_adjustment.get_upper() -
(parse_color(Config.Facade.DEFAULT_BG_COLOR).red * 65536.0f));
on_value_changed();
return true;
}
return false;
}
private void on_dir_pattern_combo_changed() {
PathFormat pf = path_formats.get(dir_pattern_combo.get_active());
if (null == pf.pattern) {
// Custom format.
string? dir_pattern = Config.Facade.get_instance().get_directory_pattern_custom();
if (is_string_empty(dir_pattern))
dir_pattern = "";
dir_pattern_entry.set_text(dir_pattern);
dir_pattern_entry.editable = true;
dir_pattern_entry.sensitive = true;
} else {
dir_pattern_entry.set_text(pf.pattern);
dir_pattern_entry.editable = false;
dir_pattern_entry.sensitive = false;
}
}
private void on_dir_pattern_entry_changed() {
string example = example_date.format(dir_pattern_entry.text);
if (is_string_empty(example) && !is_string_empty(dir_pattern_entry.text)) {
// Invalid pattern.
dir_pattern_example.set_text(_("Invalid pattern"));
dir_pattern_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "dialog-error");
dir_pattern_entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, false);
set_allow_closing(false);
} else {
// Valid pattern.
dir_pattern_example.set_text(example);
dir_pattern_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, null);
set_allow_closing(true);
}
}
private void set_allow_closing(bool allow) {
dialog.set_deletable(allow);
allow_closing = allow;
}
private bool get_allow_closing() {
return allow_closing;
}
private void set_background_color(double bg_color_value) {
Config.Facade.get_instance().set_bg_color(to_grayscale(bg_color_value));
}
private Gdk.RGBA to_grayscale(double color_value) {
Gdk.RGBA color = Gdk.RGBA();
color.red = color_value;
color.green = color_value;
color.blue = color_value;
color.alpha = 1.0;
return color;
}
private void on_photo_editor_changed() {
int photo_app_choice_index = (photo_editor_combo.get_active() < external_photo_apps.size) ?
photo_editor_combo.get_active() : external_photo_apps.size;
AppInfo app = external_photo_apps.get_at(photo_app_choice_index);
Config.Facade.get_instance().set_external_photo_app(DesktopIntegration.get_app_open_command(app));
debug("setting external photo editor to: %s", DesktopIntegration.get_app_open_command(app));
}
private void on_raw_editor_changed() {
int raw_app_choice_index = (raw_editor_combo.get_active() < external_raw_apps.size) ?
raw_editor_combo.get_active() : external_raw_apps.size;
AppInfo app = external_raw_apps.get_at(raw_app_choice_index);
Config.Facade.get_instance().set_external_raw_app(app.get_commandline());
debug("setting external raw editor to: %s", app.get_commandline());
}
private RawDeveloper raw_developer_from_combo() {
if (default_raw_developer_combo.get_active() == 0)
return RawDeveloper.CAMERA;
return RawDeveloper.SHOTWELL;
}
private void set_raw_developer_combo(RawDeveloper d) {
if (d == RawDeveloper.CAMERA)
default_raw_developer_combo.set_active(0);
else
default_raw_developer_combo.set_active(1);
}
private void on_default_raw_developer_changed() {
Config.Facade.get_instance().set_default_raw_developer(raw_developer_from_combo());
}
private void on_current_folder_changed() {
lib_dir = library_dir_button.get_filename();
}
private bool map_event() {
// Set the signal for the lib dir button after the dialog is displayed,
// because the FileChooserButton has a nasty habbit of selecting a
// different folder when displayed if the provided path doesn't exist.
// See ticket #3000 for more info.
library_dir_button.current_folder_changed.connect(on_current_folder_changed);
return true;
}
private void add_to_dir_formats(string name, string? pattern) {
PathFormat pf = new PathFormat(name, pattern);
path_formats.add(pf);
dir_pattern_combo.append_text(name);
}
private void on_lowercase_toggled() {
Config.Facade.get_instance().set_use_lowercase_filenames(lowercase.get_active());
}
}
// 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