// SPDX-License-Identifier: GPL-2.0-or-later /* * The main Inkscape application. * * Copyright (C) 2018 Tavmjong Bah * * The contents of this file may be used under the GNU General Public License Version 2 or later. * */ #include #include // Internationalization #include "inkscape-application.h" #include "inkscape-window.h" #include "inkscape.h" // Inkscape::Application #include "desktop.h" // Access to window #include "file.h" // sp_file_convert_dpi #include "inkgc/gc-core.h" // Garbage Collecting init #include "io/file.h" // File open (command line). #include "io/resource.h" // TEMPLATE #include "io/resource-manager.h" // Fix up references. #include "object/sp-root.h" // Inkscape version. #include "ui/dialog/font-substitution.h" // Warn user about font substitution. #include "ui/widget/panel.h" // Panel prep #include "widgets/desktop-widget.h" // Close without saving dialog #include "util/units.h" // Redimension window #include "actions/actions-base.h" // Actions #include "actions/actions-output.h" // Actions #include "actions/actions-selection.h" // Actions #include "actions/actions-transform.h" // Actions #ifdef WITH_DBUS # include "extension/dbus/dbus-init.h" #endif #ifdef ENABLE_NLS // Native Language Support - shouldn't this always be used? #include "helper/gettext.h" // gettext init #endif // ENABLE_NLS #include "io/resource.h" using Inkscape::IO::Resource::UIS; // This is a bit confusing as there are two ways to handle command line arguments and files // depending on if the Gio::APPLICATION_HANDLES_OPEN and/or Gio::APPLICATION_HANDLES_COMMAND_LINE // flags are set. If the open flag is set and the command line not, the all the remainng arguments // after calling on_handle_local_options() are assumed to be filenames. InkscapeApplication::InkscapeApplication() : _with_gui(true) , _batch_process(false) , _use_shell(false) , _active_document(nullptr) , _active_selection(nullptr) , _active_view(nullptr) {} // Add document to app. void InkscapeApplication::document_add(SPDocument* document) { if (document) { auto it = _documents.find(document); if (it == _documents.end()) { _documents[document] = std::vector(); } else { // Should never happen. std::cerr << "InkscapeApplication::add_document: Document already opened!" << std::endl; } } else { // Should never happen! std::cerr << "InkscapeApplication::add_document: No document!" << std::endl; } } // New document, add it to app. TODO: This should really be open_document with option to strip template data. SPDocument* InkscapeApplication::document_new(const std::string &Template) { // Open file SPDocument *document = ink_file_new(Template); if (document) { document_add(document); // Set viewBox if it doesn't exist. if (!document->getRoot()->viewBox_set) { document->setViewBox(); } } else { std::cerr << "InkscapeApplication::new_document: failed to open new document!" << std::endl; } return document; } // Open a document, add it to app. SPDocument* InkscapeApplication::document_open(const Glib::RefPtr& file) { // Open file bool cancelled = false; SPDocument *document = ink_file_open(file, cancelled); if (document) { document->virgin = false; // Prevents replacing document in same window during file open. document_add (document); } else { std::cerr << "InkscapeApplication::open_document: Failed to open: " << file->get_parse_name() << std::endl; } return document; } /** Swap out one document for another in a window... maybe this should disappear. * Does not delete old document! */ bool InkscapeApplication::document_swap(InkscapeWindow* window, SPDocument* document) { if (!document || !window) { std::cerr << "InkscapeAppliation::swap_document: Missing window or document!" << std::endl; return false; } SPDesktop* desktop = window->get_desktop(); SPDocument* old_document = window->get_document(); desktop->change_document(document); document->emitResizedSignal(document->getWidth().value("px"), document->getHeight().value("px")); // We need to move window from the old document to the new document. // Find old document auto it = _documents.find(old_document); if (it != _documents.end()) { // Remove window from document map. auto it2 = std::find(it->second.begin(), it->second.end(), window); if (it2 != it->second.end()) { it->second.erase(it2); } else { std::cerr << "InkscapeApplication::swap_document: Window not found!" << std::endl; } } else { std::cerr << "InkscapeApplication::swap_document: Document not in map!" << std::endl; } // Find new document it = _documents.find(document); if (it != _documents.end()) { it->second.push_back(window); } else { std::cerr << "InkscapeApplication::swap_document: Document not in map!" << std::endl; } // To be removed (add/delete once per window)! INKSCAPE.add_document(document); INKSCAPE.remove_document(old_document); // ActionContext should be removed once verbs are gone but we use it for now. Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); _active_document = document; _active_selection = context.getSelection(); _active_view = context.getView(); return true; } /** Revert document: open saved document and swap it for each window. */ bool InkscapeApplication::document_revert(SPDocument* document) { // Find saved document. gchar const *path = document->getDocumentURI(); if (!path) { std::cerr << "InkscapeApplication::revert_document: Document never saved, cannot revert." << std::endl; return false; } // Open saved document. Glib::RefPtr file = Gio::File::create_for_path(document->getDocumentURI()); SPDocument* new_document = document_open (file); if (!new_document) { std::cerr << "InkscapeApplication::revert_document: Cannot open saved document!" << std::endl; return false; } // Allow overwriting current document. document->virgin = true; auto it = _documents.find(document); if (it != _documents.end()) { // Swap reverted document in all windows. for (auto it2 : it->second) { SPDesktop* desktop = it2->get_desktop(); // Remember current zoom and view. double zoom = desktop->current_zoom(); Geom::Point c = desktop->get_display_area().midpoint(); bool reverted = document_swap (it2, new_document); if (reverted) { desktop->zoom_absolute_center_point (c, zoom); } else { std::cerr << "InkscapeApplication::revert_document: Revert failed!" << std::endl; } } document_close (document); } else { std::cerr << "InkscapeApplication::revert_document: Document not found!" << std::endl; return false; } return true; } /** Close a document, remove from app. No checking is done on modified status, etc. */ void InkscapeApplication::document_close(SPDocument* document) { if (document) { auto it = _documents.find(document); if (it != _documents.end()) { if (it->second.size() != 0) { std::cerr << "InkscapeApplication::close_document: Window vector not empty!" << std::endl; } _documents.erase(it); } else { std::cerr << "InkscapeApplication::close_document: Document not registered with application." << std::endl; } delete document; } else { std::cerr << "InkscapeApplication::close_document: No document!" << std::endl; } } /** Return number of windows with document. */ unsigned InkscapeApplication::document_window_count(SPDocument* document) { unsigned count = 0; auto it = _documents.find(document); if (it != _documents.end()) { count = it->second.size(); } else { std::cerr << "InkscapeApplication::document_window_count: Document not in map!" << std::endl; } return count; } /** Fix up a document if necessary (Only fixes that require GUI). */ void InkscapeApplication::document_fix(InkscapeWindow* window) { // Most fixes are handled when document is opened in SPDocument::createDoc(). // But some require the GUI to be present. These are handled here. if (_with_gui) { SPDocument* document = window->get_document(); // Perform a fixup pass for hrefs. if ( Inkscape::ResourceManager::getManager().fixupBrokenLinks(document) ) { Glib::ustring msg = _("Broken links have been changed to point to existing files."); SPDesktop* desktop = window->get_desktop(); if (desktop != nullptr) { desktop->showInfoDialog(msg); } } // Fix dpi (pre-92 files). if ( sp_version_inside_range( document->getRoot()->version.inkscape, 0, 1, 0, 92 ) ) { sp_file_convert_dpi(document); } // Check for font substitutions, requires text to have been rendered. Inkscape::UI::Dialog::FontSubstitution::getInstance().checkFontSubstitutions(document); } } // Take an already open document and create a new window, adding window to document map. InkscapeWindow* InkscapeApplication::window_open(SPDocument* document) { InkscapeWindow* window = new InkscapeWindow(document); // TODO Add window to application. (Instead of in InkscapeWindow constructor.) SPDesktop* desktop = window->get_desktop(); // To be removed (add once per window)! INKSCAPE.add_document(document); // ActionContext should be removed once verbs are gone but we use it for now. Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); _active_selection = context.getSelection(); _active_view = context.getView(); _active_document = document; auto it = _documents.find(document); if (it != _documents.end()) { it->second.push_back(window); } else { std::cerr << "InkscapeApplication::open_window: Document not in map!" << std::endl; } document_fix(window); // May need flag to prevent this from being called more than once. return window; } // Close a window. Does not delete document. void InkscapeApplication::window_close(InkscapeWindow* window) { // std::cout << "InkscapeApplication::close_window" << std::endl; // dump(); if (window) { SPDocument* document = window->get_document(); if (document) { // To be removed (remove once per window)! bool last = INKSCAPE.remove_document(document); _active_selection = nullptr; _active_view = nullptr; _active_document = nullptr; // Remove window from document map. auto it = _documents.find(document); if (it != _documents.end()) { auto it2 = std::find(it->second.begin(), it->second.end(), window); if (it2 != it->second.end()) { it->second.erase(it2); delete window; // Results in call to SPDesktop::destroy() } else { std::cerr << "ConcreteInkscapeApplication::close_window: window not found!" << std::endl; } } else { std::cerr << "ConcreteInkscapeApplication::close_window: document not in map!" << std::endl; } } else { std::cerr << "ConcreteInkscapeApplication::close_window: No document!" << std::endl; } } else { std::cerr << "ConcreteInkscapeApplication::close_window: No window!" << std::endl; } // dump(); } /** Update windows in response to: * - New active window * - Document change * - Selection change */ void InkscapeApplication::windows_update(SPDocument* document) { // Find windows: auto it = _documents.find( document ); if (it != _documents.end()) { std::vector windows = it->second; // std::cout << "InkscapeApplication::update_windows: windows size: " << windows.size() << std::endl; // Loop over InkscapeWindows. // Loop over DialogWindows. TBD } else { // std::cout << "InkscapeApplication::update_windows: no windows found" << std::endl; } } /** Debug function */ void InkscapeApplication::dump() { std::cout << "InkscapeApplication::dump()" << std::endl; std::cout << " Documents: " << _documents.size() << std::endl; for (auto i : _documents) { std::cout << " Document: " << (i.first->getDocumentName()?i.first->getDocumentName():"unnamed") << std::endl; for (auto j : i.second) { std::cout << " Window: " << j->get_title() << std::endl; } } } template ConcreteInkscapeApplication& ConcreteInkscapeApplication::get_instance() { static ConcreteInkscapeApplication instance; return instance; } template ConcreteInkscapeApplication::ConcreteInkscapeApplication() : T("org.inkscape.application.with_gui", Gio::APPLICATION_HANDLES_OPEN | // Use default file opening. Gio::APPLICATION_NON_UNIQUE ) // Allows different instances of Inkscape to run at same time. , InkscapeApplication() { // ==================== Initializations ===================== // Garbage Collector Inkscape::GC::init(); #ifdef ENABLE_NLS // Native Language Support (shouldn't this always be used?). Inkscape::initialize_gettext(); #endif Glib::set_application_name(N_("Inkscape - A Vector Drawing Program")); // After gettext() init. // ======================== Actions ========================= add_actions_base(this); // actions that are GUI independent add_actions_output(this); // actions for file export add_actions_selection(this); // actions for object selection add_actions_transform(this); // actions for transforming selected objects // ====================== Command Line ====================== // Will automatically handle character conversions. // Note: OPTION_TYPE_FILENAME => std::string, OPTION_TYPE_STRING => Glib::ustring. // Actions this->add_main_option_entry(T::OPTION_TYPE_STRING, "actions", 'a', N_("Actions (with optional arguments), semi-colon separated."), N_("ACTION(:ARG)[;ACTION(:ARG)]*")); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "action-list", '\0', N_("Actions: List available actions."), ""); // Query this->add_main_option_entry(T::OPTION_TYPE_BOOL, "version", 'V', N_("Print: Inkscape version."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "extension-directory", 'x', N_("Print: Extensions directory."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "verb-list", '\0', N_("Print: List verbs."), ""); // Interface this->add_main_option_entry(T::OPTION_TYPE_BOOL, "with-gui", 'g', N_("GUI: With graphical interface."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "without-gui", 'G', N_("GUI: Console only."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "batch-process", '\0', N_("GUI: Close window after processing actions (needed as some verbs require GUI)."), ""); // Open/Import this->add_main_option_entry(T::OPTION_TYPE_INT, "pdf-page", '\0', N_("Open: PDF page to import"), N_("PAGE")); this->add_main_option_entry(T::OPTION_TYPE_STRING, "convert-dpi-method", '\0', N_("Open: Method used to convert pre-0.92 document dpi, if needed: [none|scale-viewbox|scale-document]."), "[...]"); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "no-convert-text-baseline-spacing", 0, N_("Open: Do not fix pre-0.92 document's text baseline spacing on opening."), ""); // Query - Geometry this->add_main_option_entry(T::OPTION_TYPE_STRING, "query-id", 'I', N_("Query: ID(s) of object(s) to be queried."), N_("OBJECT-ID[,OBJECT-ID]*")); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-all", 'S', N_("Query: Print bounding boxes of all objects."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-x", 'X', N_("Query: X coordinate of drawing or object (if specified by --query-id)."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-y", 'Y', N_("Query: Y coordinate of drawing or object (if specified by --query-id)."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-width", 'W', N_("Query: Width of drawing or object (if specified by --query-id)."), ""); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-height", 'H', N_("Query: Height of drawing or object (if specified by --query-id)."), ""); // Processing this->add_main_option_entry(T::OPTION_TYPE_BOOL, "vacuum-defs", '\0', N_("Process: Remove unused definitions from the section(s) of document."), ""); this->add_main_option_entry(T::OPTION_TYPE_STRING, "select", '\0', N_("Process: Select objects: comma separated list of IDs."), N_("OBJECT-ID[,OBJECT-ID]*")); this->add_main_option_entry(T::OPTION_TYPE_STRING, "verb", '\0', N_("Process: Verb(s) to call when Inkscape opens."), N_("VERB-ID[;VERB-ID]*")); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "shell", '\0', N_("Process: Start Inkscape in interactive shell mode."), ""); // Export - File and File Type this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-type", '\0', N_("Export: File type:[svg,png,ps,psf,tex,emf,wmf,xaml]"), "[...]"); this->add_main_option_entry(T::OPTION_TYPE_FILENAME, "export-file", 'o', N_("Export: File name"), N_("EXPORT-FILENAME")); this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-overwrite", '\0', N_("Export: Overwrite input file."), ""); // BSP // B = PNG, S = SVG, P = PS/EPS/PDF // Export - Geometry this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-area", 'a', N_("Export: Area to export in SVG user units."), N_("x0:y0:x1:y1")); // BSP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-drawing", 'D', N_("Export: Area to export is drawing (not page)."), ""); // BSP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-page", 'C', N_("Export: Area to export is page."), ""); // BSP this->add_main_option_entry(T::OPTION_TYPE_INT, "export-margin", '\0', N_("Export: Margin around export area: units of page size for SVG, mm for PS/EPS/PDF."), ""); // xSP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-snap", '\0', N_("Export: Snap the bitmap export area outwards to the nearest integer values."), ""); // Bxx this->add_main_option_entry(T::OPTION_TYPE_INT, "export-width", 'w', N_("Export: Bitmap width in pixels (overrides --export-dpi)."), N_("WIDTH")); // Bxx this->add_main_option_entry(T::OPTION_TYPE_INT, "export-height", 'h', N_("Export: Bitmap height in pixels (overrides --export-dpi)."), N_("HEIGHT")); // Bxx // Export - Options this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-id", 'i', N_("Export: ID(s) of object(s) to export."), N_("OBJECT-ID[;OBJECT-ID]*")); // BSP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-id-only", 'j', N_("Export: Hide all objects except object with ID selected by export-id."), ""); // BSx this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-plain-svg", 'l', N_("Export: Remove items in the Inkscape namespace."), ""); // xSx this->add_main_option_entry(T::OPTION_TYPE_INT, "export-dpi", 'd', N_("Export: Resolution for rasterization bitmaps and filters (default is 96)."), N_("DPI")); // BxP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-ignore-filters", '\0', N_("Export: Render objects without filters instead of rasterizing. (PS/EPS/PDF)"), ""); // xxP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-text-to-path", 'T', N_("Export: Convert text to paths. (PS/EPS/PDF/SVG)."), ""); // xxP this->add_main_option_entry(T::OPTION_TYPE_INT, "export-ps-level", '\0', N_("Export: Postscript level (2 or 3). Default is 3."), N_("PS-Level")); // xxP this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-pdf-level", '\0', N_("Export: PDF level (1.4 or 1.5)"), N_("PDF-Level")); // xxP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-latex", '\0', N_("Export: Export text separately to LaTeX file (PS/EPS/PDF). Include via \\input{file.tex}"), ""); // xxP this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-use-hints", 't', N_("Export: Use stored filename and DPI hints when exporting object selected by --export-id."), ""); // Bxx this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-background", 'b', N_("Export: Background color for exported bitmaps (any SVG color string)."), N_("COLOR")); // Bxx this->add_main_option_entry(T::OPTION_TYPE_DOUBLE, "export-background-opacity", 'y', N_("Export: Background opacity for exported bitmaps (either 0.0 to 1.0 or 1 to 255)."), N_("VALUE")); // Bxx #ifdef WITH_YAML this->add_main_option_entry(T::OPTION_TYPE_FILENAME, "xverbs", '\0', N_("Process: xverb command file."), N_("XVERBS-FILENAME")); #endif // WITH_YAML #ifdef WITH_DBUS this->add_main_option_entry(T::OPTION_TYPE_BOOL, "dbus-listen", '\0', N_("D-Bus: Enter a listening loop for D-Bus messages in console mode."), ""); this->add_main_option_entry(T::OPTION_TYPE_STRING, "dbus-name", '\0', N_("D-Bus: Specify the D-Bus name (default is 'org.inkscape')."), N_("BUS-NAME")); #endif // WITH_DBUS Gio::Application::signal_handle_local_options().connect(sigc::mem_fun(*this, &InkscapeApplication::on_handle_local_options)); // This is normally called for us... but after the "handle_local_options" signal is emitted. If // we want to rely on actions for handling options, we need to call it here. This appears to // have no unwanted side-effect. It will also trigger the call to on_startup(). T::register_application(); } template void ConcreteInkscapeApplication::on_startup() { T::on_startup(); } // Here are things that should be in on_startup() but cannot be as we don't set _with_gui until // on_handle_local_options() is called. template<> void ConcreteInkscapeApplication::on_startup2() { Inkscape::Application::create(nullptr, false); } template<> void ConcreteInkscapeApplication::on_startup2() { // This should be completely rewritten. Inkscape::Application::create(nullptr, _with_gui); // argv appears to not be used. if (!_with_gui) { return; } // ======================= Actions (GUI) ====================== add_action("new", sigc::mem_fun(*this, &InkscapeApplication::on_new )); add_action("quit", sigc::mem_fun(*this, &InkscapeApplication::on_quit )); // ========================= GUI Init ========================= Gtk::Window::set_default_icon_name("org.inkscape.Inkscape"); Inkscape::UI::Widget::Panel::prep(); // ========================= Builder ========================== // App menus deprecated in 3.32. This whole block of code should be // removed after confirming this code isn't required. _builder = Gtk::Builder::create(); Glib::ustring app_builder_file = get_filename(UIS, "inkscape-application.xml"); try { _builder->add_from_file(app_builder_file); } catch (const Glib::Error& ex) { std::cerr << "InkscapeApplication: " << app_builder_file << " file not read! " << ex.what() << std::endl; } auto object = _builder->get_object("menu-application"); auto menu = Glib::RefPtr::cast_dynamic(object); if (!menu) { std::cerr << "InkscapeApplication: failed to load application menu!" << std::endl; } else { // set_app_menu(menu); } } /** We should not create a window if T is Gio::Applicaton. */ template SPDesktop* ConcreteInkscapeApplication::create_window(const Glib::RefPtr& file, bool add_to_recent, bool replace_empty) { std::cerr << "ConcreteInkscapeApplication::create_window: Should not be called!"; return nullptr; } /** Create a window given a Gio::File. This is what most functions should call. The booleans are only false when opening a help file. */ template<> SPDesktop* ConcreteInkscapeApplication::create_window(const Glib::RefPtr& file, bool add_to_recent, bool replace_empty) { SPDesktop* desktop = nullptr; if (file) { SPDocument* document = document_open (file); if (document) { if (add_to_recent) { auto recentmanager = Gtk::RecentManager::get_default(); Gtk::RecentManager::Data data; data.app_name = g_get_prgname(); // Must match Gtk::RecentFilter in menubar.cpp data.mime_type = "image"; // We don't know if we opened an SVG! (image/svg+xml). recentmanager->add_item (file->get_uri(), data); } // TODO Remove this code... handle document replacement elsewhere. SPDocument* old_document = _active_document; if (replace_empty && old_document && old_document->virgin) { // virgin == true => an empty document (template). // Is there a better place for this? It requires GUI. document->ensureUpToDate(); // TODO this will trigger broken line warnings, etc. InkscapeWindow* window = dynamic_cast(get_active_window()); if (window) { document_swap (window, document); // Delete old document if no longer attached to any window. auto it = _documents.find (old_document); if (it != _documents.end()) { if (it->second.size() == 0) { document_close (old_document); } } document->emitResizedSignal(document->getWidth().value("px"), document->getHeight().value("px")); desktop = window->get_desktop(); } else { std::cerr << "ConcreteInkscapeApplication::create_window: Failed to find active window!" << std::endl; } } else { InkscapeWindow* window = window_open (document); desktop = window->get_desktop(); } } else { std::cerr << "ConcreteInkscapeApplication::create_window: Failed to load: " << file->get_parse_name() << std::endl; } } else { std::string Template = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::TEMPLATES, "default.svg", true); SPDocument* document = document_new (Template); if (document) { InkscapeWindow* window = window_open (document); desktop = window->get_desktop(); } else { std::cerr << "ConcreteInkscapeApplication::create_window: Failed to open default template! " << Template << std::endl; } } if (desktop) { _active_document = desktop->getDocument(); #ifdef WITH_DBUS Inkscape::Extension::Dbus::dbus_init_desktop_interface(desktop); #endif } else { std::cerr << "ConcreteInkscapeApplication::create_window: Failed to create desktop!" << std::endl; } return (desktop); // Temp: Need to track desktop for shell mode. } /** No need to destroy window if T is Gio::Application. */ template bool ConcreteInkscapeApplication::destroy_window(InkscapeWindow* window) { std::cerr << "ConcreteInkscapeApplication::destroy_window: Should not be called!"; return false; } /** Destroy a window. Aborts if document needs saving. * Returns true if window destroyed. */ template<> bool ConcreteInkscapeApplication::destroy_window(InkscapeWindow* window) { SPDocument* document = window->get_document(); // Remove document if no windows left. if (document) { auto it = _documents.find(document); if (it != _documents.end()) { // If only one window for document: if (it->second.size() == 1) { // Check if document needs saving. bool abort = window->get_desktop_widget()->shutdown(); if (abort) { return false; } } window_close(window); if (it->second.size() == 0) { document_close (document); } } else { std::cerr << "ConcreteInkscapeApplication::destroy_window: Could not find document!" << std::endl; } } // Debug // auto windows = get_windows(); // std::cout << "destroy_windows: app windows size: " << windows.size() << std::endl; return true; } /* Close all windows and exit. **/ template void ConcreteInkscapeApplication::destroy_all() { std::cerr << "ConcreteInkscapeApplication::destroy_all: Should not be called!"; } template<> void ConcreteInkscapeApplication::destroy_all() { while (_documents.size() != 0) { auto it = _documents.begin(); while (it->second.size() != 0) { auto it2 = it->second.begin(); if (!destroy_window (*it2)) return; // If destroy aborted, we need to stop exit. } } } // Open document window with default document. Either this or on_open() is called. template void ConcreteInkscapeApplication::on_activate() { on_startup2(); if (_with_gui) { if (_use_shell) { shell(); // Shell will create its own windows. } else { create_window(); } } else { std::cerr << "InkscapeApplication::on_activate: Without GUI" << std::endl; if (_use_shell) { shell2(); } // Create blank document? } } // Open document window for each file. Either this or on_activate() is called. // type_vec_files == std::vector > template void ConcreteInkscapeApplication::on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) { on_startup2(); for (auto file : files) { // Open file SPDocument *document = document_open (file); if (!document) continue; // Add to Inkscape::Application... INKSCAPE.add_document(document); // ActionContext should be removed once verbs are gone but we use it for now. Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); _active_document = document; _active_selection = context.getSelection(); _active_view = context.getView(); document->ensureUpToDate(); // Or queries don't work! // process_file(file); for (auto action: _command_line_actions) { Gio::Application::activate_action( action.first, action.second ); } if (_use_shell) { shell2(); } else { // Save... can't use action yet. _file_export.do_export(document, file->get_path()); } _active_document = nullptr; _active_selection = nullptr; _active_view = nullptr; // Close file INKSCAPE.remove_document(document); document_close (document); } } // Open document window for each file. Either this or on_activate() is called. // type_vec_files == std::vector > template<> void ConcreteInkscapeApplication::on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) { on_startup2(); for (auto file : files) { if (_with_gui) { // Create a window for each file. SPDesktop* desktop = create_window(file); // Process each file. for (auto action: _command_line_actions) { Gio::Application::activate_action( action.first, action.second ); } // Close window after we're done with file. This may not be the best way... // but we need to rewrite most of the window handling code so do this for now. if (_batch_process) { std::vector windows = get_windows(); remove_window(*windows[0]); // There should be only one window (added in InkscapeWindow constructor). // Eventually create_window() should return a pointer to the window, not the desktop. } } else { // Open file SPDocument *document = document_open (file); if (!document) continue; // Add to Inkscape::Application... INKSCAPE.add_document(document); // ActionContext should be removed once verbs are gone but we use it for now. Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); _active_document = document; _active_selection = context.getSelection(); _active_view = context.getView(); document->ensureUpToDate(); // Or queries don't work! // process_file(file); for (auto action: _command_line_actions) { Gio::Application::activate_action( action.first, action.second ); } if (_use_shell) { shell2(); } else { // Save... can't use action yet. _file_export.do_export(document, file->get_path()); } _active_document = nullptr; _active_selection = nullptr; _active_view = nullptr; // Close file INKSCAPE.remove_document(document); document_close (document); } } } template void ConcreteInkscapeApplication::parse_actions(const Glib::ustring& input, action_vector_t& action_vector) { // Split action list std::vector tokens = Glib::Regex::split_simple("\\s*;\\s*", input); for (auto token : tokens) { std::vector tokens2 = Glib::Regex::split_simple("\\s*:\\s*", token); std::string action; std::string value; if (tokens2.size() > 0) { action = tokens2[0]; } if (tokens2.size() > 1) { value = tokens2[1]; } Glib::RefPtr action_ptr = Gio::Application::lookup_action(action); if (action_ptr) { // Doesn't seem to be a way to test this using the C++ binding without Glib-CRITICAL errors. const GVariantType* gtype = g_action_get_parameter_type(action_ptr->gobj()); if (gtype) { // With value. Glib::VariantType type = action_ptr->get_parameter_type(); if (type.get_string() == "b") { bool b = false; if (value == "0" || value == "true" || value.empty()) { b = true; } else if (value =="1" || value == "false") { b = false; } else { std::cerr << "InkscapeApplication::parse_actions: Invalid boolean value: " << action << ":" << value << std::endl; } action_vector.push_back( std::make_pair( action, Glib::Variant::create(b))); } else if (type.get_string() == "i") { action_vector.push_back( std::make_pair( action, Glib::Variant::create(std::stoi(value)))); } else if (type.get_string() == "d") { action_vector.push_back( std::make_pair( action, Glib::Variant::create(std::stod(value)))); } else if (type.get_string() == "s") { action_vector.push_back( std::make_pair( action, Glib::Variant::create(value) )); } else { std::cerr << "InkscapeApplication::parse_actions: unhandled action value: " << action << ": " << type.get_string() << std::endl; } } else { // Stateless (i.e. no value). action_vector.push_back( std::make_pair( action, Glib::VariantBase() ) ); } } else { // Assume a verb // std::cerr << "InkscapeApplication::parse_actions: '" // << action << "' is not a valid action! Assuming verb!" << std::endl; action_vector.push_back( std::make_pair("verb", Glib::Variant::create(action))); } } } // Interactively trigger actions. This is a travesty! Due to most verbs requiring a desktop we must // create one even in shell mode! template void ConcreteInkscapeApplication::shell() { std::cout << "Inkscape interactive shell mode. Type 'quit' to quit." << std::endl; std::cout << " Input of the form:" << std::endl; std::cout << "> filename action1:arg1; action2:arg2; verb1; verb2; ..." << std::endl; std::string input; while (true) { std::cout << "> "; std::getline(std::cin, input); if (input == "quit") break; // Get filename which must be first and separated by space (and not contain ':' or ';'). // Regex works on Glib::ustrings and will give bad results if one tries to use std::string. Glib::ustring input_u = input; Glib::ustring filename; Glib::RefPtr regex = Glib::Regex::create("^\\s*([^:;\\s]+)\\s+(.*)"); Glib::MatchInfo match_info; regex->match(input_u, match_info); if (match_info.matches()) { filename = match_info.fetch(1); input_u = match_info.fetch(2); } else { std::cerr << "InkscapeApplication::shell: Failed to find file in |" << input << "|" << std::endl; } // Create desktop. SPDesktop* desktop = nullptr; if (!filename.empty()) { Glib::RefPtr file = Gio::File::create_for_path(filename); desktop = create_window(file); } // Find and execute actions (verbs). action_vector_t action_vector; parse_actions(input_u, action_vector); for (auto action: action_vector) { Gio::Application::activate_action( action.first, action.second ); } if (desktop) { desktop->destroy(); } } T::quit(); // Must quit or segfault. (Might be fixed by using desktop->getToplevel()->hide() above.); } // Once we don't need to create a window just to process verbs! template void ConcreteInkscapeApplication::shell2() { std::cout << "Inkscape interactive shell mode. Type 'quit' to quit." << std::endl; std::cout << " Input of the form:" << std::endl; std::cout << "> action1:arg1; action2;arg2; verb1; verb2; ..." << std::endl; std::cout << "Only verbs that don't require a desktop may be used." << std::endl; std::string input; while (true) { std::cout << "> "; std::cin >> input; if (input == "quit") break; action_vector_t action_vector; parse_actions(input, action_vector); for (auto action: action_vector) { T::activate_action( action.first, action.second ); } } } // ========================= Callbacks ========================== /* * Handle command line options. * * Options are processed in the order they appear in this function. * We process in order: Print -> GUI -> Open -> Query -> Process -> Export. * For each file without GUI: Open -> Query -> Process -> Export * More flexible processing can be done via actions or xverbs. */ template int ConcreteInkscapeApplication::on_handle_local_options(const Glib::RefPtr& options) { if (!options) { std::cerr << "InkscapeApplication::on_handle_local_options: options is null!" << std::endl; return -1; // Keep going } // ===================== QUERY ===================== // These are processed first as they result in immediate program termination. if (options->contains("version")) { T::activate_action("inkscape-version"); return EXIT_SUCCESS; } if (options->contains("extension-directory")) { T::activate_action("extension-directory"); return EXIT_SUCCESS; } if (options->contains("verb-list")) { T::activate_action("verb-list"); return EXIT_SUCCESS; } if (options->contains("action-list")) { std::vector actions = T::list_actions(); std::sort(actions.begin(), actions.end()); for (auto action : actions) { std::cout << action << std::endl; } return EXIT_SUCCESS; } // For options without arguments. auto base = Glib::VariantBase(); // ================== GUI and Shell ================ if (options->contains("without-gui")) _with_gui = false; if (options->contains("with-gui")) _with_gui = true; if (options->contains("batch-process")) _batch_process = true; if (options->contains("shell")) _use_shell = true; // Some options should preclude using gui! if (options->contains("query-id") || options->contains("query-x") || options->contains("query-all") || options->contains("query-y") || options->contains("query-width") || options->contains("query-height") || options->contains("export-file") || options->contains("export-type") || options->contains("export-overwrite") || options->contains("export-id") || options->contains("export-plain-svg") || options->contains("export-text-to_path") ) { _with_gui = false; } // ==================== ACTIONS ==================== // Actions as an argument string: e.g.: --actions="query-id:rect1;query-x". // Actions will be processed in order that they are given in argument. Glib::ustring actions; if (options->contains("actions")) { options->lookup_value("actions", actions); parse_actions(actions, _command_line_actions); } // ================= OPEN/IMPORT =================== if (options->contains("pdf-page")) { // Maybe useful for other file types? int page = 0; options->lookup_value("pdf-page", page); _command_line_actions.push_back( std::make_pair("open-page", Glib::Variant::create(page))); } if (options->contains("convert-dpi-method")) { Glib::ustring method; options->lookup_value("convert-dpi-method", method); if (!method.empty()) { _command_line_actions.push_back( std::make_pair("convert-dpi-method", Glib::Variant::create(method))); } } if (options->contains("no-convert-text-baseline-spacing")) _command_line_actions.push_back(std::make_pair("no-convert-baseline", base)); // ===================== QUERY ===================== // 'query-id' should be processed first! Can be a comma separated list. if (options->contains("query-id")) { Glib::ustring query_id; options->lookup_value("query-id", query_id); if (!query_id.empty()) { _command_line_actions.push_back( std::make_pair("select-via-id", Glib::Variant::create(query_id))); } } if (options->contains("query-all")) _command_line_actions.push_back(std::make_pair("query-all", base)); if (options->contains("query-x")) _command_line_actions.push_back(std::make_pair("query-x", base)); if (options->contains("query-y")) _command_line_actions.push_back(std::make_pair("query-y", base)); if (options->contains("query-width")) _command_line_actions.push_back(std::make_pair("query-width", base)); if (options->contains("query-height")) _command_line_actions.push_back(std::make_pair("query-height",base)); // =================== PROCESS ===================== // Note: this won't work with --verb="FileSave,FileClose" unless some additional verb changes the file. FIXME // One can use --verb="FileVacuum,FileSave,FileClose". if (options->contains("vacuum-defs")) _command_line_actions.push_back(std::make_pair("vacuum-defs", base)); if (options->contains("select")) { Glib::ustring select; options->lookup_value("select", select); if (!select.empty()) { _command_line_actions.push_back( std::make_pair("select", Glib::Variant::create(select))); } } if (options->contains("verb")) { Glib::ustring verb; options->lookup_value("verb", verb); if (!verb.empty()) { _command_line_actions.push_back( std::make_pair("verb", Glib::Variant::create(verb))); } } // ==================== EXPORT ===================== if (options->contains("export-file")) { options->lookup_value("export-file", _file_export.export_filename); } if (options->contains("export-type")) { options->lookup_value("export-type", _file_export.export_type); } if (options->contains("export-overwrite")) _file_export.export_overwrite = true; // Export - Geometry if (options->contains("export-area")) { options->lookup_value("export-area", _file_export.export_area); } if (options->contains("export-area-drawing")) _file_export.export_area_drawing = true; if (options->contains("export-area-page")) _file_export.export_area_page = true; if (options->contains("export-margin")) { options->lookup_value("export-margin", _file_export.export_margin); } if (options->contains("export-area-snap")) _file_export.export_area_snap = true; if (options->contains("export-width")) { options->lookup_value("export-width", _file_export.export_width); } if (options->contains("export-height")) { options->lookup_value("export-height", _file_export.export_height); } // Export - Options if (options->contains("export-id")) { options->lookup_value("export-id", _file_export.export_id); } if (options->contains("export-id-only")) _file_export.export_id_only = true; if (options->contains("export-plain-svg")) _file_export.export_plain_svg = true; if (options->contains("export-dpi")) { options->lookup_value("export-dpi", _file_export.export_dpi); } if (options->contains("export-ignore-filters")) _file_export.export_ignore_filters = true; if (options->contains("export-text-to-path")) _file_export.export_text_to_path = true; if (options->contains("export-ps-level")) { options->lookup_value("export-ps-level", _file_export.export_ps_level); } if (options->contains("export-pdf-level")) { options->lookup_value("export-pdf-level", _file_export.export_pdf_level); } if (options->contains("export-latex")) _file_export.export_latex = true; if (options->contains("export-use-hints")) _file_export.export_use_hints = true; if (options->contains("export-background")) { options->lookup_value("export-background",_file_export.export_background); } if (options->contains("export-background-opacity")) { options->lookup_value("export-background-opacity", _file_export.export_background_opacity); } // ==================== D-BUS ====================== #ifdef WITH_DBUS // Before initializing extensions, we must set the DBus bus name if required if (options->contains("dbus-listen")) { std::string dbus_name; options->lookup_value("dbus-name", dbus_name); if (!dbus_name.empty()) { Inkscape::Extension::Dbus::dbus_set_bus_name(dbus_name.c_str()); } } #endif return -1; // Keep going } // ======================== Actions ========================= template void ConcreteInkscapeApplication::on_new() { create_window(); } template void ConcreteInkscapeApplication::on_quit(){ T::quit(); } template<> void ConcreteInkscapeApplication::on_quit() { // Delete all windows (quit() doesn't do this). std::vector windows = get_windows(); for (auto window: windows) { // Do something } quit(); } template class ConcreteInkscapeApplication; template class ConcreteInkscapeApplication; /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :