From 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Wed, 23 Jul 2014 09:06:59 +0200 Subject: Imported Upstream version 0.18.1 --- src/data_imports/DataImportJob.vala | 177 ++++++++++ src/data_imports/DataImportSource.vala | 135 ++++++++ src/data_imports/DataImports.vala | 30 ++ src/data_imports/DataImportsPluginHost.vala | 482 ++++++++++++++++++++++++++++ src/data_imports/DataImportsUI.vala | 445 +++++++++++++++++++++++++ src/data_imports/mk/data_imports.mk | 31 ++ 6 files changed, 1300 insertions(+) create mode 100644 src/data_imports/DataImportJob.vala create mode 100644 src/data_imports/DataImportSource.vala create mode 100644 src/data_imports/DataImports.vala create mode 100644 src/data_imports/DataImportsPluginHost.vala create mode 100644 src/data_imports/DataImportsUI.vala create mode 100644 src/data_imports/mk/data_imports.mk (limited to 'src/data_imports') diff --git a/src/data_imports/DataImportJob.vala b/src/data_imports/DataImportJob.vala new file mode 100644 index 0000000..b27997c --- /dev/null +++ b/src/data_imports/DataImportJob.vala @@ -0,0 +1,177 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Spit.DataImports { + +/** + * A specialized import job implementation for alien databases. + */ +public class DataImportJob : BatchImportJob { + private DataImportSource import_source; + private File? src_file; + private uint64 filesize; + private time_t exposure_time; + private DataImportJob? associated = null; + private HierarchicalTagIndex? detected_htags = null; + + public DataImportJob(DataImportSource import_source) { + this.import_source = import_source; + + // stash everything called in prepare(), as it may/will be called from a separate thread + src_file = import_source.get_file(); + filesize = import_source.get_filesize(); + exposure_time = import_source.get_exposure_time(); + } + + private HierarchicalTagIndex? build_exclusion_index(ImportableTag[] src_tags) { + Gee.Set detected_htags = new Gee.HashSet(); + + foreach (ImportableTag src_tag in src_tags) { + string? prepped = HierarchicalTagUtilities.join_path_components( + Tag.prep_tag_names( + build_path_components(src_tag) + ) + ); + + if (prepped != null && prepped.has_prefix(Tag.PATH_SEPARATOR_STRING)) { + detected_htags.add(prepped); + + Gee.List parents = HierarchicalTagUtilities.enumerate_parent_paths(prepped); + foreach (string parent in parents) + detected_htags.add(parent); + } + } + + return (detected_htags.size > 0) ? HierarchicalTagIndex.from_paths(detected_htags) : null; + } + + public time_t get_exposure_time() { + return exposure_time; + } + + public override string get_dest_identifier() { + return import_source.get_filename(); + } + + public override string get_source_identifier() { + return import_source.get_filename(); + } + + public override bool is_directory() { + return false; + } + + public override string get_basename() { + return src_file.get_basename(); + } + + public override string get_path() { + return src_file.get_parent().get_path(); + } + + public override void set_associated(BatchImportJob associated) { + this.associated = associated as DataImportJob; + } + + public override bool determine_file_size(out uint64 filesize, out File file) { + file = null; + filesize = this.filesize; + + return true; + } + + public override bool prepare(out File file_to_import, out bool copy_to_library) throws Error { + file_to_import = src_file; + copy_to_library = false; + + detected_htags = build_exclusion_index(import_source.get_photo().get_tags()); + + return true; + } + + public override bool complete(MediaSource source, BatchImportRoll import_roll) throws Error { + LibraryPhoto? photo = source as LibraryPhoto; + if (photo == null) + return false; + + ImportableMediaItem src_photo = import_source.get_photo(); + + // tags + if (detected_htags != null) { + Gee.Collection paths = detected_htags.get_all_paths(); + + foreach (string path in paths) + Tag.for_path(path); + } + + ImportableTag[] src_tags = src_photo.get_tags(); + foreach (ImportableTag src_tag in src_tags) { + string? prepped = HierarchicalTagUtilities.join_path_components( + Tag.prep_tag_names( + build_path_components(src_tag) + ) + ); + if (prepped != null) { + if (HierarchicalTagUtilities.enumerate_path_components(prepped).size == 1) { + if (prepped.has_prefix(Tag.PATH_SEPARATOR_STRING)) + prepped = HierarchicalTagUtilities.hierarchical_to_flat(prepped); + } else { + Gee.List parents = + HierarchicalTagUtilities.enumerate_parent_paths(prepped); + + assert(parents.size > 0); + + string top_level_parent = parents.get(0); + string flat_top_level_parent = + HierarchicalTagUtilities.hierarchical_to_flat(top_level_parent); + + if (Tag.global.exists(flat_top_level_parent)) + Tag.for_path(flat_top_level_parent).promote(); + } + + Tag.for_path(prepped).attach(photo); + } + } + // event + ImportableEvent? src_event = src_photo.get_event(); + if (src_event != null) { + string? prepped = prepare_input_text(src_event.get_name(), + PrepareInputTextOptions.DEFAULT, -1); + if (prepped != null) + Event.generate_single_event(photo, import_roll.generated_events, prepped); + } + // rating + Rating dst_rating; + ImportableRating src_rating = src_photo.get_rating(); + if (src_rating.is_rejected()) + dst_rating = Rating.REJECTED; + else if (src_rating.is_unrated()) + dst_rating = Rating.UNRATED; + else + dst_rating = Rating.unserialize(src_rating.get_value()); + photo.set_rating(dst_rating); + // title + string? title = src_photo.get_title(); + if (title != null) + photo.set_title(title); + // import ID + photo.set_import_id(import_roll.import_id); + + return true; + } + + private string[] build_path_components(ImportableTag tag) { + // use a linked list as we are always inserting in head position + Gee.List components = new Gee.LinkedList(); + for (ImportableTag current_tag = tag; current_tag != null; current_tag = current_tag.get_parent()) { + components.insert(0, HierarchicalTagUtilities.make_flat_tag_safe(current_tag.get_name())); + } + return components.to_array(); + } +} + +} + diff --git a/src/data_imports/DataImportSource.vala b/src/data_imports/DataImportSource.vala new file mode 100644 index 0000000..d7e8ec8 --- /dev/null +++ b/src/data_imports/DataImportSource.vala @@ -0,0 +1,135 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Spit.DataImports { + +/** + * Photo source implementation for alien databases. This class is responsible + * for extracting meta-data out of a source photo to support the import + * process. + * + * This class does not extend PhotoSource in order to minimise the API to the + * absolute minimum required to run the import job. + */ +public class DataImportSource { + private bool backing_file_found; + private ImportableMediaItem db_photo; + private string? title = null; + private string? preview_md5 = null; + private uint64 file_size; + private time_t modification_time; + private MetadataDateTime? exposure_time; + + public DataImportSource(ImportableMediaItem db_photo) { + this.db_photo = db_photo; + + // A well-behaved plugin will ensure that the path and file name are + // not null but we check just in case + string folder_path = db_photo.get_folder_path(); + string filename = db_photo.get_filename(); + File? photo = null; + if (folder_path != null && filename != null) { + photo = File.new_for_path(db_photo.get_folder_path()). + get_child(db_photo.get_filename()); + + backing_file_found = photo.query_exists(); + } else { + backing_file_found = false; + } + + if (photo != null && backing_file_found) { + PhotoMetadata? metadata = new PhotoMetadata(); + try { + metadata.read_from_file(photo); + } catch(Error e) { + warning("Could not get file metadata for %s: %s", get_filename(), e.message); + metadata = null; + } + + title = (metadata != null) ? metadata.get_title() : null; + exposure_time = (metadata != null) ? metadata.get_exposure_date_time() : null; + PhotoPreview? preview = metadata != null ? metadata.get_preview(0) : null; + if (preview != null) { + try { + uint8[] preview_raw = preview.flatten(); + preview_md5 = md5_binary(preview_raw, preview_raw.length); + } catch(Error e) { + warning("Could not get raw preview for %s: %s", get_filename(), e.message); + } + } +#if TRACE_MD5 + debug("Photo MD5 %s: preview=%s", get_filename(), preview_md5); +#endif + + try { + file_size = query_total_file_size(photo); + } catch(Error e) { + warning("Could not get file size for %s: %s", get_filename(), e.message); + } + try { + modification_time = query_file_modified(photo); + } catch(Error e) { + warning("Could not get modification time for %s: %s", get_filename(), e.message); + } + } else { + debug ("Photo file %s not found".printf(photo.get_path())); + } + } + + public string get_filename() { + return db_photo.get_filename(); + } + + public string get_fulldir() { + return db_photo.get_folder_path(); + } + + public File get_file() { + return File.new_for_path(get_fulldir()).get_child(get_filename()); + } + + public string get_name() { + return !is_string_empty(title) ? title : get_filename(); + } + + public string? get_title() { + return title; + } + + public PhotoFileFormat get_file_format() { + return PhotoFileFormat.get_by_basename_extension(get_filename()); + } + + public string to_string() { + return get_name(); + } + + public time_t get_exposure_time() { + return (exposure_time != null) ? exposure_time.get_timestamp() : modification_time; + } + + public uint64 get_filesize() { + return file_size; + } + + public ImportableMediaItem get_photo() { + return db_photo; + } + + public bool is_already_imported() { + // ignore trashed duplicates + return (preview_md5 != null) + ? LibraryPhoto.has_nontrash_duplicate(null, preview_md5, null, get_file_format()) + : false; + } + + public bool was_backing_file_found() { + return backing_file_found; + } +} + +} + diff --git a/src/data_imports/DataImports.vala b/src/data_imports/DataImports.vala new file mode 100644 index 0000000..72c8b4d --- /dev/null +++ b/src/data_imports/DataImports.vala @@ -0,0 +1,30 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/* This file is the master unit file for the DataImports unit. It should be edited to include + * whatever code is deemed necessary. + * + * The init() and terminate() methods are mandatory. + * + * If the unit needs to be configured prior to initialization, add the proper parameters to + * the preconfigure() method, implement it, and ensure in init() that it's been called. + */ + +namespace DataImports { + +public void init() throws Error { + string[] core_ids = new string[0]; + core_ids += "org.yorba.shotwell.dataimports.fspot"; + + Plugins.register_extension_point(typeof(Spit.DataImports.Service), _("Data Imports"), + Resources.IMPORT, core_ids); +} + +public void terminate() { +} + +} + diff --git a/src/data_imports/DataImportsPluginHost.vala b/src/data_imports/DataImportsPluginHost.vala new file mode 100644 index 0000000..f92bc53 --- /dev/null +++ b/src/data_imports/DataImportsPluginHost.vala @@ -0,0 +1,482 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Spit.DataImports { + +private class CoreImporter { + private weak Spit.DataImports.PluginHost host; + public int imported_items_count = 0; + public BatchImportRoll? current_import_roll = null; + + public CoreImporter(Spit.DataImports.PluginHost host) { + this.host = host; + } + + public void prepare_media_items_for_import( + ImportableMediaItem[] items, + double progress, + double host_progress_delta = 0.0, + string? progress_message = null + ) { + host.update_import_progress_pane(progress, progress_message); + // + SortedList jobs = + new SortedList(import_job_comparator); + Gee.ArrayList already_imported = + new Gee.ArrayList(); + Gee.ArrayList failed = + new Gee.ArrayList(); + + int item_idx = 0; + double item_progress_delta = host_progress_delta / items.length; + foreach (ImportableMediaItem src_item in items) { + DataImportSource import_source = new DataImportSource(src_item); + + if (!import_source.was_backing_file_found()) { + message("Skipping import of %s: backing file not found", + import_source.get_filename()); + failed.add(new DataImportJob(import_source)); + + continue; + } + + if (import_source.is_already_imported()) { + message("Skipping import of %s: checksum detected in library", + import_source.get_filename()); + already_imported.add(new DataImportJob(import_source)); + + continue; + } + + jobs.add(new DataImportJob(import_source)); + item_idx++; + host.update_import_progress_pane(progress + item_idx * item_progress_delta); + } + + if (jobs.size > 0) { + // If there it no current import roll, create one to ensure that all + // imported items end up in the same roll even if this method is called + // several times + if (current_import_roll == null) + current_import_roll = new BatchImportRoll(); + string db_name = _("%s Database").printf(host.get_data_importer().get_service().get_pluggable_name()); + BatchImport batch_import = new BatchImport(jobs, db_name, data_import_reporter, + failed, already_imported, null, current_import_roll); + + LibraryWindow.get_app().enqueue_batch_import(batch_import, true); + imported_items_count += jobs.size; + } + + host.update_import_progress_pane(progress + host_progress_delta); + } + + public void finalize_import() { + // Send an empty job to the queue to mark the end of the import + string db_name = _("%s Database").printf(host.get_data_importer().get_service().get_pluggable_name()); + BatchImport batch_import = new BatchImport( + new Gee.ArrayList(), db_name, data_import_reporter, null, null, null, current_import_roll + ); + LibraryWindow.get_app().enqueue_batch_import(batch_import, true); + current_import_roll = null; + } +} + +public class ConcreteDataImportsHost : Plugins.StandardHostInterface, + Spit.DataImports.PluginHost { + + private Spit.DataImports.DataImporter active_importer = null; + private weak DataImportsUI.DataImportsDialog dialog = null; + private DataImportsUI.ProgressPane? progress_pane = null; + private bool importing_halted = false; + private CoreImporter core_importer; + + public ConcreteDataImportsHost(Service service, DataImportsUI.DataImportsDialog dialog) { + base(service, "data_imports"); + this.dialog = dialog; + + this.active_importer = service.create_data_importer(this); + this.core_importer = new CoreImporter(this); + } + + public DataImporter get_data_importer() { + return active_importer; + } + + public void start_importing() { + if (get_data_importer().is_running()) + return; + + debug("ConcreteDataImportsHost.start_importing( ): invoked."); + + get_data_importer().start(); + } + + public void stop_importing() { + debug("ConcreteDataImportsHost.stop_importing( ): invoked."); + + if (get_data_importer().is_running()) + get_data_importer().stop(); + + clean_up(); + + importing_halted = true; + } + + private void clean_up() { + progress_pane = null; + } + + public void set_button_mode(Spit.DataImports.PluginHost.ButtonMode mode) { + if (mode == Spit.DataImports.PluginHost.ButtonMode.CLOSE) + dialog.set_close_button_mode(); + else if (mode == Spit.DataImports.PluginHost.ButtonMode.CANCEL) + dialog.set_cancel_button_mode(); + else + error("unrecognized button mode enumeration value"); + } + + // Pane handling methods + + public void post_error(Error err) { + post_error_message(err.message); + } + + public void post_error_message(string message) { + string msg = _("Importing from %s can't continue because an error occurred:").printf( + active_importer.get_service().get_pluggable_name()); + msg += GLib.Markup.printf_escaped("\n\n%s\n\n", message); + msg += _("To try importing from another service, select one from the above menu."); + + dialog.install_pane(new DataImportsUI.StaticMessagePane.with_pango(msg)); + dialog.set_close_button_mode(); + dialog.unlock_service(); + + get_data_importer().stop(); + + // post_error_message( ) tells the active_importer to stop importing and displays a + // non-removable error pane that effectively ends the publishing interaction, + // so no problem calling clean_up( ) here. + clean_up(); + } + + public void install_dialog_pane(Spit.DataImports.DialogPane pane, + Spit.DataImports.PluginHost.ButtonMode button_mode = Spit.DataImports.PluginHost.ButtonMode.CANCEL) { + debug("DataImports.PluginHost: install_dialog_pane( ): invoked."); + + if (get_data_importer() == null || (!get_data_importer().is_running())) + return; + + dialog.install_pane(pane); + + set_button_mode(button_mode); + } + + public void install_static_message_pane(string message, + Spit.DataImports.PluginHost.ButtonMode button_mode = Spit.DataImports.PluginHost.ButtonMode.CANCEL) { + + set_button_mode(button_mode); + + dialog.install_pane(new DataImportsUI.StaticMessagePane.with_pango(message)); + } + + public void install_library_selection_pane( + string welcome_message, + ImportableLibrary[] discovered_libraries, + string? file_select_label + ) { + if (discovered_libraries.length == 0 && file_select_label == null) + post_error_message("Libraries or file option needed"); + else + dialog.install_pane(new DataImportsUI.LibrarySelectionPane( + this, + welcome_message, + discovered_libraries, + file_select_label + )); + set_button_mode(Spit.DataImports.PluginHost.ButtonMode.CLOSE); + } + + public void install_import_progress_pane( + string message + ) { + progress_pane = new DataImportsUI.ProgressPane(message); + dialog.install_pane(progress_pane); + set_button_mode(Spit.DataImports.PluginHost.ButtonMode.CANCEL); + // initialize the import + core_importer.imported_items_count = 0; + core_importer.current_import_roll = null; + } + + public void update_import_progress_pane( + double progress, + string? progress_message = null + ) { + if (progress_pane != null) { + progress_pane.update_progress(progress, progress_message); + } + } + + public void prepare_media_items_for_import( + ImportableMediaItem[] items, + double progress, + double host_progress_delta = 0.0, + string? progress_message = null + ) { + core_importer.prepare_media_items_for_import(items, progress, host_progress_delta, progress_message); + } + + public void finalize_import( + ImportedItemsCountCallback report_imported_items_count, + string? finalize_message = null + ) { + update_import_progress_pane(1.0, finalize_message); + set_button_mode(Spit.DataImports.PluginHost.ButtonMode.CLOSE); + core_importer.finalize_import(); + report_imported_items_count(core_importer.imported_items_count); + if (core_importer.imported_items_count > 0) + LibraryWindow.get_app().switch_to_import_queue_page(); + } +} + +public class WelcomeDataImportsHost : Plugins.StandardHostInterface, + Spit.DataImports.PluginHost { + + private weak WelcomeImportMetaHost meta_host; + private Spit.DataImports.DataImporter active_importer = null; + private bool importing_halted = false; + private CoreImporter core_importer; + + public WelcomeDataImportsHost(Service service, WelcomeImportMetaHost meta_host) { + base(service, "data_imports"); + + this.active_importer = service.create_data_importer(this); + this.core_importer = new CoreImporter(this); + this.meta_host = meta_host; + } + + public DataImporter get_data_importer() { + return active_importer; + } + + public void start_importing() { + if (get_data_importer().is_running()) + return; + + debug("WelcomeDataImportsHost.start_importing( ): invoked."); + + get_data_importer().start(); + } + + public void stop_importing() { + debug("WelcomeDataImportsHost.stop_importing( ): invoked."); + + if (get_data_importer().is_running()) + get_data_importer().stop(); + + clean_up(); + + importing_halted = true; + } + + private void clean_up() { + } + + // Pane handling methods + + public void post_error(Error err) { + post_error_message(err.message); + } + + public void post_error_message(string message) { + string msg = _("Importing from %s can't continue because an error occurred:").printf( + active_importer.get_service().get_pluggable_name()); + + debug(msg); + + get_data_importer().stop(); + + // post_error_message( ) tells the active_importer to stop importing and displays a + // non-removable error pane that effectively ends the publishing interaction, + // so no problem calling clean_up( ) here. + clean_up(); + } + + public void install_dialog_pane(Spit.DataImports.DialogPane pane, + Spit.DataImports.PluginHost.ButtonMode button_mode = Spit.DataImports.PluginHost.ButtonMode.CANCEL) { + // do nothing + } + + public void install_static_message_pane(string message, + Spit.DataImports.PluginHost.ButtonMode button_mode = Spit.DataImports.PluginHost.ButtonMode.CANCEL) { + // do nothing + } + + public void install_library_selection_pane( + string welcome_message, + ImportableLibrary[] discovered_libraries, + string? file_select_label + ) { + debug("WelcomeDataImportsHost: Installing library selection pane for %s".printf(get_data_importer().get_service().get_pluggable_name())); + if (discovered_libraries.length > 0) { + meta_host.install_service_entry(new WelcomeImportServiceEntry( + this, + get_data_importer().get_service().get_pluggable_name(), + discovered_libraries + )); + } + } + + public void install_import_progress_pane( + string message + ) { + // empty implementation + } + + public void update_import_progress_pane( + double progress, + string? progress_message = null + ) { + // empty implementation + } + + public void prepare_media_items_for_import( + ImportableMediaItem[] items, + double progress, + double host_progress_delta = 0.0, + string? progress_message = null + ) { + core_importer.prepare_media_items_for_import(items, progress, host_progress_delta, progress_message); + } + + public void finalize_import( + ImportedItemsCountCallback report_imported_items_count, + string? finalize_message = null + ) { + core_importer.finalize_import(); + report_imported_items_count(core_importer.imported_items_count); + meta_host.finalize_import(this); + } +} + + +//public delegate void WelcomeImporterCallback(); + +public class WelcomeImportServiceEntry : GLib.Object, WelcomeServiceEntry { + private string pluggable_name; + private ImportableLibrary[] discovered_libraries; + private Spit.DataImports.PluginHost host; + + public WelcomeImportServiceEntry( + Spit.DataImports.PluginHost host, + string pluggable_name, ImportableLibrary[] discovered_libraries) { + + this.host = host; + this.pluggable_name = pluggable_name; + this.discovered_libraries = discovered_libraries; + } + + public string get_service_name() { + return pluggable_name; + } + + public void execute() { + foreach (ImportableLibrary library in discovered_libraries) { + host.get_data_importer().on_library_selected(library); + } + } +} + +public class WelcomeImportMetaHost : GLib.Object { + private WelcomeDialog dialog; + + public WelcomeImportMetaHost(WelcomeDialog dialog) { + this.dialog = dialog; + } + + public void start() { + Service[] services = load_all_services(); + foreach (Service service in services) { + WelcomeDataImportsHost host = new WelcomeDataImportsHost(service, this); + host.start_importing(); + } + } + + public void finalize_import(WelcomeDataImportsHost host) { + host.stop_importing(); + } + + public void install_service_entry(WelcomeServiceEntry entry) { + debug("WelcomeImportMetaHost: Installing service entry for %s".printf(entry.get_service_name())); + dialog.install_service_entry(entry); + } +} + +public static Spit.DataImports.Service[] load_all_services() { + return load_services(true); +} + +public static Spit.DataImports.Service[] load_services(bool load_all = false) { + Spit.DataImports.Service[] loaded_services = new Spit.DataImports.Service[0]; + + // load publishing services from plug-ins + Gee.Collection pluggables = Plugins.get_pluggables_for_type( + typeof(Spit.DataImports.Service), null, load_all); + // TODO: include sorting function to ensure consistent order + + debug("DataImportsDialog: discovered %d pluggable data import services.", pluggables.size); + + foreach (Spit.Pluggable pluggable in pluggables) { + int pluggable_interface = pluggable.get_pluggable_interface( + Spit.DataImports.CURRENT_INTERFACE, Spit.DataImports.CURRENT_INTERFACE); + if (pluggable_interface != Spit.DataImports.CURRENT_INTERFACE) { + warning("Unable to load data import plugin %s: reported interface %d.", + Plugins.get_pluggable_module_id(pluggable), pluggable_interface); + + continue; + } + + Spit.DataImports.Service service = + (Spit.DataImports.Service) pluggable; + + debug("DataImportsDialog: discovered pluggable data import service '%s'.", + service.get_pluggable_name()); + + loaded_services += service; + } + + // Sort import services by name. + // TODO: extract to a function to sort it on initial request + Posix.qsort(loaded_services, loaded_services.length, sizeof(Spit.DataImports.Service), + (a, b) => {return utf8_cs_compare((*((Spit.DataImports.Service**) a))->get_pluggable_name(), + (*((Spit.DataImports.Service**) b))->get_pluggable_name()); + }); + + return loaded_services; +} + +private ImportManifest? meta_manifest = null; + +private void data_import_reporter(ImportManifest manifest, BatchImportRoll import_roll) { + if (manifest.all.size > 0) { + if (meta_manifest == null) + meta_manifest = new ImportManifest(); + foreach (BatchImportResult result in manifest.all) { + meta_manifest.add_result(result); + } + } else { + DataImportsUI.DataImportsDialog.terminate_instance(); + ImportUI.report_manifest(meta_manifest, true); + meta_manifest = null; + } +} + +private int64 import_job_comparator(void *a, void *b) { + return ((DataImportJob *) a)->get_exposure_time() + - ((DataImportJob *) b)->get_exposure_time(); +} + +} + diff --git a/src/data_imports/DataImportsUI.vala b/src/data_imports/DataImportsUI.vala new file mode 100644 index 0000000..9b171b1 --- /dev/null +++ b/src/data_imports/DataImportsUI.vala @@ -0,0 +1,445 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace DataImportsUI { + +internal const string NO_PLUGINS_ENABLED_MESSAGE = + _("You do not have any data imports plugins enabled.\n\nIn order to use the Import From Application functionality, you need to have at least one data imports plugin enabled. Plugins can be enabled in the Preferences dialog."); + +public class ConcreteDialogPane : Spit.DataImports.DialogPane, GLib.Object { + private Gtk.Box pane_widget; + + public ConcreteDialogPane() { + pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 8); + } + + public Gtk.Widget get_widget() { + return pane_widget; + } + + public Spit.DataImports.DialogPane.GeometryOptions get_preferred_geometry() { + return Spit.DataImports.DialogPane.GeometryOptions.NONE; + } + + public void on_pane_installed() { + } + + public void on_pane_uninstalled() { + } +} + +public class StaticMessagePane : ConcreteDialogPane { + public StaticMessagePane(string message_string) { + Gtk.Label message_label = new Gtk.Label(message_string); + (get_widget() as Gtk.Box).pack_start(message_label, true, true, 0); + } + + public StaticMessagePane.with_pango(string msg) { + Gtk.Label label = new Gtk.Label(null); + label.set_markup(msg); + label.set_line_wrap(true); + + (get_widget() as Gtk.Box).pack_start(label, true, true, 0); + } +} + +public class LibrarySelectionPane : ConcreteDialogPane { + private weak Spit.DataImports.PluginHost host; + private Spit.DataImports.ImportableLibrary? selected_library = null; + private File? selected_file = null; + private Gtk.Button import_button; + private Gtk.RadioButton? file_radio = null; + + public LibrarySelectionPane( + Spit.DataImports.PluginHost host, + string welcome_message, + Spit.DataImports.ImportableLibrary[] discovered_libraries, + string? file_select_label + ) { + assert(discovered_libraries.length > 0 || on_file_selected != null); + + this.host = host; + + Gtk.Box content_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 8); + content_box.set_margin_left(30); + content_box.set_margin_right(30); + Gtk.Label welcome_label = new Gtk.Label(null); + welcome_label.set_markup(welcome_message); + welcome_label.set_line_wrap(true); + welcome_label.set_halign(Gtk.Align.START); + content_box.pack_start(welcome_label, true, true, 6); + + // margins for buttons + int radio_margin_left = 20; + int radio_margin_right = 20; + int chooser_margin_left = radio_margin_left; + int chooser_margin_right = radio_margin_right; + + Gtk.RadioButton lib_radio = null; + if (discovered_libraries.length > 0) { + chooser_margin_left = radio_margin_left + 20; + foreach (Spit.DataImports.ImportableLibrary library in discovered_libraries) { + string lib_radio_label = library.get_display_name(); + lib_radio = create_radio_button( + content_box, lib_radio, library, lib_radio_label, + radio_margin_left, radio_margin_right + ); + } + if (file_select_label != null) { + lib_radio = create_radio_button( + content_box, lib_radio, null, file_select_label, + radio_margin_left, radio_margin_right + ); + file_radio = lib_radio; + } + } + if (file_select_label != null) { + Gtk.FileChooserButton file_chooser = new Gtk.FileChooserButton(_("Database file:"), Gtk.FileChooserAction.OPEN); + file_chooser.selection_changed.connect(() => { + selected_file = file_chooser.get_file(); + if (file_radio != null) + file_radio.active = true; + set_import_button_sensitivity(); + }); + file_chooser.set_margin_left(chooser_margin_left); + file_chooser.set_margin_right(chooser_margin_right); + content_box.pack_start(file_chooser, false, false, 6); + } + + import_button = new Gtk.Button.with_mnemonic(_("_Import")); + import_button.clicked.connect(() => { + if (selected_library != null) + on_library_selected(selected_library); + else if (selected_file != null) + on_file_selected(selected_file); + else + debug("LibrarySelectionPane: Library or file should be selected."); + }); + Gtk.ButtonBox button_box = new Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL); + button_box.layout_style = Gtk.ButtonBoxStyle.CENTER; + button_box.add(import_button); + content_box.pack_end(button_box, true, false, 6); + + (get_widget() as Gtk.Box).pack_start(content_box, true, true, 0); + + set_import_button_sensitivity(); + } + + private Gtk.RadioButton create_radio_button( + Gtk.Box box, Gtk.RadioButton? group, Spit.DataImports.ImportableLibrary? library, string label, + int margin_left, int margin_right + ) { + var button = new Gtk.RadioButton.with_label_from_widget (group, label); + if (group == null) { // first radio button is active + button.active = true; + selected_library = library; + } + button.toggled.connect (() => { + if (button.active) { + this.selected_library = library; + set_import_button_sensitivity(); + } + + }); + button.set_margin_left(margin_left); + button.set_margin_right(margin_right); + box.pack_start(button, false, false, 6); + return button; + } + + private void set_import_button_sensitivity() { + import_button.set_sensitive(selected_library != null || selected_file != null); + } + + private void on_library_selected(Spit.DataImports.ImportableLibrary library) { + host.get_data_importer().on_library_selected(library); + } + + private void on_file_selected(File file) { + host.get_data_importer().on_file_selected(file); + } +} + +public class ProgressPane : ConcreteDialogPane { + private Gtk.Label message_label; + private Gtk.Label progress_label; + private Gtk.ProgressBar progress_bar; + + public ProgressPane(string message) { + Gtk.Box content_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 8); + message_label = new Gtk.Label(message); + content_box.pack_start(message_label, true, true, 6); + progress_bar = new Gtk.ProgressBar(); + content_box.pack_start(progress_bar, false, true, 6); + progress_label = new Gtk.Label(""); + content_box.pack_start(progress_label, false, true, 6); + + (get_widget() as Gtk.Container).add(content_box); + } + + public void update_progress(double progress, string? progress_message) { + progress_bar.set_fraction(progress); + if (progress_message != null) + progress_label.set_label(progress_message); + spin_event_loop(); + } +} + +public class DataImportsDialog : Gtk.Dialog { + private const int LARGE_WINDOW_WIDTH = 860; + private const int LARGE_WINDOW_HEIGHT = 688; + private const int COLOSSAL_WINDOW_WIDTH = 1024; + private const int COLOSSAL_WINDOW_HEIGHT = 688; + private const int STANDARD_WINDOW_WIDTH = 600; + private const int STANDARD_WINDOW_HEIGHT = 510; + private const int BORDER_REGION_WIDTH = 16; + private const int BORDER_REGION_HEIGHT = 100; + + public const int STANDARD_CONTENT_LABEL_WIDTH = 500; + public const int STANDARD_ACTION_BUTTON_WIDTH = 128; + + private Gtk.ComboBoxText service_selector_box; + private Gtk.Label service_selector_box_label; + private Gtk.Box central_area_layouter; + private Gtk.Button close_cancel_button; + private Spit.DataImports.DialogPane active_pane; + private Spit.DataImports.ConcreteDataImportsHost host; + + protected DataImportsDialog() { + + resizable = false; + delete_event.connect(on_window_close); + + string title = _("Import From Application"); + string label = _("Import media _from:"); + + set_title(title); + + Spit.DataImports.Service[] loaded_services = Spit.DataImports.load_services(); + + if (loaded_services.length > 0) { + // Install the service selector part only if there is at least one + // service to select from + service_selector_box = new Gtk.ComboBoxText(); + service_selector_box.set_active(0); + service_selector_box_label = new Gtk.Label.with_mnemonic(label); + service_selector_box_label.set_mnemonic_widget(service_selector_box); + service_selector_box_label.set_alignment(0.0f, 0.5f); + + // get the name of the service the user last used + string? last_used_service = Config.Facade.get_instance().get_last_used_dataimports_service(); + + int ticker = 0; + int last_used_index = -1; + foreach (Spit.DataImports.Service service in loaded_services) { + string curr_service_id = service.get_id(); + if (last_used_service != null && last_used_service == curr_service_id) + last_used_index = ticker; + + service_selector_box.append_text(service.get_pluggable_name()); + ticker++; + } + if (last_used_index >= 0) + service_selector_box.set_active(last_used_index); + else + service_selector_box.set_active(0); + + service_selector_box.changed.connect(on_service_changed); + + /* the wrapper is not an extraneous widget -- it's necessary to prevent the service + selection box from growing and shrinking whenever its parent's size changes. + When wrapped inside a Gtk.Alignment, the Alignment grows and shrinks instead of + the service selection box. */ + Gtk.Alignment service_selector_box_wrapper = new Gtk.Alignment(1.0f, 0.5f, 0.0f, 0.0f); + service_selector_box_wrapper.add(service_selector_box); + + Gtk.Box service_selector_layouter = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8); + service_selector_layouter.set_border_width(12); + service_selector_layouter.add(service_selector_box_label); + service_selector_layouter.pack_start(service_selector_box_wrapper, true, true, 0); + + /* 'service area' is the selector assembly plus the horizontal rule dividing it from the + rest of the dialog */ + Gtk.Box service_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + service_area_layouter.pack_start(service_selector_layouter, true, true, 0); + Gtk.Separator service_central_separator = new Gtk.Separator(Gtk.Orientation.HORIZONTAL); + service_area_layouter.add(service_central_separator); + + Gtk.Alignment service_area_wrapper = new Gtk.Alignment(0.0f, 0.0f, 1.0f, 0.0f); + service_area_wrapper.add(service_area_layouter); + + ((Gtk.Box) get_content_area()).pack_start(service_area_wrapper, false, false, 0); + } + + // Intall the central area in all cases + central_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + ((Gtk.Box) get_content_area()).pack_start(central_area_layouter, true, true, 0); + + close_cancel_button = new Gtk.Button.with_mnemonic("_Cancel"); + close_cancel_button.set_can_default(true); + close_cancel_button.clicked.connect(on_close_cancel_clicked); + ((Gtk.Box) get_action_area()).add(close_cancel_button); + + set_standard_window_mode(); + + if (loaded_services.length > 0) { + // trigger the selected service if at least one service is available + on_service_changed(); + } else { + // otherwise, install a message pane advising the user what to do + install_pane(new StaticMessagePane.with_pango(NO_PLUGINS_ENABLED_MESSAGE)); + set_close_button_mode(); + } + + show_all(); + } + + public static DataImportsDialog get_or_create_instance() { + if (instance == null) { + instance = new DataImportsDialog(); + } + return instance; + } + + public static void terminate_instance() { + if (instance != null) { + instance.terminate(); + } + instance = null; + } + + private bool on_window_close(Gdk.EventAny evt) { + debug("DataImportsDialog: on_window_close( ): invoked."); + terminate(); + + return true; + } + + private void on_service_changed() { + debug("DataImportsDialog: on_service_changed invoked."); + string service_name = service_selector_box.get_active_text(); + + Spit.DataImports.Service? selected_service = null; + Spit.DataImports.Service[] services = Spit.DataImports.load_all_services(); + foreach (Spit.DataImports.Service service in services) { + if (service.get_pluggable_name() == service_name) { + selected_service = service; + break; + } + } + assert(selected_service != null); + + Config.Facade.get_instance().set_last_used_dataimports_service(selected_service.get_id()); + + host = new Spit.DataImports.ConcreteDataImportsHost(selected_service, this); + host.start_importing(); + } + + private void on_close_cancel_clicked() { + debug("DataImportsDialog: on_close_cancel_clicked( ): invoked."); + + terminate(); + } + + private void terminate() { + debug("DataImportsDialog: terminate( ): invoked."); + + if (host != null) { + host.stop_importing(); + host = null; + } + + hide(); + destroy(); + instance = null; + } + + private void set_large_window_mode() { + set_size_request(LARGE_WINDOW_WIDTH, LARGE_WINDOW_HEIGHT); + central_area_layouter.set_size_request(LARGE_WINDOW_WIDTH - BORDER_REGION_WIDTH, + LARGE_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); + resizable = false; + } + + private void set_colossal_window_mode() { + set_size_request(COLOSSAL_WINDOW_WIDTH, COLOSSAL_WINDOW_HEIGHT); + central_area_layouter.set_size_request(COLOSSAL_WINDOW_WIDTH - BORDER_REGION_WIDTH, + COLOSSAL_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); + resizable = false; + } + + private void set_standard_window_mode() { + set_size_request(STANDARD_WINDOW_WIDTH, STANDARD_WINDOW_HEIGHT); + central_area_layouter.set_size_request(STANDARD_WINDOW_WIDTH - BORDER_REGION_WIDTH, + STANDARD_WINDOW_HEIGHT - BORDER_REGION_HEIGHT); + resizable = false; + } + + private void set_free_sizable_window_mode() { + resizable = true; + } + + private void clear_free_sizable_window_mode() { + resizable = false; + } + + public Spit.DataImports.DialogPane get_active_pane() { + return active_pane; + } + + public void set_close_button_mode() { + close_cancel_button.set_label(_("_Close")); + set_default(close_cancel_button); + } + + public void set_cancel_button_mode() { + close_cancel_button.set_label(_("_Cancel")); + } + + public void lock_service() { + service_selector_box.set_sensitive(false); + } + + public void unlock_service() { + service_selector_box.set_sensitive(true); + } + + public void install_pane(Spit.DataImports.DialogPane pane) { + debug("DataImportsDialog: install_pane( ): invoked."); + + if (active_pane != null) { + debug("DataImportsDialog: install_pane( ): a pane is already installed; removing it."); + + active_pane.on_pane_uninstalled(); + central_area_layouter.remove(active_pane.get_widget()); + } + + central_area_layouter.pack_start(pane.get_widget(), true, true, 0); + show_all(); + + Spit.DataImports.DialogPane.GeometryOptions geometry_options = + pane.get_preferred_geometry(); + if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.EXTENDED_SIZE) != 0) + set_large_window_mode(); + else if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.COLOSSAL_SIZE) != 0) + set_colossal_window_mode(); + else + set_standard_window_mode(); + + if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.RESIZABLE) != 0) + set_free_sizable_window_mode(); + else + clear_free_sizable_window_mode(); + + active_pane = pane; + pane.on_pane_installed(); + } + + private static DataImportsDialog? instance; +} + +} + diff --git a/src/data_imports/mk/data_imports.mk b/src/data_imports/mk/data_imports.mk new file mode 100644 index 0000000..771ba74 --- /dev/null +++ b/src/data_imports/mk/data_imports.mk @@ -0,0 +1,31 @@ + +# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with +# a init() and terminate() function declared in the namespace. +UNIT_NAME := DataImports + +# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all +# lowercase. The name of this file should be UNIT_DIR.mk. +UNIT_DIR := data_imports + +# All Vala files in the unit should be listed here with no subdirectory prefix. +# +# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala. +UNIT_FILES := \ + DataImportsPluginHost.vala \ + DataImportsUI.vala \ + DataImportJob.vala \ + DataImportSource.vala + +# Any unit this unit relies upon (and should be initialized before it's initialized) should +# be listed here using its Vala namespace. +# +# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here. +UNIT_USES := + +# List any additional files that are used in the build process as a part of this unit that should +# be packaged in the tarball. File names should be relative to the unit's home directory. +UNIT_RC := + +# unitize.mk must be called at the end of each UNIT_DIR.mk file. +include unitize.mk + -- cgit v1.2.3