summaryrefslogtreecommitdiffstats
path: root/src/inkscape-application.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/inkscape-application.cpp
parentInitial commit. (diff)
downloadinkscape-upstream.tar.xz
inkscape-upstream.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/inkscape-application.cpp1736
1 files changed, 1736 insertions, 0 deletions
diff --git a/src/inkscape-application.cpp b/src/inkscape-application.cpp
new file mode 100644
index 0000000..1eebde9
--- /dev/null
+++ b/src/inkscape-application.cpp
@@ -0,0 +1,1736 @@
+// 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.
+ *
+ */
+
+/* Application flow:
+ * main() -> InkscapeApplication::singleton().gio_app()->run(argc, argv);
+ *
+ * InkscapeApplication::InkscapeApplication
+ * Initialized: GC, Debug, Gettext, Autosave, Actions, Commandline
+ * InkscapeApplication::on_handle_local_options
+ * InkscapeAppllication::parse_actions
+ *
+ * -- Switch to main instance if new Inkscape instance is merged with existing instance. --
+ * New instances are merged with existing instance unless app_id is changed, see below.
+ *
+ * InkscapeApplication::on_startup | Only called for main instance
+ *
+ * InkscapeApplication::on_activate (no file specified) OR InkscapeApplication::on_open (file specified)
+ * InkscapeApplication::process_document | Will use command-line actions from main instance!
+ *
+ * InkscapeApplication::create_window (document)
+ * Inkscape::Shortcuts
+ *
+ * InkscapeApplication::create_window (file) |
+ * InkscapeApplication::create_window (document) | Open/Close document
+ * InkscapeApplication::destroy_window |
+ *
+ * InkscapeApplication::on_quit
+ * InkscapeApplication::destroy_all
+ * InkscapeApplication::destroy_window
+ */
+
+#include <iostream>
+#include <iomanip>
+#include <cerrno> // History file
+#include <regex>
+#include <numeric>
+
+// checking if dithering is supported
+#ifdef WITH_PATCHED_CAIRO
+#include "3rdparty/cairo/src/cairo.h"
+#else
+#include <cairo.h>
+#endif
+
+#include <glibmm/i18n.h> // Internationalization
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // Defines ENABLE_NLS
+#endif
+
+#include "inkscape-application.h"
+#include "inkscape-version-info.h"
+#include "inkscape-window.h"
+
+#include "auto-save.h" // Auto-save
+#include "desktop.h" // Access to window
+#include "file.h" // sp_file_convert_dpi
+#include "inkscape.h" // Inkscape::Application
+#include "path-prefix.h" // Data directory
+#include "event-log.h"
+
+#include "include/glibmm_version.h"
+
+#include "inkgc/gc-core.h" // Garbage Collecting init
+#include "debug/logger.h" // INKSCAPE_DEBUG_LOG support
+
+#include "extension/init.h"
+
+#include "io/file.h" // File open (command line).
+#include "io/resource.h" // TEMPLATE
+#include "io/fix-broken-links.h" // Fix up references.
+
+#include "object/sp-root.h" // Inkscape version.
+
+#include "ui/interface.h" // sp_ui_error_dialog
+#include "ui/desktop/document-check.h" // Check for data loss on closing document window.
+#include "ui/desktop/menubar.h"
+#include "ui/dialog/dialog-manager.h" // Save state
+#include "ui/dialog/font-substitution.h" // Warn user about font substitution.
+#include "ui/dialog/startup.h"
+#include "ui/shortcuts.h" // Shortcuts... init
+
+#include "util/units.h" // Redimension window
+
+#include "actions/actions-base.h" // Actions
+#include "actions/actions-file.h" // Actions
+#include "actions/actions-edit.h" // Actions
+#include "actions/actions-effect.h" // Actions
+#include "actions/actions-element-a.h" // Actions
+#include "actions/actions-element-image.h" // Actions
+#include "actions/actions-hide-lock.h" // Actions
+#include "actions/actions-object.h" // Actions
+#include "actions/actions-object-align.h" // Actions
+#include "actions/actions-output.h" // Actions
+#include "actions/actions-paths.h" // Actions
+#include "actions/actions-selection-object.h" // Actions
+#include "actions/actions-selection.h" // Actions
+#include "actions/actions-transform.h" // Actions
+#include "actions/actions-text.h" // Actions
+#include "actions/actions-window.h" // Actions
+#include "actions/actions-undo-document.h"
+
+// With GUI
+#include "actions/actions-tutorial.h" // Actions
+
+#include "widgets/desktop-widget.h" // Access dialog container.
+
+#ifdef ENABLE_NLS
+// Native Language Support - shouldn't this always be used?
+#include "helper/gettext.h" // gettext init
+#endif // ENABLE_NLS
+
+#ifdef WITH_GNU_READLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
+#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.
+
+// 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<InkscapeWindow*>();
+ } 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)
+{
+ auto my_template = Template;
+ if (my_template.empty()) {
+ my_template = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::TEMPLATES, "default.svg", true);
+ }
+
+ // Open file
+ SPDocument *document = ink_file_new(my_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<Gio::File>& file, bool *cancelled)
+{
+ // Open file
+ SPDocument *document = ink_file_open(file, cancelled);
+
+ if (document) {
+ document->setVirgin(false); // Prevents replacing document in same window during file open.
+
+ // Add/promote recent file; when we call add_item and file is on a recent list already,
+ // then apparently only "modified" time changes.
+ if (auto recentmanager = Gtk::RecentManager::get_default()) {
+ recentmanager->add_item(file->get_uri());
+ }
+
+ document_add (document);
+ } else if (cancelled == nullptr || !(*cancelled)) {
+ std::cerr << "InkscapeApplication::document_open: Failed to open: " << file->get_parse_name() << std::endl;
+ }
+
+ return document;
+}
+
+
+// Open a document, add it to app.
+SPDocument*
+InkscapeApplication::document_open(const std::string& data)
+{
+ // Open file
+ SPDocument *document = ink_file_open(data);
+
+ if (document) {
+ document->setVirgin(false); // Prevents replacing document in same window during file open.
+
+ document_add (document);
+ } else {
+ std::cerr << "InkscapeApplication::document_open: Failed to open memory document." << 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);
+
+ // 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);
+
+ _active_document = document;
+ _active_selection = desktop->getSelection();
+ _active_view = desktop;
+ _active_window = window;
+ 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->getDocumentFilename();
+ if (!path) {
+ std::cerr << "InkscapeApplication::revert_document: Document never saved, cannot revert." << std::endl;
+ return false;
+ }
+
+ // Open saved document.
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(document->getDocumentFilename());
+ 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->setVirgin(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->current_center();
+
+ bool reverted = document_swap (it2, new_document);
+
+ if (reverted) {
+ desktop->zoom_absolute(c, zoom, false);
+ /** Update LPE and Fix legacy LPE system **/
+ sp_file_fix_lpe(desktop->getDocument());
+ } 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). MOVE TO ANOTHER FILE!
+ */
+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::fixBrokenLinks(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);
+ }
+ /** Update LPE and Fix legacy LPE system **/
+ sp_file_fix_lpe(document);
+
+ // Check for font substitutions, requires text to have been rendered.
+ Inkscape::UI::Dialog::FontSubstitution::getInstance().checkFontSubstitutions(document);
+ }
+}
+
+/** Get a list of open documents (from document map).
+ */
+std::vector<SPDocument*>
+InkscapeApplication::get_documents()
+{
+ std::vector<SPDocument*> documents;
+ for (auto &i : _documents) {
+ documents.push_back(i.first);
+ }
+ return documents;
+}
+
+
+
+// Take an already open document and create a new window, adding window to document map.
+InkscapeWindow*
+InkscapeApplication::window_open(SPDocument* document)
+{
+ // Once we've removed Inkscape::Application (separating GUI from non-GUI stuff)
+ // it will be more easy to start up the GUI after-the-fact. Until then, prevent
+ // opening a window if GUI not selected at start-up time.
+ if (!_with_gui) {
+ std::cerr << "InkscapeApplication::window_open: Not in gui mode!" << std::endl;
+ return nullptr;
+ }
+
+ InkscapeWindow* window = new InkscapeWindow(document);
+ // TODO Add window to application. (Instead of in InkscapeWindow constructor.)
+
+ // To be removed (add once per window)!
+ INKSCAPE.add_document(document);
+
+ _active_window = window;
+ _active_view = window->get_desktop();
+ _active_selection = window->get_desktop()->getSelection();
+ _active_document = document;
+
+ auto it = _documents.find(document);
+ if (it != _documents.end()) {
+ it->second.push_back(window);
+ } else {
+ std::cerr << "InkscapeApplication::window_open: 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);
+
+ // Leave active document alone (maybe should find new active window and reset variables).
+ _active_selection = nullptr;
+ _active_view = nullptr;
+ _active_window = 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()) {
+ if (get_number_of_windows() == 1) {
+ // persist layout of docked and floating dialogs before deleting the last window
+ Inkscape::UI::Dialog::DialogManager::singleton().save_dialogs_state(
+ window->get_desktop_widget()->getDialogContainer());
+ }
+ it->second.erase(it2);
+ delete window; // Results in call to SPDesktop::destroy()
+ } else {
+ std::cerr << "InkscapeApplication::close_window: window not found!" << std::endl;
+ }
+ } else {
+ std::cerr << "InkscapeApplication::close_window: document not in map!" << std::endl;
+ }
+ } else {
+ std::cerr << "InkscapeApplication::close_window: No document!" << std::endl;
+ }
+
+ } else {
+ std::cerr << "InkscapeApplication::close_window: No window!" << std::endl;
+ }
+
+ // dump();
+}
+
+
+// Closes active window (useful for scripting).
+void
+InkscapeApplication::window_close_active()
+{
+ if (_active_window) {
+ window_close (_active_window);
+ } else {
+ std::cerr << "InkscapeApplication::window_close_active: no active window!" << std::endl;
+ }
+}
+
+
+/** 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<InkscapeWindow*> 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;
+ }
+ }
+}
+
+static InkscapeApplication *_instance = nullptr;
+
+InkscapeApplication &InkscapeApplication::singleton()
+{
+ if (!_instance) {
+ _instance = new InkscapeApplication();
+ }
+ return *_instance;
+}
+
+InkscapeApplication *InkscapeApplication::instance()
+{
+ return _instance;
+}
+
+void
+InkscapeApplication::_start_main_option_section(const Glib::ustring& section_name)
+{
+#ifndef _WIN32
+ // Avoid outputting control characters to non-tty destinations.
+ //
+ // However, isatty() is not useful on Windows
+ // - it doesn't recognize mintty and similar terminals
+ // - it doesn't work in cmd.exe either, where we have to use the inkscape.com wrapper, connecting stdout to a pipe
+ if (!isatty(fileno(stdout))) {
+ return;
+ }
+#endif
+
+ auto *gapp = gio_app();
+
+ if (section_name.empty()) {
+ gapp->add_main_option_entry(Gio::Application::OPTION_TYPE_BOOL, Glib::ustring("\b\b "));
+ } else {
+ gapp->add_main_option_entry(Gio::Application::OPTION_TYPE_BOOL, Glib::ustring("\b\b \n") + section_name + ":");
+ }
+}
+
+InkscapeApplication::InkscapeApplication()
+{
+ using T = Gio::Application;
+
+ auto app_id = Glib::ustring("org.inkscape.Inkscape");
+ auto flags = Gio::APPLICATION_HANDLES_OPEN | // Use default file opening.
+ Gio::APPLICATION_CAN_OVERRIDE_APP_ID;
+ auto non_unique = false;
+
+ // Allow an independent instance of Inkscape to run. Will have matching DBus name and paths
+ // (e.g org.inkscape.Inkscape.tag, /org/inkscape/Inkscape/tag/window/1).
+ // If this flag isn't set, any new instance of Inkscape will be merged with the already running
+ // instance of Inkscape before on_open() or on_activate() is called.
+ if (Glib::getenv("INKSCAPE_APP_ID_TAG") != "") {
+ flags |= Gio::APPLICATION_CAN_OVERRIDE_APP_ID;
+ app_id += "." + Glib::getenv("INKSCAPE_APP_ID_TAG");
+ if (!Gio::Application::id_is_valid(app_id)) {
+ std::cerr << "InkscapeApplication: invalid application id: " << app_id << std::endl;
+ std::cerr << " tag must be ASCII and not start with a number." << std::endl;
+ }
+ non_unique = true;
+ }
+
+ if (gtk_init_check(nullptr, nullptr)) {
+ g_set_prgname(app_id.c_str());
+ _gio_application = Gtk::Application::create(app_id, flags);
+ } else {
+ _gio_application = Gio::Application::create(app_id, flags);
+ _with_gui = false;
+ }
+
+ auto *gapp = gio_app();
+
+ gapp->signal_startup().connect([this]() { this->on_startup(); });
+ gapp->signal_activate().connect([this]() { this->on_activate(); });
+ gapp->signal_open().connect(sigc::mem_fun(*this, &InkscapeApplication::on_open));
+
+ // ==================== Initializations =====================
+ // Garbage Collector
+ Inkscape::GC::init();
+
+#ifndef NDEBUG
+ // Use environment variable INKSCAPE_DEBUG_LOG=log.txt for event logging
+ Inkscape::Debug::Logger::init();
+#endif
+
+#ifdef ENABLE_NLS
+ // Native Language Support (shouldn't this always be used?).
+ Inkscape::initialize_gettext();
+#endif
+
+ // Autosave
+ Inkscape::AutoSave::getInstance().init(this);
+
+ // Don't set application name for now. We don't use it anywhere but
+ // it overrides the name used for adding recently opened files and breaks the Gtk::RecentFilter
+ // 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_edit(this); // actions for editing
+ add_actions_effect(this); // actions for Filters and Extensions
+ add_actions_element_a(this); // actions for the SVG a (anchor) element
+ add_actions_element_image(this); // actions for the SVG image element
+ add_actions_file(this); // actions for file handling
+ add_actions_hide_lock(this); // actions for hiding/locking items.
+ add_actions_object(this); // actions for object manipulation
+ add_actions_object_align(this); // actions for object alignment
+ add_actions_output(this); // actions for file export
+ add_actions_selection(this); // actions for object selection
+ add_actions_path(this); // actions for Paths
+ add_actions_selection_object(this); // actions for selected objects
+ add_actions_text(this); // actions for Text
+ add_actions_tutorial(this); // actions for opening tutorials (with GUI only)
+ add_actions_transform(this); // actions for transforming selected objects
+ add_actions_window(this); // actions for windows
+ add_actions_undo_app(this); // Actions to undo/redo from the application level
+
+ // ====================== Command Line ======================
+
+ // Will automatically handle character conversions.
+ // Note: OPTION_TYPE_FILENAME => std::string, OPTION_TYPE_STRING => Glib::ustring.
+
+#if GLIBMM_CHECK_VERSION(2,56,0)
+ // Additional informational strings for --help output
+ // TODO: Claims to be translated automatically, but seems broken, so pass already translated strings
+ gapp->set_option_context_parameter_string(_("file1 [file2 [fileN]]"));
+ gapp->set_option_context_summary(_("Process (or open) one or more files."));
+ gapp->set_option_context_description(Glib::ustring("\n") + _("Examples:") + '\n'
+ + " " + Glib::ustring::compose(_("Export input SVG (%1) to PDF (%2) format:"), "in.svg", "out.pdf") + '\n'
+ + '\t' + "inkscape --export-filename=out.pdf in.svg\n"
+ + " " + Glib::ustring::compose(_("Export input files (%1) to PNG format keeping original name (%2):"), "in1.svg, in2.svg", "in1.png, in2.png") + '\n'
+ + '\t' + "inkscape --export-type=png in1.svg in2.svg\n"
+ + " " + Glib::ustring::compose(_("See %1 and %2 for more details."), "'man inkscape'", "http://wiki.inkscape.org/wiki/index.php/Using_the_Command_Line"));
+#endif
+
+ // clang-format off
+ // General
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "version", 'V', N_("Print Inkscape version"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "debug-info", '\0', N_("Print debugging information"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "system-data-directory", '\0', N_("Print system data directory"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "user-data-directory", '\0', N_("Print user data directory"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "app-id-tag", '\0', N_("Create a unique instance of Inkscape with the application ID 'org.inkscape.Inkscape.TAG'"), "");
+
+ // Open/Import
+ _start_main_option_section(_("File import"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "pipe", 'p', N_("Read input file from standard input (stdin)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_INT, "pdf-page", '\0', N_("PDF page number to import"), N_("PAGE"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "pdf-poppler", '\0', N_("Use poppler when importing via commandline"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "convert-dpi-method", '\0', N_("Method used to convert pre-0.92 document dpi, if needed: [none|scale-viewbox|scale-document]"), N_("METHOD"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "no-convert-text-baseline-spacing", '\0', N_("Do not fix pre-0.92 document's text baseline spacing on opening"), "");
+
+ // Export - File and File Type
+ _start_main_option_section(_("File export"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_FILENAME, "export-filename", 'o', N_("Output file name (defaults to input filename; file type is guessed from extension if present; use '-' to write to stdout)"), N_("FILENAME"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-overwrite", '\0', N_("Overwrite input file (otherwise add '_out' suffix if type doesn't change)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-type", '\0', N_("File type(s) to export: [svg,png,ps,eps,pdf,emf,wmf,xaml]"), N_("TYPE[,TYPE]*"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-extension", '\0', N_("Extension ID to use for exporting"), N_("EXTENSION-ID"));
+
+ // Export - Geometry
+ _start_main_option_section(_("Export geometry")); // B = PNG, S = SVG, P = PS/EPS/PDF
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-page", 'C', N_("Area to export is page"), ""); // BSP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-drawing", 'D', N_("Area to export is whole drawing (ignoring page size)"), ""); // BSP
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-area", 'a', N_("Area to export in SVG user units"), N_("x0:y0:x1:y1")); // BSP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-snap", '\0', N_("Snap the bitmap export area outwards to the nearest integer values"), ""); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_DOUBLE, "export-dpi", 'd', N_("Resolution for bitmaps and rasterized filters; default is 96"), N_("DPI")); // BxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_INT, "export-width", 'w', N_("Bitmap width in pixels (overrides --export-dpi)"), N_("WIDTH")); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_INT, "export-height", 'h', N_("Bitmap height in pixels (overrides --export-dpi)"), N_("HEIGHT")); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_INT, "export-margin", '\0', N_("Margin around export area: units of page size for SVG, mm for PS/PDF"), N_("MARGIN")); // xSP
+
+ // Export - Options
+ _start_main_option_section(_("Export options"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-id", 'i', N_("ID(s) of object(s) to export"), N_("OBJECT-ID[;OBJECT-ID]*")); // BSP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-id-only", 'j', N_("Hide all objects except object with ID selected by export-id"), ""); // BSx
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-plain-svg", 'l', N_("Remove Inkscape-specific SVG attributes/properties"), ""); // xSx
+ gapp->add_main_option_entry(T::OPTION_TYPE_INT, "export-ps-level", '\0', N_("Postscript level (2 or 3); default is 3"), N_("LEVEL")); // xxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-pdf-version", '\0', N_("PDF version (1.4 or 1.5); default is 1.5"), N_("VERSION")); // xxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-text-to-path", 'T', N_("Convert text to paths (PS/EPS/PDF/SVG)"), ""); // xxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-latex", '\0', N_("Export text separately to LaTeX file (PS/EPS/PDF)"), ""); // xxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-ignore-filters", '\0', N_("Render objects without filters instead of rasterizing (PS/EPS/PDF)"), ""); // xxP
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-use-hints", 't', N_("Use stored filename and DPI hints when exporting object selected by --export-id"), ""); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-background", 'b', N_("Background color for exported bitmaps (any SVG color string)"), N_("COLOR")); // Bxx
+ // FIXME: Opacity should really be a DOUBLE, but an upstream bug means 0.0 is detected as NULL
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-background-opacity", 'y', N_("Background opacity for exported bitmaps (0.0 to 1.0, or 1 to 255)"), N_("VALUE")); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-png-color-mode", '\0', N_("Color mode (bit depth and color type) for exported bitmaps (Gray_1/Gray_2/Gray_4/Gray_8/Gray_16/RGB_8/RGB_16/GrayAlpha_8/GrayAlpha_16/RGBA_8/RGBA_16)"), N_("COLOR-MODE")); // Bxx
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "export-png-use-dithering", '\0', N_("Force dithering or disables it"), "false|true"); // Bxx
+
+ // Query - Geometry
+ _start_main_option_section(_("Query object/document geometry"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "query-id", 'I', N_("ID(s) of object(s) to be queried"), N_("OBJECT-ID[,OBJECT-ID]*"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-all", 'S', N_("Print bounding boxes of all objects"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-x", 'X', N_("X coordinate of drawing or object (if specified by --query-id)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-y", 'Y', N_("Y coordinate of drawing or object (if specified by --query-id)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-width", 'W', N_("Width of drawing or object (if specified by --query-id)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-height", 'H', N_("Height of drawing or object (if specified by --query-id)"), "");
+
+ // Processing
+ _start_main_option_section(_("Advanced file processing"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "vacuum-defs", '\0', N_("Remove unused definitions from the <defs> section(s) of document"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "select", '\0', N_("Select objects: comma-separated list of IDs"), N_("OBJECT-ID[,OBJECT-ID]*"));
+
+ // Actions
+ _start_main_option_section();
+ gapp->add_main_option_entry(T::OPTION_TYPE_STRING, "actions", 'a', N_("List of actions (with optional arguments) to execute"), N_("ACTION(:ARG)[;ACTION(:ARG)]*"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "action-list", '\0', N_("List all available actions"), "");
+
+ // Interface
+ _start_main_option_section(_("Interface"));
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "with-gui", 'g', N_("With graphical user interface (required by some actions)"), "");
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "batch-process", '\0', N_("Close GUI after executing all actions"), "");
+ _start_main_option_section();
+ gapp->add_main_option_entry(T::OPTION_TYPE_BOOL, "shell", '\0', N_("Start Inkscape in interactive shell mode"), "");
+
+ // clang-format on
+
+ gapp->signal_handle_local_options().connect(sigc::mem_fun(*this, &InkscapeApplication::on_handle_local_options));
+
+ if (_with_gui && !non_unique) { // Will fail to register if not unique.
+ // On macOS, this enables:
+ // - DnD via dock icon
+ // - system menu "Quit"
+ gtk_app()->property_register_session() = true;
+ }
+}
+
+/** Create a window given a document. This is used internally in InkscapeApplication.
+ */
+InkscapeWindow*
+InkscapeApplication::create_window(SPDocument *document, bool replace)
+{
+ if (!gtk_app()) {
+ g_assert_not_reached();
+ return nullptr;
+ }
+
+ SPDocument *old_document = _active_document;
+ InkscapeWindow* window = InkscapeApplication::get_active_window();
+
+ if (replace && old_document && 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);
+ }
+ }
+ } else {
+ window = window_open (document);
+ }
+ window->show();
+
+ return window;
+}
+
+
+/** Create a window given a Gio::File. This is what most external functions should call.
+ *
+ * @param file - The filename to open as a Gio::File object
+*/
+void
+InkscapeApplication::create_window(const Glib::RefPtr<Gio::File>& file)
+{
+ if (!gtk_app()) {
+ g_assert_not_reached();
+ return;
+ }
+
+ SPDocument* document = nullptr;
+ InkscapeWindow* window = nullptr;
+ bool cancelled = false;
+
+ if (file) {
+ startup_close();
+ document = document_open(file, &cancelled);
+ if (document) {
+ // Remember document so much that we'll add it to recent documents
+ auto recentmanager = Gtk::RecentManager::get_default();
+ recentmanager->add_item (file->get_uri());
+
+ SPDocument* old_document = _active_document;
+ bool replace = old_document && old_document->getVirgin();
+
+ window = create_window (document, replace);
+ document_fix(window);
+ } else if (!cancelled) {
+ std::cerr << "ConcreteInkscapeApplication<T>::create_window: Failed to load: "
+ << file->get_parse_name() << std::endl;
+
+ gchar *text = g_strdup_printf(_("Failed to load the requested file %s"), file->get_parse_name().c_str());
+ sp_ui_error_dialog(text);
+ g_free(text);
+ }
+
+ } else {
+ document = document_new ();
+ if (document) {
+ window = window_open (document);
+ } else {
+ std::cerr << "ConcreteInkscapeApplication<T>::create_window: Failed to open default document!" << std::endl;
+ }
+ }
+
+ _active_document = document;
+ _active_window = window;
+}
+
+/** Destroy a window and close the document it contains. Aborts if document needs saving.
+ * Replaces document and keeps window open if last window and keep_alive is true.
+ * Returns true if window destroyed.
+ */
+bool
+InkscapeApplication::destroy_window(InkscapeWindow* window, bool keep_alive)
+{
+ if (!gtk_app()) {
+ g_assert_not_reached();
+ return false;
+ }
+
+ SPDocument* document = window->get_document();
+
+ if (!document) {
+ std::cerr << "InkscapeApplication::destroy_window: window has no document!" << std::endl;
+ return false;
+ }
+
+ // Remove document if no window with document is left.
+ 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 = document_check_for_data_loss(window);
+ if (abort) {
+ return false;
+ }
+ }
+
+ if (get_number_of_windows() == 1 && keep_alive) {
+ // Last window, replace with new document.
+ auto new_document = document_new();
+ document_swap(window, new_document);
+ } else {
+ window_close(window);
+ if (get_number_of_windows() == 0) {
+ // No Inkscape windows left, remove dialog windows.
+ for (auto const &window : gtk_app()->get_windows()) {
+ window->close();
+ }
+ }
+ }
+
+ if (it->second.size() == 0) {
+ // No window contains document so let's close it.
+ document_close (document);
+ }
+
+ } else {
+ std::cerr << "ConcreteInkscapeApplication<Gtk::Application>::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;
+}
+
+bool
+InkscapeApplication::destroy_all()
+{
+ if (!gtk_app()) {
+ g_assert_not_reached();
+ return false;
+ }
+
+ while (_documents.size() != 0) {
+ auto it = _documents.begin();
+ if (!it->second.empty()) {
+ auto it2 = it->second.begin();
+ if (!destroy_window (*it2)) {
+ return false; // If destroy aborted, we need to stop exit.
+ }
+ }
+ }
+ return true;
+}
+
+/** Common processing for documents
+ */
+void
+InkscapeApplication::process_document(SPDocument* document, std::string output_path)
+{
+ // Add to Inkscape::Application...
+ INKSCAPE.add_document(document);
+
+ // Are we doing one file at a time? In that case, we don't recreate new windows for each file.
+ bool replace = _use_pipe || _batch_process;
+
+ // Open window if needed (reuse window if we are doing one file at a time inorder to save overhead).
+ _active_document = document;
+ if (_with_gui) {
+ _active_window = create_window(document, replace);
+ _active_view = _active_window->get_desktop();
+ } else {
+ _active_window = nullptr;
+ _active_view = nullptr;
+ _active_selection = document->getSelection();
+ }
+
+ document->ensureUpToDate(); // Or queries don't work!
+
+ // process_file
+ for (auto action: _command_line_actions) {
+ if (!_gio_application->has_action(action.first)) {
+ std::cerr << "ConcreteInkscapeApplication<T>::process_document: Unknown action name: " << action.first << std::endl;
+ }
+ _gio_application->activate_action( action.first, action.second );
+ }
+
+ if (_use_shell) {
+ shell();
+ }
+ if (_with_gui && _active_window) {
+ document_fix(_active_window);
+ }
+ // Only if --export-filename, --export-type --export-overwrite, or --export-use-hints are used.
+ if (_auto_export) {
+ // Save... can't use action yet.
+ _file_export.do_export(document, output_path);
+ }
+}
+
+/*
+ * Called on first Inkscape instance creation. Not called if a new Inkscape instance is merged
+ * with an existing instance.
+ */
+void
+InkscapeApplication::on_startup()
+{
+ // Deprecated...
+ Inkscape::Application::create(_with_gui);
+
+ // Extensions
+ Inkscape::Extension::init();
+
+ // Command line execution. Must be after Extensions are initialized.
+ parse_actions(_command_line_actions_input, _command_line_actions);
+
+ if (!_with_gui) {
+ return;
+ }
+
+ auto *gapp = gio_app();
+
+ // ======================= Actions (GUI) ======================
+ gapp->add_action("new", sigc::mem_fun(*this, &InkscapeApplication::on_new ));
+ gapp->add_action("quit", sigc::mem_fun(*this, &InkscapeApplication::on_quit ));
+
+ // ========================= GUI Init =========================
+ Gtk::Window::set_default_icon_name("org.inkscape.Inkscape");
+
+ // build_menu(); // Builds and adds menu to app. Used by all Inkscape windows. This can be done
+ // before all actions defined. * For the moment done by each window so we can add
+ // window action info to menu_label_to_tooltip map.
+}
+
+// Open document window with default document or pipe. Either this or on_open() is called.
+void
+InkscapeApplication::on_activate()
+{
+ std::string output;
+
+ // Create new document, either from pipe or from template.
+ SPDocument *document = nullptr;
+ auto prefs = Inkscape::Preferences::get();
+
+ if (_use_pipe) {
+
+ // Create document from pipe in.
+ std::istreambuf_iterator<char> begin(std::cin), end;
+ std::string s(begin, end);
+ document = document_open (s);
+ output = "-";
+
+ } else if(prefs->getBool("/options/boot/enabled", true)
+ && !_use_command_line_argument
+ && (gtk_app() && gtk_app()->get_windows().empty())) {
+
+ Inkscape::UI::Dialog::StartScreen start_screen;
+
+ // add start window to gtk_app to ensure proper closing on quit
+ gtk_app()->add_window(start_screen);
+
+ start_screen.run();
+ document = start_screen.get_document();
+ } else {
+
+ // Create a blank document from template
+ document = document_new();
+ }
+ startup_close();
+
+ if (!document) {
+ std::cerr << "ConcreteInkscapeApplication::on_activate: failed to create document!" << std::endl;
+ return;
+ }
+
+ // Process document (command line actions, shell, create window)
+ process_document (document, output);
+
+ if (_batch_process) {
+ // If with_gui, we've reused a window for each file. We must quit to destroy it.
+ gio_app()->quit();
+ }
+}
+
+void
+InkscapeApplication::startup_close()
+{
+ if (auto app = gtk_app()) {
+ // Close any open start screens preventing double opens
+ for (auto win : app->get_windows()) {
+ if (auto start = dynamic_cast<Inkscape::UI::Dialog::StartScreen *>(win)) {
+ start->close();
+ }
+ }
+ }
+}
+
+// Open document window for each file. Either this or on_activate() is called.
+// type_vec_files == std::vector<Glib::RefPtr<Gio::File> >
+void
+InkscapeApplication::on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint)
+{
+ if(_pdf_poppler)
+ INKSCAPE.set_pdf_poppler(_pdf_poppler);
+ if(_pdf_page)
+ INKSCAPE.set_pdf_page(_pdf_page);
+
+ if (files.size() > 1 && !_file_export.export_filename.empty()) {
+ std::cerr << "ConcreteInkscapeApplication<Gtk::Application>::on_open: "
+ "Can't use '--export-filename' with multiple input files "
+ "(output file would be overwritten for each input file). "
+ "Please use '--export-type' instead and rename manually."
+ << std::endl;
+ return;
+ }
+
+ startup_close();
+ for (auto file : files) {
+
+ // Open file
+ SPDocument *document = document_open (file);
+ if (!document) {
+ std::cerr << "ConcreteInkscapeApplication::on_open: failed to create document!" << std::endl;
+ continue;
+ }
+
+ // Process document (command line actions, shell, create window)
+ process_document (document, file->get_path());
+ }
+
+ if (_batch_process) {
+ // If with_gui, we've reused a window for each file. We must quit to destroy it.
+ gio_app()->quit();
+ }
+}
+
+void
+InkscapeApplication::parse_actions(const Glib::ustring& input, action_vector_t& action_vector)
+{
+ const auto re_colon = Glib::Regex::create("\\s*:\\s*");
+
+ // Split action list
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*;\\s*", input);
+ for (auto token : tokens) {
+ // Note: split into 2 tokens max ("param:value"); allows value to contain colon (e.g. abs. paths on Windows)
+ std::vector<Glib::ustring> tokens2 = re_colon->split(token, 0, static_cast<Glib::RegexMatchFlags>(0), 2);
+ std::string action;
+ std::string value;
+ if (tokens2.size() > 0) {
+ action = tokens2[0];
+ }
+ if (action.find_first_not_of(" \f\n\r\t\v") == std::string::npos) {
+ continue;
+ }
+ if (tokens2.size() > 1) {
+ value = tokens2[1];
+ }
+
+ Glib::RefPtr<Gio::Action> 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 == "1" || value == "true" || value.empty()) {
+ b = true;
+ } else if (value == "0" || 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<bool>::create(b)));
+ } else if (type.get_string() == "i") {
+ action_vector.push_back(
+ std::make_pair( action, Glib::Variant<int>::create(std::stoi(value))));
+ } else if (type.get_string() == "d") {
+ action_vector.push_back(
+ std::make_pair( action, Glib::Variant<double>::create(std::stod(value))));
+ } else if (type.get_string() == "s") {
+ action_vector.push_back(
+ std::make_pair( action, Glib::Variant<Glib::ustring>::create(value) ));
+ } else if (type.get_string() == "(dd)") {
+ std::vector<Glib::ustring> tokens3 = Glib::Regex::split_simple(",", value);
+ if (tokens3.size() != 2) {
+ std::cerr << "InkscapeApplication::parse_actions: " << action << " requires two comma separated numbers" << std::endl;
+ continue;
+ }
+
+ double d0 = 0;
+ double d1 = 0;
+ try {
+ d0 = std::stod(tokens3[0]);
+ d1 = std::stod(tokens3[1]);
+ } catch (...) {
+ std::cerr << "InkscapeApplication::parse_actions: " << action << " requires two comma separated numbers" << std::endl;
+ continue;
+ }
+
+ action_vector.push_back(
+ std::make_pair( action, Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1))));
+ } 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 {
+ std::cerr << "InkscapeApplication::parse_actions: could not find action for: " << action << std::endl;
+ }
+ }
+}
+
+#ifdef WITH_GNU_READLINE
+
+// For use in shell mode. Command completion of action names.
+char* readline_generator (const char* text, int state)
+{
+ static std::vector<Glib::ustring> actions;
+
+ // Fill the vector of action names.
+ if (actions.size() == 0) {
+ auto *app = InkscapeApplication::instance();
+ actions = app->gio_app()->list_actions();
+ std::sort(actions.begin(), actions.end());
+ }
+
+ static int list_index = 0;
+ static int len = 0;
+
+ if (!state) {
+ list_index = 0;
+ len = strlen(text);
+ }
+
+ const char* name = nullptr;
+ while (list_index < actions.size()) {
+ name = actions[list_index].c_str();
+ list_index++;
+ if (strncmp (name, text, len) == 0) {
+ return (strdup(name));
+ }
+ }
+
+ return ((char*)nullptr);
+}
+
+char** readline_completion(const char* text, int start, int end)
+{
+ char **matches = (char**)nullptr;
+
+ // Match actions names, but only at start of line.
+ // It would be nice to also match action names after a ';' but it's not possible as text won't include ';'.
+ if (start == 0) {
+ matches = rl_completion_matches (text, readline_generator);
+ }
+
+ return (matches);
+}
+
+void readline_init()
+{
+ rl_readline_name = "inkscape";
+ rl_attempted_completion_function = readline_completion;
+}
+#endif // WITH_GNU_READLINE
+
+
+// Once we don't need to create a window just to process verbs!
+void
+InkscapeApplication::shell()
+{
+ std::cout << "Inkscape interactive shell mode. Type 'action-list' to list all actions. "
+ << "Type 'quit' to quit." << std::endl;
+ std::cout << " Input of the form:" << std::endl;
+ std::cout << " action1:arg1; action2:arg2; ..." << std::endl;
+ if (!_with_gui) {
+ std::cout << "Only actions that don't require a desktop may be used." << std::endl;
+ }
+
+#ifdef WITH_GNU_READLINE
+ auto history_file = Glib::build_filename(Inkscape::IO::Resource::profile_path(), "shell.history");
+
+#ifdef _WIN32
+ gchar *locale_filename = g_win32_locale_filename_from_utf8(history_file.c_str());
+ if (locale_filename) {
+ history_file = locale_filename;
+ g_free(locale_filename);
+ }
+#endif
+
+ static bool init = false;
+ if (!init) {
+ readline_init();
+ using_history();
+ init = true;
+
+ int error = read_history(history_file.c_str());
+ if (error && error != ENOENT) {
+ std::cerr << "read_history error: " << std::strerror(error) << " " << history_file << std::endl;
+ }
+ }
+#endif
+
+ while (std::cin.good()) {
+ bool eof = false;
+ std::string input;
+
+#ifdef WITH_GNU_READLINE
+ char *readline_input = readline("> ");
+ if (readline_input) {
+ input = readline_input;
+ if (input != "quit" && input != "q") {
+ add_history(readline_input);
+ }
+ } else {
+ eof = true;
+ }
+ free(readline_input);
+#else
+ std::cout << "> ";
+ std::getline(std::cin, input);
+#endif
+
+ // Remove trailing space
+ input = std::regex_replace(input, std::regex(" +$"), "");
+
+ if (eof || input == "quit" || input == "q") {
+ break;
+ }
+
+ action_vector_t action_vector;
+ parse_actions(input, action_vector);
+ for (auto action: action_vector) {
+ _gio_application->activate_action( action.first, action.second );
+ }
+
+ // This would allow displaying the results of actions on the fly... but it needs to be well
+ // vetted first.
+ Glib::RefPtr<Glib::MainContext> context = Glib::MainContext::get_default();
+ while (context->iteration(false)) {};
+ }
+
+#ifdef WITH_GNU_READLINE
+ stifle_history(200); // ToDo: Make number a preference.
+ int error = write_history(history_file.c_str());
+ if (error) {
+ std::cerr << "write_history error: " << std::strerror(error) << " " << history_file << std::endl;
+ }
+#endif
+
+ if (_with_gui) {
+ _gio_application->quit(); // Force closing windows.
+ }
+}
+
+
+// ========================= 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.
+ */
+int
+InkscapeApplication::on_handle_local_options(const Glib::RefPtr<Glib::VariantDict>& options)
+{
+ auto prefs = Inkscape::Preferences::get();
+ if (!options) {
+ std::cerr << "InkscapeApplication::on_handle_local_options: options is null!" << std::endl;
+ return -1; // Keep going
+ }
+
+ // ===================== APP ID ====================
+ if (options->contains("app-id-tag")) {
+ Glib::ustring id_tag;
+ options->lookup_value("app-id-tag", id_tag);
+ Glib::ustring app_id = "org.inkscape.Inkscape." + id_tag;
+ if (Gio::Application::id_is_valid(app_id)) {
+ _gio_application->set_id(app_id);
+ } else {
+ std::cerr << "InkscapeApplication: invalid application id: " << app_id << std::endl;
+ std::cerr << " tag must be ASCII and not start with a number." << std::endl;
+ }
+ }
+
+ // ===================== QUERY =====================
+ // These are processed first as they result in immediate program termination.
+ // Note: we cannot use actions here as the app has not been registered yet (registering earlier
+ // causes problems with changing the app id).
+ if (options->contains("version")) {
+ std::cout << Inkscape::inkscape_version() << std::endl;
+ return EXIT_SUCCESS;
+ }
+
+ if (options->contains("debug-info")) {
+ std::cout << Inkscape::debug_info() << std::endl;
+ return EXIT_SUCCESS;
+ }
+
+ if (options->contains("system-data-directory")) {
+ std::cout << Glib::build_filename(get_inkscape_datadir(), "inkscape") << std::endl;
+ return EXIT_SUCCESS;
+ }
+
+ if (options->contains("user-data-directory")) {
+ std::cout << Inkscape::IO::Resource::profile_path("") << std::endl;
+ return EXIT_SUCCESS;
+ }
+
+ // Can't do this until after app is registered!
+ // if (options->contains("action-list")) {
+ // print_action_list();
+ // return EXIT_SUCCESS;
+ // }
+
+ // For options without arguments.
+ auto base = Glib::VariantBase();
+
+ // ================== GUI and Shell ================
+
+ // Use of most command line options turns off use of gui unless explicitly requested!
+ // Listed in order that they appear in constructor.
+ if (options->contains("pipe") ||
+
+ options->contains("export-filename") ||
+ options->contains("export-overwrite") ||
+ options->contains("export-type") ||
+
+ options->contains("export-area-page") ||
+ options->contains("export-area-drawing") ||
+ options->contains("export-area") ||
+ options->contains("export-area-snap") ||
+ options->contains("export-dpi") ||
+ options->contains("export-width") ||
+ options->contains("export-height") ||
+ options->contains("export-margin") ||
+ options->contains("export-height") ||
+
+ options->contains("export-id") ||
+ options->contains("export-id-only") ||
+ options->contains("export-plain-svg") ||
+ options->contains("export-ps-level") ||
+ options->contains("export-pdf-version") ||
+ options->contains("export-text-to_path") ||
+ options->contains("export-latex") ||
+ options->contains("export-ignore-filters") ||
+ options->contains("export-use-hints") ||
+ options->contains("export-background") ||
+ options->contains("export-background-opacity") ||
+ options->contains("export-text-to_path") ||
+
+ 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("vacuum-defs") ||
+ options->contains("select") ||
+ options->contains("action-list") ||
+ options->contains("actions") ||
+ options->contains("shell")
+ ) {
+ _with_gui = false;
+ }
+
+ if (options->contains("with-gui") ||
+ options->contains("batch-process")
+ ) {
+ _with_gui = bool(gtk_app()); // Override turning GUI off
+ if (!_with_gui)
+ std::cerr << "No GUI available, some actions may fail" << std::endl;
+ }
+
+ if (options->contains("batch-process")) _batch_process = true;
+ if (options->contains("shell")) _use_shell = true;
+ if (options->contains("pipe")) _use_pipe = true;
+
+
+ // Enable auto-export
+ if (options->contains("export-filename") ||
+ options->contains("export-type") ||
+ options->contains("export-overwrite") ||
+ options->contains("export-use-hints")
+ ) {
+ _auto_export = true;
+ }
+
+ // If we are running in command-line mode (without gui) and we haven't explicitly changed the app_id,
+ // change it here so that this instance of Inkscape is not merged with an existing instance (otherwise
+ // unwanted windows will pop up and the command-line arguments will be ignored).
+ if (_with_gui == false &&
+ !options->contains("app-id-tag")) {
+ Glib::ustring app_id = "org.inkscape.Inkscape.p" + std::to_string(getpid());
+ _gio_application->set_id(app_id);
+ }
+
+ // ==================== 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", _command_line_actions_input);
+ // Parsing done after extensions initialized.
+ }
+
+ // This must be done after the app has been registered!
+ if (options->contains("action-list")) {
+ _command_line_actions.push_back(std::make_pair("action-list", base));
+ }
+
+ // ================= OPEN/IMPORT ===================
+
+ if (options->contains("pdf-poppler")) {
+ _pdf_poppler = true;
+ }
+ if (options->contains("pdf-page")) { // Maybe useful for other file types?
+ int page = 0;
+ options->lookup_value("pdf-page", page);
+ _pdf_page = 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<Glib::ustring>::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-by-id", Glib::Variant<Glib::ustring>::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 =====================
+
+ 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<Glib::ustring>::create(select)));
+ }
+ }
+
+ // ==================== EXPORT =====================
+ if (options->contains("export-filename")) {
+ options->lookup_value("export-filename", _file_export.export_filename);
+ }
+
+ if (options->contains("export-type")) {
+ options->lookup_value("export-type", _file_export.export_type);
+ }
+ if (options->contains("export-extension")) {
+ options->lookup_value("export-extension", _file_export.export_extension);
+ _file_export.export_extension = _file_export.export_extension.lowercase();
+ }
+
+ 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-version")) {
+ options->lookup_value("export-pdf-version", _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);
+ }
+
+ // FIXME: Upstream bug means DOUBLE is ignored if set to 0.0 so doesn't exist in options
+ if (options->contains("export-background-opacity")) {
+ Glib::ustring opacity;
+ options->lookup_value("export-background-opacity", opacity);
+ _file_export.export_background_opacity = Glib::Ascii::strtod(opacity);
+ }
+
+ if (options->contains("export-png-color-mode")) {
+ options->lookup_value("export-png-color-mode", _file_export.export_png_color_mode);
+ }
+
+ if (options->contains("export-png-use-dithering")) {
+#ifndef CAIRO_HAS_DITHER
+ std::cerr << "Your cairo version does not support dithering! Option will be ignored." << std::endl;
+#endif
+ Glib::ustring val;
+ options->lookup_value("export-png-use-dithering", val);
+ if (val == "true") _file_export.export_png_use_dithering = true;
+ else if (val == "false") _file_export.export_png_use_dithering = false;
+ else std::cerr << "invalid value for export-png-use-dithering. Ignoring." << std::endl;
+ } else _file_export.export_png_use_dithering = prefs->getBool("/options/dithering/render", false);
+
+
+ GVariantDict *options_copy = options->gobj_copy();
+ GVariant *options_var = g_variant_dict_end(options_copy);
+ if (g_variant_get_size(options_var) != 0) {
+ _use_command_line_argument = true;
+ }
+ g_variant_dict_unref(options_copy);
+ g_variant_unref(options_var);
+
+ return -1; // Keep going
+}
+
+// ======================== Actions =========================
+
+void
+InkscapeApplication::on_new()
+{
+ create_window();
+}
+
+void
+InkscapeApplication::on_quit()
+{
+ if (gtk_app()) {
+ if (!destroy_all()) return; // Quit aborted.
+ // For mac, ensure closing the gtk_app windows
+ for (auto window : gtk_app()->get_windows()) {
+ window->close();
+ }
+ }
+
+ gio_app()->quit();
+}
+
+/*
+ * Quit without checking for data loss.
+ */
+void
+InkscapeApplication::on_quit_immediate()
+{
+ gio_app()->quit();
+}
+
+void
+InkscapeApplication::print_action_list()
+{
+ auto const *gapp = gio_app();
+
+ auto actions = gapp->list_actions();
+ std::sort(actions.begin(), actions.end());
+ for (auto const &action : actions) {
+ Glib::ustring fullname("app." + action);
+ std::cout << std::left << std::setw(20) << action
+ << ": " << _action_extra_data.get_tooltip_for_action(fullname) << std::endl;
+ }
+}
+
+/**
+ * Return number of open Inkscape Windows (irrespective of number of documents)
+.*/
+int InkscapeApplication::get_number_of_windows() const {
+ if (_with_gui) {
+ return std::accumulate(_documents.begin(), _documents.end(), 0,
+ [&](int sum, auto& v){ return sum + static_cast<int>(v.second.size()); });
+ }
+ return 0;
+}
+
+void InkscapeApplication::set_active_document(SPDocument* document)
+{
+ _active_document = document;
+ if (document) {
+ // Keep macOS undo menu items upto date.
+ document->get_event_log()->updateUndoVerbs();
+ }
+}
+
+/*
+ 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 :