diff --git a/CMakeLists.txt b/CMakeLists.txt
index f6bb83c9981e9bd70627a624c076288c51cd5316..a1d3a7f780029fdb83db73c2c3d74a151b0e0fbc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -115,6 +115,7 @@ option(WITH_LIBWPG "Compile with support of libwpg for WordPerfect Graphics" ON)
option(WITH_NLS "Compile with Native Language Support (using gettext)" ON)
option(WITH_YAML "Compile with YAML support (enables xverbs)" ON)
option(WITH_JEMALLOC "Compile with JEMALLOC support" ON)
+option(WITH_PYTHON "Compile with embedded Python support" OFF)
option(WITH_FUZZ "Compile for fuzzing purpose (use 'make fuzz' only)" OFF)
mark_as_advanced(WITH_FUZZ)
@@ -312,6 +313,7 @@ message("WITH_OPENMP: ${WITH_OPENMP}")
message("WITH_PROFILING: ${WITH_PROFILING}")
message("WITH_YAML: ${WITH_YAML}")
message("WITH_JEMALLOC: ${WITH_JEMALLOC}")
+message("WITH_PYTHON: ${WITH_PYTHON}")
if(WIN32)
message("")
diff --git a/CMakeScripts/DefineDependsandFlags.cmake b/CMakeScripts/DefineDependsandFlags.cmake
index 4f17c77cf181eec5e5ce67be756242d503db2654..0570aabbded9d90327c5a7a2cd540ebe94a20734 100644
--- a/CMakeScripts/DefineDependsandFlags.cmake
+++ b/CMakeScripts/DefineDependsandFlags.cmake
@@ -359,6 +359,19 @@ if(WITH_YAML)
endif()
endif()
+if(WITH_PYTHON)
+ find_package(PythonLibs)
+ if(PYTHONLIBS_FOUND)
+ set (WITH_PYTHON ON)
+ list(APPEND INKSCAPE_INCS_SYS ${PYTHON_INCLUDE_DIRS})
+ list(APPEND INKSCAPE_LIBS ${PYTHON_LIBRARIES})
+ add_definitions(-DWITH_PYTHON)
+ else(PYTHONLIBS_FOUND)
+ set(WITH_PYTHON OFF)
+ message(STATUS "Could not locate the Python library headers: Embedded Python feature will be disabled")
+ endif()
+endif()
+
list(REMOVE_DUPLICATES INKSCAPE_CXX_FLAGS)
foreach(flag ${INKSCAPE_CXX_FLAGS})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}" CACHE STRING "" FORCE)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8565b9c751c32d9c1c1356591b53dcb0d1181b30..6654a7598857373fad63849ab24ff9a2dd079b5e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -16,6 +16,7 @@ src/display/canvas-axonomgrid.cpp
src/display/canvas-grid.cpp
src/display/snap-indicator.cpp
src/document.cpp
+src/emb_python.cpp
src/event-log.cpp
src/extension/dependency.cpp
src/extension/effect.cpp
@@ -209,6 +210,7 @@ src/object/sp-use.cpp
src/path-chemistry.cpp
src/preferences-skeleton.h
src/preferences.cpp
+src/pyhelper.cpp
src/rdf.cpp
src/selection-chemistry.cpp
src/selection-describer.cpp
@@ -228,6 +230,7 @@ src/ui/dialog/attrdialog.cpp
src/ui/dialog/calligraphic-profile-rename.cpp
src/ui/dialog/clonetiler.cpp
src/ui/dialog/color-item.cpp
+src/ui/dialog/console.cpp
src/ui/dialog/debug.cpp
src/ui/dialog/document-metadata.cpp
src/ui/dialog/document-properties.cpp
diff --git a/share/CMakeLists.txt b/share/CMakeLists.txt
index eb541701474868091616f1ade34f24778cb47907..0fdf03af7d8aceacf48c643fd0b76a8a049efba9 100644
--- a/share/CMakeLists.txt
+++ b/share/CMakeLists.txt
@@ -10,6 +10,7 @@ add_subdirectory(keys)
add_subdirectory(markers)
add_subdirectory(palettes)
add_subdirectory(patterns)
+add_subdirectory(python)
add_subdirectory(screens)
add_subdirectory(symbols)
add_subdirectory(templates)
diff --git a/share/README.md b/share/README.md
index cb136774cb390fa953894775a5a102978e4d405d..65aceed5cb45402a82b35febe2e066a41d0ab4fe 100644
--- a/share/README.md
+++ b/share/README.md
@@ -13,6 +13,7 @@ keys -
markers -
palettes -
patterns -
+python - Support file for embedded Python.
screens -
symbols -
templates -
diff --git a/share/icons/Tango/scalable/actions/dialog-console-script.svg b/share/icons/Tango/scalable/actions/dialog-console-script.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3af580818df49dc046a06af995a738309331aa88
--- /dev/null
+++ b/share/icons/Tango/scalable/actions/dialog-console-script.svg
@@ -0,0 +1,217 @@
+
+
diff --git a/share/icons/Tango/scalable/actions/dialog-console.svg b/share/icons/Tango/scalable/actions/dialog-console.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bdac3544daa7a408d8bcfbd80439c99c7f62a5e3
--- /dev/null
+++ b/share/icons/Tango/scalable/actions/dialog-console.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/share/icons/hicolor/scalable/actions/dialog-console-script.svg b/share/icons/hicolor/scalable/actions/dialog-console-script.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a5a3c0a3f092aa9029254137a9d9dda2148416c8
--- /dev/null
+++ b/share/icons/hicolor/scalable/actions/dialog-console-script.svg
@@ -0,0 +1,217 @@
+
+
diff --git a/share/icons/hicolor/scalable/actions/dialog-console.svg b/share/icons/hicolor/scalable/actions/dialog-console.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bdac3544daa7a408d8bcfbd80439c99c7f62a5e3
--- /dev/null
+++ b/share/icons/hicolor/scalable/actions/dialog-console.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/share/keys/inkscape.xml b/share/keys/inkscape.xml
index 7b674b04e8fa8ccc917df150cb9fdc2bd8859d44..a5eb070ff86b2869581c2ebe8c5d2b8b920b2cf8 100644
--- a/share/keys/inkscape.xml
+++ b/share/keys/inkscape.xml
@@ -630,6 +630,8 @@ override) the bindings in the main default.xml.
+
+
diff --git a/share/python/CMakeLists.txt b/share/python/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d2ae8d566cf02c68fc02b08c16dc9b404439e365
--- /dev/null
+++ b/share/python/CMakeLists.txt
@@ -0,0 +1,6 @@
+file(GLOB _FILES "README" "*.py")
+install(FILES ${_FILES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/python)
+
+add_subdirectory(inkscape)
+add_subdirectory(tests)
+
diff --git a/share/python/README b/share/python/README
new file mode 100644
index 0000000000000000000000000000000000000000..5e4d434750ec029291e11f43f11238dad6ee03f0
--- /dev/null
+++ b/share/python/README
@@ -0,0 +1,4 @@
+This is the directory for global embedded Python files.
+
+Only Python 3 is supported for the Python code executed from within Inkscape.
+
diff --git a/share/python/init.py b/share/python/init.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e77ed7d0190ebdd204b658b26dc43ec42df2621
--- /dev/null
+++ b/share/python/init.py
@@ -0,0 +1,39 @@
+# Inkscape Python internal initialization
+#
+# Authors:
+# Thomas Wiesner
+#
+# Copyright (C) 2018 Authors
+#
+# Released under GNU GPL, read the file 'COPYING' for more information
+#
+
+import sys
+import os
+import _inkscape
+
+# Functions for stdout and stderr redirection.
+class StdOutRedir:
+ def write(self, line):
+ _inkscape.conAppendStdOut(line)
+
+class StdErrRedir:
+ def write(self, line):
+ _inkscape.conAppendStdErr(line)
+
+print('init.py: Script running in ' + _inkscape.getMode() + ' mode')
+
+# If Inkscape was started in GUI mode, we want to setup the redirection
+# for stdout and stderr. We do this very early on, to make possible
+# later errors show up in the GUI console.
+if _inkscape.getMode() == 'gui':
+ print('init.py: Setting up stdout and stderr redirection.')
+ sys.stderr = StdErrRedir()
+ sys.stdout = StdOutRedir()
+
+# Setup path to additional Python ressources.
+inkscapePath = os.path.join(_inkscape.getPythonPath(), 'inkscape')
+sys.path.append(inkscapePath)
+import inkscape
+
+
diff --git a/share/python/inkscape/CMakeLists.txt b/share/python/inkscape/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7c1b3cfe4162fca1b74837635caabbb7e3331ee8
--- /dev/null
+++ b/share/python/inkscape/CMakeLists.txt
@@ -0,0 +1,3 @@
+file(GLOB _FILES "*.py")
+install(FILES ${_FILES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/python/inkscape)
+
diff --git a/share/python/inkscape/inkscape.py b/share/python/inkscape/inkscape.py
new file mode 100644
index 0000000000000000000000000000000000000000..808f107f76ee93d1256d2c65c4fe09f310f2761d
--- /dev/null
+++ b/share/python/inkscape/inkscape.py
@@ -0,0 +1,4 @@
+# Currently only one simple test function.
+def inkscapetest():
+ print('Hello from inkscapetest')
+
diff --git a/share/python/tests/CMakeLists.txt b/share/python/tests/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fb1cc84c50e1e540ea0c1f0682c9a69786083678
--- /dev/null
+++ b/share/python/tests/CMakeLists.txt
@@ -0,0 +1,3 @@
+file(GLOB _FILES "README" "*.py" "*.svg")
+install(FILES ${_FILES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/python/tests)
+
diff --git a/share/python/tests/README b/share/python/tests/README
new file mode 100644
index 0000000000000000000000000000000000000000..1660459ac8b022f99145ad6953d2488137d87f5b
--- /dev/null
+++ b/share/python/tests/README
@@ -0,0 +1,5 @@
+This directory contains a collection of Python scripts for testing
+the functionality and serving as a starting point for custom scripts.
+
+Note that Python 3 syntax is required for scripts running within Inkscape.
+
diff --git a/share/python/tests/exportPNGCMDline.py b/share/python/tests/exportPNGCMDline.py
new file mode 100644
index 0000000000000000000000000000000000000000..d42c665af5a7d2dcb11aca665d8f34375b93f635
--- /dev/null
+++ b/share/python/tests/exportPNGCMDline.py
@@ -0,0 +1,32 @@
+# Test file demonstrating opening and PNG exporting from the command line
+
+import os
+import tempfile
+
+if _inkscape.getMode() == 'gui':
+ # Since this script is for testing the export from the command line,
+ # it should be called accordingly.
+ print("This test must be run without GUI.");
+else:
+ # Script parameter must be the filename to export. Get it and check it.
+ fn = _inkscape.getScriptParam()
+ if len(fn) == 0:
+ print("No filename supplied via --python-script-argument")
+ else:
+ # Open the file
+ _inkscape.fileOpen(fn)
+
+ # Get system directory for temp-files and create an absolute path for the export output.
+ tempdir = tempfile.gettempdir()
+ outPNG = os.path.join(tempdir, 'export.png')
+
+ # Define the export settings
+ dpi = 150
+ area = 'page'
+ areaSnap = 1
+
+ # Run the export
+ _inkscape.exportPNG(outPNG, dpi, area, areaSnap)
+
+ print("Exported file saved to " + outPNG)
+
diff --git a/share/python/tests/inkscape_mode.py b/share/python/tests/inkscape_mode.py
new file mode 100644
index 0000000000000000000000000000000000000000..00f26b8ed6bf084da5e24e386b9bc057ab03c881
--- /dev/null
+++ b/share/python/tests/inkscape_mode.py
@@ -0,0 +1,17 @@
+# Test for getting the Inkscape mode (GUI or commandline).
+
+# Test command line mode with
+# inkscape --without-gui --python-script=inkscape_mode.py --python-script-argument="Test argument"
+
+mode = _inkscape.getMode()
+
+if mode == 'gui':
+ print("Inkscape is running in GUI mode")
+elif mode == 'commandline':
+ print("Inkscape is running in commandline mode")
+
+ param = _inkscape.getScriptParam()
+ print('The script parameter is "' + param + '"')
+else:
+ print('ERROR: Mode should either be "gui" or "commandline" but is "' + mode + '"')
+
diff --git a/share/python/tests/misc.py b/share/python/tests/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebcb48f9d5d9669ce3199bf16ab21ce5788a907c
--- /dev/null
+++ b/share/python/tests/misc.py
@@ -0,0 +1,5 @@
+# Misc tests
+
+print('Inkscape version: ' + _inkscape.getVersion())
+print('Python ressource directory: ' + _inkscape.getPythonPath())
+
diff --git a/share/python/tests/openSaveExport.py b/share/python/tests/openSaveExport.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5b047366f099d90cbbe19eeb23a43b89e77d2b6
--- /dev/null
+++ b/share/python/tests/openSaveExport.py
@@ -0,0 +1,32 @@
+# Test file demonstrating opening, saving and PNG exporting
+
+import os
+import tempfile
+
+# Get the path to the splash screen
+parent = os.path.split(_inkscape.getPythonPath())
+parent = parent[0]
+fnam = os.path.join(parent, 'screens/about.svg')
+
+# Open the file
+_inkscape.fileOpen(fnam)
+
+tempdir = tempfile.gettempdir()
+
+outPDF = os.path.join(tempdir, 'about.pdf')
+outPNG = os.path.join(tempdir, 'about.png')
+
+# Try to save as PDF. This may pop up user interaction dialogs.
+_inkscape.fileSaveCopy(outPDF, 'application/pdf')
+
+# Define the export settings
+dpi = 150
+area = 'page'
+areaSnap = 1
+
+# Run the export
+_inkscape.exportPNG(outPNG, dpi, area, areaSnap)
+
+print('Wrote about.pdf and about.png to ' + tempdir)
+
+
diff --git a/share/python/tests/outputMIMEs.py b/share/python/tests/outputMIMEs.py
new file mode 100644
index 0000000000000000000000000000000000000000..8775c2c746794a69c7d97c2c877c34a577445951
--- /dev/null
+++ b/share/python/tests/outputMIMEs.py
@@ -0,0 +1,14 @@
+# Dump the available output MIME types
+
+mimes = _inkscape.getOutputMIMEs()
+
+for i in mimes:
+ name = _inkscape.getOutputFileTypeName(i)
+ tooltip = _inkscape.getOutputFileTooltip(i)
+ extension = _inkscape.getOutputFileExtension(i)
+
+ print('MIME: ' + i)
+ print(' Name: ' + name)
+ print(' Extension: ' + extension)
+ print(' Tooltip: ' + tooltip)
+
diff --git a/share/python/tests/verbs.py b/share/python/tests/verbs.py
new file mode 100644
index 0000000000000000000000000000000000000000..045dc3ef2cd30722f0dd62c671870ef674583748
--- /dev/null
+++ b/share/python/tests/verbs.py
@@ -0,0 +1,22 @@
+# Tests for obtaining and running Inkscape verbs
+
+def dumpVerbs():
+ # Get all of Inkscape's verbs
+ verbs = _inkscape.getVerbs()
+
+ # Loop over the list, get the names and tooltip texts and produce a nice output.
+ for i in verbs:
+ name = _inkscape.getVerbName(i)
+ tooltip = _inkscape.getVerbTooltip(i)
+
+ print('Verb: ' + i)
+ print(' Name: ' + name)
+ print(' Tooltip: ' + tooltip)
+
+def runVerbTest():
+ # Just toggle the grid to show how to run verbs.
+ _inkscape.runVerbs(['ToggleGrid'])
+
+dumpVerbs()
+
+runVerbTest()
diff --git a/share/ui/CMakeLists.txt b/share/ui/CMakeLists.txt
index 7b1450bd59a3578994f12e83af909e4acba7b02f..f42cfa1b7ecb43a8f7bf912b31e0f2454510b6c4 100644
--- a/share/ui/CMakeLists.txt
+++ b/share/ui/CMakeLists.txt
@@ -1,4 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
+
file(GLOB _FILES "*.xml" "*.rc" "*.css" "*.ui" "*.glade")
install(FILES ${_FILES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/ui)
+
+# The menus.xml file needs more attention, since the menu entries may need to be
+# enabled or disabled depending on the configuration.
+if(WITH_PYTHON)
+ set(dialog_console "")
+else(WITH_PYTHON)
+ set(dialog_console "")
+endif()
+
+configure_file(menus.xml.in menus.xml)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/menus.xml DESTINATION ${INKSCAPE_SHARE_INSTALL}/ui)
+
diff --git a/share/ui/menus.xml b/share/ui/menus.xml.in
similarity index 99%
rename from share/ui/menus.xml
rename to share/ui/menus.xml.in
index fe11177935b0223a1d6564864c55ec179359128d..178dbe7ec045a40b39febdbcc24ab929e0520126 100644
--- a/share/ui/menus.xml
+++ b/share/ui/menus.xml.in
@@ -82,6 +82,7 @@
+ ${dialog_console}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5e4ca8fc8914267d1078ab873c29cc6cce406d89..0d2e18937942a8f5b110e8a11d198ffb89b5afd6 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -24,6 +24,8 @@ set(inkscape_SRC
document-undo.cpp
document.cpp
ege-color-prof-tracker.cpp
+ emb_python.cpp
+ pyhelper.cpp
event-log.cpp
extract-uri.cpp
file.cpp
@@ -124,6 +126,8 @@ set(inkscape_SRC
document-undo.h
document.h
ege-color-prof-tracker.h
+ emb_python.h
+ emb_python_doc.h
enums.h
event-log.h
event.h
diff --git a/src/desktop.cpp b/src/desktop.cpp
index 2c47c5296d90d8b9dbf4398fcaf9acbec77121c6..7100e2dc821b14b23f531934277114d6dcda2dda 100644
--- a/src/desktop.cpp
+++ b/src/desktop.cpp
@@ -1975,6 +1975,7 @@ SPDesktop::show_dialogs()
mapVerbPreference.insert(std::make_pair ("FillAndStroke", "/dialogs/fillstroke") );
mapVerbPreference.insert(std::make_pair ("ExtensionEditor", "/dialogs/extensioneditor") );
mapVerbPreference.insert(std::make_pair ("AlignAndDistribute", "/dialogs/align") );
+ mapVerbPreference.insert(std::make_pair ("Console", "/dialogs/console") );
mapVerbPreference.insert(std::make_pair ("DocumentMetadata", "/dialogs/documentmetadata") );
mapVerbPreference.insert(std::make_pair ("DocumentProperties", "/dialogs/documentoptions") );
mapVerbPreference.insert(std::make_pair ("FilterEffectsDialog", "/dialogs/filtereffects") );
diff --git a/src/emb_python.cpp b/src/emb_python.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..952b8505d25692b4ba520cd53ab7baf55cbb2a3c
--- /dev/null
+++ b/src/emb_python.cpp
@@ -0,0 +1,765 @@
+/** @file
+ * @brief Embedded Python stuff
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+/*
+ * This file contains some code copied/modified from other Inkscape source files since it is a kind of glue
+ * layer. However, I may have failed to place the correct notice on every location. (I don't know if this is even
+ * necessary.)
+ */
+
+#if WITH_PYTHON
+
+#include "emb_python.h"
+
+
+namespace Inkscape {
+
+// ------------ C wrappers for interfacing Python ---------------------------------------
+extern "C" {
+// ---------- Inkscape integrated Python console handling ----------
+
+static PyObject *inkscapeConAppendStdOut(PyObject *self, PyObject *args) {
+ const char *str;
+
+ if(!PyArg_ParseTuple(args, "s", &str)) {
+ return NULL;
+ }
+
+ // Handle listeners
+ pyInstance *pi = pyInstance::getInstance("");
+ pi->emitStdOut(str);
+
+ return Py_BuildValue("");
+}
+
+static PyObject *inkscapeConAppendStdErr(PyObject *self, PyObject *args) {
+ const char *str;
+
+ if(!PyArg_ParseTuple(args, "s", &str)) {
+ return NULL;
+ }
+
+ // Handle listeners
+ pyInstance *pi = pyInstance::getInstance("");
+ pi->emitStdErr(str);
+
+ return Py_BuildValue("");
+}
+
+static PyObject *inkscapeConClear(PyObject *self, PyObject *args) {
+ // Handle listeners
+ pyInstance *pi = pyInstance::getInstance("");
+ pi->emitClear();
+
+ return Py_BuildValue("");
+}
+
+// ---------- Misc utility functions ----------
+
+static PyObject *inkscapeGetVersion(PyObject *self, PyObject *args) {
+ return PyUnicode_FromString(Inkscape::version_string);
+}
+
+static PyObject *inkscapeGetMode(PyObject *self, PyObject *args) {
+ if(INKSCAPE.use_gui()) {
+ return PyUnicode_FromString(pyInstance::MODE_GUI);
+ } else {
+ return PyUnicode_FromString(pyInstance::MODE_COMMANDLINE);
+ }
+}
+
+static PyObject *inkscapeGetScriptParam(PyObject *self, PyObject *args) {
+ if(pyInstance::inkscapeScriptParam) {
+ return PyUnicode_FromString(pyInstance::inkscapeScriptParam);
+ }
+ return PyUnicode_FromString("");
+}
+
+static PyObject *inkscapeGetPythonPath(PyObject *self, PyObject *args) {
+ std::string path = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::PYTHON, "");
+
+ return PyUnicode_FromString(path.c_str());
+}
+
+static PyObject *inkscapeSetWaitingCursor(PyObject *self, PyObject *args) {
+ int wait;
+
+ if(!PyArg_ParseTuple(args, "i", &wait)) {
+ return NULL;
+ }
+
+ pyHelper::setWaitingCursor(wait);
+
+ return Py_BuildValue("");
+}
+
+// ---------- Getting (information on) verbs and running verbs ----------
+
+static PyObject *inkscapeRunVerbs(PyObject *self, PyObject *args) {
+ // Used Python's builtin_sum() from bltinmodule.c as a template.
+ PyObject *seq, *iter, *item;
+
+ int i = 0;
+ const char *str;
+
+ if(!PyArg_UnpackTuple(args, "runVerbs", 1, 1, &seq)) {
+ return NULL;
+ }
+
+ iter = PyObject_GetIter(seq);
+ if (iter == NULL) {
+ PyErr_SetString(PyExc_TypeError, _("Argument must be iterable."));
+ return NULL;
+ }
+
+ while(item = PyIter_Next(iter)) {
+ str = PyUnicode_AsUTF8(item);
+
+ if(str == NULL) {
+ PyErr_Format(PyExc_TypeError, _("Item %d in iterable is not a string."), i);
+ Py_DECREF(item);
+ Py_DECREF(iter);
+ return NULL;
+ }
+
+ if(!pyHelper::runVerb(str)) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to execute verb %s."), str);
+ Py_DECREF(item);
+ Py_DECREF(iter);
+ return NULL;
+ }
+
+ ++i; // Count objects for error message.
+ Py_DECREF(item);
+ }
+
+ Py_DECREF(iter);
+
+ return Py_BuildValue("");
+}
+
+static PyObject *inkscapeGetVerbs(PyObject *self, PyObject *args) {
+ PyObject *list;
+ PyObject *vid;
+
+
+ list = PyList_New(0);
+
+ if(!list) {
+ return NULL;
+ }
+
+ std::vector verbs = Inkscape::Verb::getList();
+
+ for(const auto &it : verbs) {
+ if(!it) {
+ continue;
+ }
+ if(!it->get_name()) {
+ continue;
+ }
+
+ vid = PyUnicode_FromString(it->get_id());
+ if(!vid) {
+ Py_DECREF(list);
+ list = NULL;
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetVerbs: Could not create Python string of verb id."));
+ break;
+ }
+
+ if(PyList_Append(list, vid)) {
+ Py_DECREF(list);
+ list = NULL;
+ Py_DECREF(vid);
+
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetVerbs: Could not append verb id to list."));
+ break;
+ }
+ Py_DECREF(vid); /// @fixme Is this necessary? Can't find it in the docs.
+ }
+
+ return list;
+}
+
+static PyObject *inkscapeGetVerbName(PyObject *self, PyObject *args) {
+ PyObject *name;
+ const char *str;
+ Inkscape::Verb *verb;
+ const char *namestr;
+
+ if(!PyArg_ParseTuple(args, "s", &str)) {
+ return NULL;
+ }
+
+ verb = Inkscape::Verb::getbyid(str);
+
+ if(!verb) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unknown verb %s."), str);
+ return NULL;
+ }
+
+ namestr = verb->get_name();
+
+ if(namestr) {
+ name = PyUnicode_FromString(namestr);
+ } else {
+ name = PyUnicode_FromString("");
+ }
+
+ if(!name) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to create name string for verb %s."), str);
+ return NULL;
+ }
+
+ return name;
+}
+
+static PyObject *inkscapeGetVerbTooltip(PyObject *self, PyObject *args) {
+ PyObject *tooltip;
+ const char *str;
+ Inkscape::Verb *verb;
+ const char *ttstr;
+
+ if(!PyArg_ParseTuple(args, "s", &str)) {
+ return NULL;
+ }
+
+ verb = Inkscape::Verb::getbyid(str);
+
+ if(!verb) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unknown verb %s."), str);
+ return NULL;
+ }
+
+ ttstr = verb->get_short_tip();
+
+ if(ttstr) {
+ tooltip = PyUnicode_FromString(ttstr);
+ } else {
+ tooltip = PyUnicode_FromString("");
+ }
+
+ if(!tooltip) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to create tooltip string for verb %s."), str);
+ return NULL;
+ }
+
+ return tooltip;
+}
+
+// ---------- File opening, saving, closing ----------
+
+/** @brief Handle saving files
+ *
+ * @todo There should really be some way (or alternative function) to pass the export settings, e.g. for PDF, PNG
+ * and so on such that saving can be done without any user interaction.
+ * Note: have a look at the do_export_* functions in main.cpp on how to implement this functionality.
+ *
+ */
+static PyObject *inkscapeFileSaveAs(PyObject *self, PyObject *args) {
+ const char *filename;
+ const char *mime;
+ const char *errmsg = nullptr;
+ bool success;
+
+ if(!PyArg_ParseTuple(args, "ss", &filename, &mime)) {
+ return NULL;
+ }
+
+ success = pyHelper::fileSaveAs(filename, mime, errmsg, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS);
+
+ if(success) {
+ return Py_BuildValue("");
+ } else {
+ PyErr_Format(pyInstance::inkscapeError, errmsg);
+ return NULL;
+ }
+}
+
+/** @brief Handle saving a copy of a file
+ *
+ * @todo There should really be some way (or alternative function) to pass the export settings, e.g. for PDF, PNG
+ * and so on such that saving can be done without any user interaction.
+ * Note: have a look at the do_export_* functions in main.cpp on how to implement this functionality.
+ *
+ */
+static PyObject *inkscapeFileSaveCopy(PyObject *self, PyObject *args) {
+ const char *filename;
+ const char *mime;
+ const char *errmsg = nullptr;
+ bool success;
+
+ if(!PyArg_ParseTuple(args, "ss", &filename, &mime)) {
+ return NULL;
+ }
+
+ success = pyHelper::fileSaveAs(filename, mime, errmsg, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY);
+
+ if(success) {
+ return Py_BuildValue("");
+ } else {
+ PyErr_Format(pyInstance::inkscapeError, errmsg);
+ return NULL;
+ }
+}
+
+static PyObject *inkscapeGetOutputMIMES(PyObject *self, PyObject *args) {
+ PyObject *list;
+ PyObject *vid;
+ const char *mime;
+
+
+ list = PyList_New(0);
+
+ if(!list) {
+ return NULL;
+ }
+
+ auto map = pyHelper::buildSaveExtensionMap();
+
+ for(const auto &it : map) {
+ const char *mime = it.second->get_mimetype();
+
+ vid = PyUnicode_FromString(mime);
+ if(!vid) {
+ Py_DECREF(list);
+ list = NULL;
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetOutputMIMES: Could not create Python string of MIME type."));
+ break;
+ }
+
+ if(PyList_Append(list, vid)) {
+ Py_DECREF(list);
+ list = NULL;
+ Py_DECREF(vid);
+
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetOutputMIMES: Could not append MIME type to list."));
+ break;
+ }
+ Py_DECREF(vid); /// @fixme Is this necessary? Can't find it in the docs.
+ }
+
+ return list;
+}
+
+static PyObject *getOutputFileTypeName(PyObject *self, PyObject *args) {
+ PyObject *nameobj;
+ const char *mime;
+ const char *filetypename;
+
+ if(!PyArg_ParseTuple(args, "s", &mime)) {
+ return NULL;
+ }
+
+ filetypename = pyHelper::getOutputMIMEFileTypeName(mime);
+
+ if(filetypename == nullptr) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unknown output MIME type %s."), mime);
+ return NULL;
+ }
+
+ nameobj = PyUnicode_FromString(filetypename);
+
+ if(!nameobj) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to create name string MIME type %s."), mime);
+ return NULL;
+ }
+
+ return nameobj;
+}
+
+static PyObject *getOutputFileExtension(PyObject *self, PyObject *args) {
+ PyObject *extobj;
+ const char *mime;
+ const char *extension;
+
+ if(!PyArg_ParseTuple(args, "s", &mime)) {
+ return NULL;
+ }
+
+ extension = pyHelper::getOutputMIMEExtension(mime);
+
+ if(extension == nullptr) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unknown output MIME type %s."), mime);
+ return NULL;
+ }
+
+ extobj = PyUnicode_FromString(extension);
+
+ if(!extobj) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to create name string MIME type %s."), mime);
+ return NULL;
+ }
+
+ return extobj;
+}
+
+static PyObject *getOutputFileTooltip(PyObject *self, PyObject *args) {
+ PyObject *tooltipobj;
+ const char *mime;
+ const char *tooltip;
+
+ if(!PyArg_ParseTuple(args, "s", &mime)) {
+ return NULL;
+ }
+
+ tooltip = pyHelper::getOutputMIMETooltip(mime);
+
+ if(tooltip == nullptr) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unknown output MIME type %s."), mime);
+ return NULL;
+ }
+
+ tooltipobj = PyUnicode_FromString(tooltip);
+
+ if(!tooltipobj) {
+ PyErr_Format(pyInstance::inkscapeError, _("Unable to create name string MIME type %s."), mime);
+ return NULL;
+ }
+
+ return tooltipobj;
+}
+
+static PyObject *inkscapeFileOpen(PyObject *self, PyObject *args) {
+ const char *filename;
+ const char *errmsg = nullptr;
+ bool success;
+
+ if(!PyArg_ParseTuple(args, "s", &filename)) {
+ return NULL;
+ }
+
+
+ success = pyHelper::fileOpen(filename, errmsg);
+
+ if(success) {
+ return Py_BuildValue("");
+ } else {
+ PyErr_Format(pyInstance::inkscapeError, errmsg);
+ return NULL;
+ }
+
+ return Py_BuildValue("");
+}
+
+
+// ---------- File exporting ----------
+static PyObject *inkscapeExportPNG(PyObject *self, PyObject *args) {
+ const char *filename;
+ double dpi = 72.0;
+ int areaSnap = true;
+ int useDocumentBGColor = true;
+ guint32 bgColor;
+ int rr = 0xff, gg = 0xff, bb = 0xff, oo = 0x00;
+ pyHelper::PNGExportArea earea = pyHelper::PAGE;
+ char *areaStr;
+
+ const char *errmsg = nullptr;
+ bool success;
+
+ if(!PyArg_ParseTuple(args, "sdsi|i(iiii)", &filename, &dpi, &areaStr, &areaSnap, &useDocumentBGColor, &rr, &gg, &bb, &oo)) {
+ return NULL;
+ }
+
+ // Check if the colors are within range.
+ if(rr < pyHelper::RGB_MIN || rr > pyHelper::RGB_MAX) {
+ PyErr_Format(pyInstance::inkscapeError, "Red color value out of range (%d, %d)", pyHelper::RGB_MIN, pyHelper::RGB_MAX);
+ return NULL;
+ }
+ if(gg < pyHelper::RGB_MIN || gg > pyHelper::RGB_MAX) {
+ PyErr_Format(pyInstance::inkscapeError, "Green color value out of range (%d, %d)", pyHelper::RGB_MIN, pyHelper::RGB_MAX);
+ return NULL;
+ }
+ if(bb < pyHelper::RGB_MIN || bb > pyHelper::RGB_MAX) {
+ PyErr_Format(pyInstance::inkscapeError, "Blue color value out of range (%d, %d)", pyHelper::RGB_MIN, pyHelper::RGB_MAX);
+ return NULL;
+ }
+ if(oo < pyHelper::RGB_MIN || oo > pyHelper::RGB_MAX) {
+ PyErr_Format(pyInstance::inkscapeError, "Opacity value out of range (%d, %d)", pyHelper::RGB_MIN, pyHelper::RGB_MAX);
+ return NULL;
+ }
+
+ // Convert color channels to uint32 value.
+ if(!useDocumentBGColor) {
+ bgColor = (rr << 24) | (gg << 16) | (bb << 8) | oo;
+ }
+
+ // Convert export mode string to enum of pyHelper
+ if(strcmp(areaStr, "page") == 0) {
+ earea = pyHelper::PAGE;
+ } else if(strcmp(areaStr, "drawing") == 0) {
+ earea = pyHelper::DRAWING;
+ } else {
+ PyErr_Format(pyInstance::inkscapeError, _("Invalid export area. Valid values are 'page' and 'drawing'."));
+ return NULL;
+ }
+
+ if(dpi < pyHelper::PNG_EXPORT_DPI_MIN || dpi > pyHelper::PNG_EXPORT_DPI_MAX) {
+ PyErr_Format(pyInstance::inkscapeError, _("Resolution (dpi) out of range.")); /// @fixme Output what the range is, but PyErr_Format has no format string for floating point numbers?
+ }
+
+ success = pyHelper::exportPNG(filename, earea, dpi, areaSnap, useDocumentBGColor, bgColor, errmsg);
+
+ if(success) {
+ return Py_BuildValue("");
+ } else {
+ PyErr_Format(pyInstance::inkscapeError, errmsg);
+ return NULL;
+ }
+
+ return Py_BuildValue("");
+}
+
+// ---------- Desktop handling ----------
+static PyObject *inkscapeGetDesktops(PyObject *self, PyObject *args) {
+ PyObject *list;
+ PyObject *puri;
+ std::vector uris = pyHelper::getDesktops();
+
+
+ list = PyList_New(0);
+
+ if(!list) {
+ return NULL;
+ }
+
+ for(const auto &it : uris) {
+ puri = PyUnicode_FromString(it.c_str());
+ if(!puri) {
+ Py_DECREF(list);
+ list = NULL;
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetDesktops: Could not create Python string of desktop URI."));
+ break;
+ }
+
+ if(PyList_Append(list, puri)) {
+ Py_DECREF(list);
+ list = NULL;
+ Py_DECREF(puri);
+
+ PyErr_SetString(pyInstance::inkscapeError, _("inkscapeGetOutputMIMES: Could not append URI desktop list."));
+ break;
+ }
+ Py_DECREF(puri); /// @fixme Is this necessary? Can't find it in the docs.
+ }
+
+ return list;
+}
+
+static PyObject *inkscapeSetActiveDesktop(PyObject *self, PyObject *args) {
+ int desktopNr;
+
+ if(!PyArg_ParseTuple(args, "i", &desktopNr)) {
+ return NULL;
+ }
+
+ if(pyHelper::setActiveDekstop(desktopNr)) {
+ return Py_BuildValue("");
+ } else {
+ PyErr_SetString(pyInstance::inkscapeError, _("setActiveDesktop: Desktop index out of range."));
+ return NULL;
+ }
+}
+
+static PyMethodDef inkscapeMethods[] = {
+ {"conAppendStdOut", inkscapeConAppendStdOut, METH_VARARGS, conAppendStdOut__doc__},
+ {"conAppendStdErr", inkscapeConAppendStdErr, METH_VARARGS, conAppendStdErr__doc__},
+ {"conClear", inkscapeConClear, METH_NOARGS, conClear__doc__},
+
+ {"getVersion", inkscapeGetVersion, METH_NOARGS, getVersion__doc__},
+ {"runVerbs", inkscapeRunVerbs, METH_VARARGS, runVerbs__doc__},
+
+ {"getVerbs", inkscapeGetVerbs, METH_NOARGS, getVerbs__doc__},
+ {"getVerbName", inkscapeGetVerbName, METH_VARARGS, getVerbName__doc__},
+ {"getVerbTooltip", inkscapeGetVerbTooltip, METH_VARARGS, getVerbTooltip__doc__},
+
+ {"getMode", inkscapeGetMode, METH_NOARGS, getMode__doc__},
+ {"getScriptParam", inkscapeGetScriptParam, METH_NOARGS, getScriptParam__doc__},
+ {"getPythonPath", inkscapeGetPythonPath, METH_NOARGS, getPythonPath__doc__},
+
+ {"setWaitingCursor", inkscapeSetWaitingCursor, METH_VARARGS, setWaitingCursor__doc__},
+
+ {"fileSaveAs", inkscapeFileSaveAs, METH_VARARGS, fileSaveAs__doc__},
+ {"fileSaveCopy", inkscapeFileSaveCopy, METH_VARARGS, fileSaveCopy__doc__},
+ {"getOutputMIMEs", inkscapeGetOutputMIMES, METH_NOARGS, getOutputMIMEs__doc__},
+ {"getOutputFileTypeName", getOutputFileTypeName, METH_VARARGS, getOutputFileTypeName__doc__},
+ {"getOutputFileExtension", getOutputFileExtension, METH_VARARGS, getOutputFileExtension__doc__},
+ {"getOutputFileTooltip", getOutputFileTooltip, METH_VARARGS, getOutputFileTooltip__doc__},
+
+ {"fileOpen", inkscapeFileOpen, METH_VARARGS, fileOpen__doc__},
+
+ {"exportPNG", inkscapeExportPNG, METH_VARARGS, exportPNG__doc__},
+
+ {"getDesktops", inkscapeGetDesktops, METH_NOARGS, getDesktops__doc__},
+ {"setActiveDesktop", inkscapeSetActiveDesktop, METH_VARARGS, setActiveDesktop__doc__},
+
+ {nullptr, nullptr, 0, nullptr}
+};
+
+
+static struct PyModuleDef inkscapeModule = {
+ PyModuleDef_HEAD_INIT,
+ "_inkscape",
+ nullptr,
+ -1,
+ inkscapeMethods
+};
+
+PyMODINIT_FUNC
+PyInit_inkscape(void) {
+ PyObject *m = nullptr;
+
+ m = PyModule_Create(&inkscapeModule);
+
+ if(m == nullptr) {
+ goto except;
+ }
+
+// if(PyModule_AddStringConstant(m, "version", Inkscape::version_string)) {
+// goto except;
+// }
+
+ pyInstance::inkscapeError = PyErr_NewException("_inkscape.Error", NULL, NULL);
+
+ if(pyInstance::inkscapeError) {
+ PyModule_AddObject(m, "Error", pyInstance::inkscapeError);
+ } else {
+ goto except;
+ }
+
+ goto finally;
+except:
+ Py_XDECREF(m);
+ m = nullptr;
+finally:
+ return m;
+}
+
+} // extern "C"
+
+// ----------------------- Constructors / Destructors and so on -----------------------
+pyInstance *pyInstance::instance = nullptr;
+const char * const pyInstance::MODE_COMMANDLINE = "commandline";
+const char * const pyInstance::MODE_GUI = "gui";
+const char * const pyInstance::INIT_FUNCTION = "inkscapeInit";
+const char * pyInstance::inkscapeScriptParam = nullptr;
+
+PyObject* pyInstance::inkscapeError;
+
+pyInstance* pyInstance::getInstance(char const *progname) {
+ if(!instance) {
+ instance = new pyInstance(progname);
+ }
+
+ return instance;
+}
+
+pyInstance::pyInstance(char const *progname)
+{
+ wchar_t *wprogname = Py_DecodeLocale(progname, NULL);
+ if(wprogname == NULL) {
+ std::cerr << _("Error: cannot decode program name. Ignoring.") << std::endl;
+ } else {
+ Py_SetProgramName(wprogname);
+ }
+
+ PyImport_AppendInittab("_inkscape", PyInit_inkscape);
+
+ Py_Initialize();
+}
+
+void
+pyInstance::runInitFile(void)
+{
+ std::string initfile = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::PYTHON, "init.py");
+
+ // Run init file
+ runFile(initfile);
+ }
+
+pyInstance::~pyInstance() {
+ Py_Finalize();
+}
+
+
+// ----------------------- Signals -----------------------
+pyInstance::type_sig_stdout
+pyInstance::signal_stdout()
+{
+ return sig_stdout;
+}
+
+pyInstance::type_sig_stderr
+pyInstance::signal_stderr()
+{
+ return sig_stderr;
+}
+
+pyInstance::type_sig_clear
+pyInstance::signal_clear()
+{
+ return sig_clear;
+}
+
+void
+pyInstance::emitStdOut(const char *str) {
+ sig_stdout.emit(str);
+}
+
+void
+pyInstance::emitStdErr(const char *str) {
+ sig_stderr.emit(str);
+}
+
+void
+pyInstance::emitClear(void) {
+ sig_clear.emit();
+}
+
+
+// ----------------------- Misc implementations -----------------------
+int
+pyInstance::runString(char const *str) {
+ return PyRun_SimpleString(str);
+}
+
+bool
+pyInstance::runFile(std::string fname) {
+ // According to
+ // https://bytes.com/topic/python/answers/840542-pyrun_simplefile-crashes
+ // and
+ // https://stackoverflow.com/questions/3654652/why-does-the-python-c-api-crash-on-pyrun-simplefile
+ // one must take care to get a library compatible version of the FILE structure:
+ //
+ // TODO: Correctly cite bytes/stackoverflow post. Is this note enough?
+
+ PyObject *PyObj = Py_BuildValue("s", fname.c_str());
+ FILE *file = _Py_fopen_obj(PyObj, "r+");
+ bool success = false;
+
+ if(file != NULL) {
+ // Run script
+ //PyRun_SimpleFile(file, fname.c_str());
+
+ if(PyRun_SimpleFileEx(file, fname.c_str(), true) >= 0) {
+ success = true;
+ }
+ }
+ Py_DECREF(PyObj);
+
+ return success;
+}
+
+
+} // namespace Inkscape
+
+
+#endif // WITH_PYTHON
diff --git a/src/emb_python.h b/src/emb_python.h
new file mode 100644
index 0000000000000000000000000000000000000000..94e80ab633eb6d8d15beb0eb017f83d450d9b494
--- /dev/null
+++ b/src/emb_python.h
@@ -0,0 +1,105 @@
+/** @file
+ * @brief Embedded Python stuff
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+#if WITH_PYTHON
+
+#ifndef INKSCAPE_EMB_PYTHON_H
+#define INKSCAPE_EMB_PYTHON_H
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+
+#include "io/resource.h"
+#include "inkscape-version.h"
+#include "verbs.h"
+#include "desktop.h"
+#include "inkscape.h"
+
+#include "helper/action.h"
+#include "helper/png-write.h"
+
+#include "pyhelper.h"
+#include "emb_python_doc.h"
+
+
+namespace Inkscape {
+
+extern "C" {
+PyMODINIT_FUNC PyInit_inkscape(void);
+
+} // extern "C"
+
+
+/*
+ * Class for handling the Python interpreter.
+ */
+class pyInstance {
+public:
+ static pyInstance* getInstance(char const *progname);
+
+ ~pyInstance();
+
+ // Constants
+ static const char * const MODE_COMMANDLINE;
+ static const char * const MODE_GUI;
+ static const char * const INIT_FUNCTION;
+
+ static PyObject* inkscapeError;
+
+ // Signals
+ typedef sigc::signal type_sig_stdout;
+ typedef sigc::signal type_sig_stderr;
+ typedef sigc::signal type_sig_clear;
+
+ type_sig_stdout signal_stdout();
+ type_sig_stderr signal_stderr();
+ type_sig_clear signal_clear();
+
+ void emitStdOut(const char *str);
+ void emitStdErr(const char *str);
+ void emitClear(void);
+
+ // Misc functions
+ void runInitFile(void);
+
+ int runString(char const *str);
+ bool runFile(std::string fname);
+
+ static const char *inkscapeScriptParam; // Script parameter string if run from command line
+
+private:
+ static pyInstance *instance;
+
+ // No default constructor, noncopyable, nonassignable
+ pyInstance();
+ pyInstance(char const *progname);
+ pyInstance operator=(pyInstance const &d);
+
+ pyInstance(pyInstance const &d);
+
+protected:
+ type_sig_stdout sig_stdout;
+ type_sig_stderr sig_stderr;
+ type_sig_clear sig_clear;
+};
+
+
+} // namespace Inkscape
+
+#endif // INKSCAPE_EMB_PYTHON_H
+
+#endif // WITH_PYTHON
diff --git a/src/emb_python_doc.h b/src/emb_python_doc.h
new file mode 100644
index 0000000000000000000000000000000000000000..2fa2fc6ca73a494f56394f060f5d7f174cde9df4
--- /dev/null
+++ b/src/emb_python_doc.h
@@ -0,0 +1,159 @@
+/** @file
+ * @brief Embedded Python doc-strings
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+#if WITH_PYTHON
+
+#ifndef INKSCAPE_EMB_PYTHON_DOC_H
+#define INKSCAPE_EMB_PYTHON_DOC_H
+
+#include
+
+namespace Inkscape {
+extern "C" {
+
+PyDoc_STRVAR(conAppendStdOut__doc__,
+ "conAppendStdOut(str)\n"
+ "--\n\n"
+ "Append string str to the Inkscape Python console in stdout style.\n");
+
+PyDoc_STRVAR(conAppendStdErr__doc__,
+ "conAppendStdErr(str)\n"
+ "--\n\n"
+ "Append string str to the Inkscape Python console in stderr style.\n");
+
+PyDoc_STRVAR(conClear__doc__,
+ "conClear()\n"
+ "--\n\n"
+ "Clear the Inkscape Python console.\n");
+
+PyDoc_STRVAR(getVersion__doc__,
+ "getVersion()\n"
+ "--\n\n"
+ "Returns the Inkscape version string.\n");
+
+PyDoc_STRVAR(runVerbs__doc__,
+ "runVerbs(iterable)\n"
+ "--\n\n"
+ "Run all the Inkscape verb strings contained in the iterable.\n");
+
+PyDoc_STRVAR(getMode__doc__,
+ "getMode()\n"
+ "--\n\n"
+ "Returns whether Inkscape runs in GUI or command line mode.\n"
+ "Return value string is either 'gui' or 'commandline'.\n");
+
+PyDoc_STRVAR(getScriptParam__doc__,
+ "getScriptParam()\n"
+ "--\n\n"
+ "Returns the command line parameter string supplied with --python-script-argument\n"
+ "on the command line.\n"
+ "An empty string is returned, if no (or an empty) parameter was specified.\n");
+
+PyDoc_STRVAR(getPythonPath__doc__,
+ "getPythonPath()\n"
+ "--\n\n"
+ "Returns the path to Inkscape's internal Python resources.\n");
+
+PyDoc_STRVAR(getVerbs__doc__,
+ "getVerbs()\n"
+ "--\n\n"
+ "Returns a list with all available verbs.\n");
+
+PyDoc_STRVAR(getVerbName__doc__,
+ "getVerbName(str)\n"
+ "--\n\n"
+ "Returns the name string of the verb named str.\n");
+
+PyDoc_STRVAR(getVerbTooltip__doc__,
+ "getVerbTooltip(str)\n"
+ "--\n\n"
+ "Returns the tooltip string of the verb named str.\n");
+
+PyDoc_STRVAR(setWaitingCursor__doc__,
+ "setWaitingCursor(enable)\n"
+ "--\n\n"
+ "If enable is true, Inkscape's waiting cursor is set and user interaction is disabled.\n");
+
+PyDoc_STRVAR(fileSaveAs__doc__,
+ "fileSaveAs(filename, mime)\n"
+ "--\n\n"
+ "Save the active desktop into the file filename using the MIME type mime.\n"
+ "This is similar to selecting the menu item File => Save As. Consequently, user interaction may be requested "
+ "for some file types, i.e., dialogs for setting the save options are presented.\n");
+
+PyDoc_STRVAR(fileSaveCopy__doc__,
+ "fileSaveCopy(filename, mime)\n"
+ "--\n\n"
+ "Save a copy of the active desktop into the file filename using the MIME type mime.\n"
+ "This is similar to selecting the menu item File => Save a Copy. Consequently, user interaction may be requested "
+ "for some file types, i.e., dialogs for setting the save options are presented.\n");
+
+PyDoc_STRVAR(getOutputMIMEs__doc__,
+ "getOutputMIMEs()\n"
+ "--\n\n"
+ "Returns a list with all available MIME strings that can be used with fileSaveAs and fileSaveCopy.\n");
+
+PyDoc_STRVAR(getOutputFileTypeName__doc__,
+ "getOutputFileTypeName(mime)\n"
+ "--\n\n"
+ "Returns the file type-name for a given output MIME type string.\n"
+ "For example, querying 'image/x-inkscape-svg' yields 'Inkscape SVG (*.svg)'.\n");
+
+PyDoc_STRVAR(getOutputFileExtension__doc__,
+ "getOutputFileExtension(mime)\n"
+ "--\n\n"
+ "Returns the file extension for a given output MIME type string including the extension dot.\n"
+ "For example, querying 'image/x-inkscape-svg' yields '.svg'.\n");
+
+PyDoc_STRVAR(getOutputFileTooltip__doc__,
+ "getOutputFileTooltip(mime)\n"
+ "--\n\n"
+ "Returns the tooltip text for a fiven output MIME type string.\n");
+
+PyDoc_STRVAR(fileOpen__doc__,
+ "fileOpen(filename)\n"
+ "--\n\n"
+ "Open file filename.\n");
+
+PyDoc_STRVAR(exportPNG__doc__,
+ "exportPNG(filename, dpi, area, areaSnap, useDocumentBGColor, colorTuple)\n"
+ "--\n\n"
+ "Export the current desktop as PNG file.\n"
+ "Params:\n"
+ " filename: Namestring of the output file.\n"
+ " dpi: Resolution (in dpi) of the output file.\n"
+ " area: Either 'drawing' to export the drawing area or 'page' to export the page area.\n"
+ " areaSnap: If true, the exported area is snapped outwards to the next integer values.\n"
+ "\n"
+ "Optional params:\n"
+ " useDocumentBGColor: If true, the document settings are used to set the backgound\n"
+ " color of the exported PNG file. If false, the values in colorTuple are used.\n"
+ " colorTuple: A tuple in the form of (r, g, b, o) where r, g, b and o are the red, green, blue\n"
+ " and opacity values in the range of 0 to 255 to be used as PNG background color.\n");
+
+PyDoc_STRVAR(getDesktops__doc__,
+ "getDesktops()\n"
+ "--\n\n"
+ "Returns a list with the URI string for each desktop. If the string is not set,\n"
+ "for example if the document has not been saved, an empty string is included.\n");
+
+PyDoc_STRVAR(setActiveDesktop__doc__,
+ "setActiveDesktop(nr)\n"
+ "--\n\n"
+ "Set the desktop nr as active. The number corresponds to index of the list returned\n"
+ "by getDekstops\n");
+
+} // extern "C"
+} // namespace Inkscape
+
+#endif // INKSCAPE_EMB_PYTHON_DOC_H
+
+#endif // WITH_PYTHON
diff --git a/src/file.cpp b/src/file.cpp
index 7cc962d38975d65883440580e4345b2436a6117e..6d4aaaf1e5868606368d9b827ae9d3b058491da8 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -209,13 +209,27 @@ sp_file_exit()
* \param replace_empty if true, and the current desktop is empty, this document
* will replace the empty one.
*/
+
bool sp_file_open(const Glib::ustring &uri,
Inkscape::Extension::Extension *key,
bool add_to_recent,
- bool replace_empty)
+ bool replace_empty,
+ bool interaction)
+{
+ const char *msg;
+ return sp_file_open(uri, key, add_to_recent, replace_empty, interaction, msg);
+}
+
+bool sp_file_open(const Glib::ustring &uri,
+ Inkscape::Extension::Extension *key,
+ bool add_to_recent,
+ bool replace_empty,
+ bool interaction,
+ const char *& msg)
{
SPDesktop *desktop = SP_ACTIVE_DESKTOP;
- if (desktop) {
+
+ if (desktop && interaction) {
desktop->setWaitingCursor();
}
SPDocument *doc = nullptr;
@@ -223,81 +237,89 @@ bool sp_file_open(const Glib::ustring &uri,
try {
doc = Inkscape::Extension::open(key, uri.c_str());
} catch (Inkscape::Extension::Input::no_extension_found &e) {
+ msg = _("No Inkscape extension found to open the document.");
doc = nullptr;
} catch (Inkscape::Extension::Input::open_failed &e) {
+ msg = _("File could not be opened.");
doc = nullptr;
} catch (Inkscape::Extension::Input::open_cancelled &e) {
+ msg = _("Open cancelled.");
doc = nullptr;
cancelled = true;
}
- if (desktop) {
+
+ if (desktop && interaction) {
desktop->clearWaitingCursor();
}
if (doc) {
+ SPDocument *existing = desktop ? desktop->getDocument() : nullptr;
+
+ if(INKSCAPE.use_gui()) {
+ if (existing && existing->virgin && replace_empty) {
+ // If the current desktop is empty, open the document there
+ doc->ensureUpToDate(); // TODO this will trigger broken link warnings, etc.
+ desktop->change_document(doc);
+ doc->emitResizedSignal(doc->getWidth().value("px"), doc->getHeight().value("px"));
+ } else {
+ // create a whole new desktop and window
+ SPViewWidget *dtw = sp_desktop_widget_new(sp_document_namedview(doc, nullptr)); // TODO this will trigger broken link warnings, etc.
+ sp_create_window(dtw, TRUE);
+ desktop = static_cast(dtw->view);
+ }
+
+ doc->virgin = FALSE;
+
+ // everyone who cares now has a reference, get rid of our`s
+ doc->doUnref();
+
+ SPRoot *root = doc->getRoot();
+
+ // This is the only place original values should be set.
+ root->original.inkscape = root->version.inkscape;
+ root->original.svg = root->version.svg;
+
+ if (INKSCAPE.use_gui()) {
+ if (sp_version_inside_range(root->version.inkscape, 0, 1, 0, 92)) {
+ sp_file_convert_dpi(doc);
+ }
+ } // If use_gui
+
+
+ // resize the window to match the document properties
+ sp_namedview_window_from_document(desktop);
+ sp_namedview_update_layers_from_document(desktop);
+
+ if (add_to_recent) {
+ sp_file_add_recent( doc->getURI() );
+ }
+
+ SPNamedView *nv = desktop->namedview;
+ if (nv->lockguides) {
+ nv->lockGuides();
+ }
+ // Perform a fixup pass for hrefs.
+ if ( Inkscape::ResourceManager::getManager().fixupBrokenLinks(doc) ) {
+ Glib::ustring msg = _("Broken links have been changed to point to existing files.");
+ desktop->showInfoDialog(msg);
+ }
+
+ // Check for font substitutions
+ Inkscape::UI::Dialog::FontSubstitution::getInstance().checkFontSubstitutions(doc);
+ }
- SPDocument *existing = desktop ? desktop->getDocument() : nullptr;
-
- if (existing && existing->virgin && replace_empty) {
- // If the current desktop is empty, open the document there
- doc->ensureUpToDate(); // TODO this will trigger broken link warnings, etc.
- desktop->change_document(doc);
- doc->emitResizedSignal(doc->getWidth().value("px"), doc->getHeight().value("px"));
- } else {
- // create a whole new desktop and window
- SPViewWidget *dtw = sp_desktop_widget_new(sp_document_namedview(doc, nullptr)); // TODO this will trigger broken link warnings, etc.
- sp_create_window(dtw, TRUE);
- desktop = static_cast(dtw->view);
- }
-
- doc->virgin = FALSE;
-
- // everyone who cares now has a reference, get rid of our`s
- doc->doUnref();
-
- SPRoot *root = doc->getRoot();
-
- // This is the only place original values should be set.
- root->original.inkscape = root->version.inkscape;
- root->original.svg = root->version.svg;
-
- if (INKSCAPE.use_gui()) {
- if (sp_version_inside_range(root->version.inkscape, 0, 1, 0, 92)) {
- sp_file_convert_dpi(doc);
- }
- } // If use_gui
-
-
- // resize the window to match the document properties
- sp_namedview_window_from_document(desktop);
- sp_namedview_update_layers_from_document(desktop);
-
- if (add_to_recent) {
- sp_file_add_recent( doc->getURI() );
- }
-
- if ( INKSCAPE.use_gui() ) {
-
- SPNamedView *nv = desktop->namedview;
- if (nv->lockguides) {
- nv->lockGuides();
- }
- // Perform a fixup pass for hrefs.
- if ( Inkscape::ResourceManager::getManager().fixupBrokenLinks(doc) ) {
- Glib::ustring msg = _("Broken links have been changed to point to existing files.");
- desktop->showInfoDialog(msg);
- }
-
- // Check for font substitutions
- Inkscape::UI::Dialog::FontSubstitution::getInstance().checkFontSubstitutions(doc);
- }
// Related bug:#1769679 #18
SPDefs * defs = dynamic_cast(doc->getDefs());
if (defs && !existing) {
defs->emitModified(SP_OBJECT_MODIFIED_CASCADE);
}
+
+ if(!INKSCAPE.use_gui()) {
+ INKSCAPE.add_document(doc);
+ }
+
return TRUE;
- } else if (!cancelled) {
+ } else if (!cancelled && interaction && INKSCAPE.use_gui()) {
gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str());
gchar *text = g_strdup_printf(_("Failed to load the requested file %s"), safeUri);
sp_ui_error_dialog(text);
@@ -309,6 +331,7 @@ bool sp_file_open(const Glib::ustring &uri,
return FALSE;
}
+
/**
* Handle prompting user for "do you want to revert"? Revert on "OK"
*/
diff --git a/src/file.h b/src/file.h
index 183233a1b71f35badd53049aa33a924c872b3c49..0d0974ba563427e0c768a10d77ec7677a14dd916 100644
--- a/src/file.h
+++ b/src/file.h
@@ -69,7 +69,17 @@ bool sp_file_open(
const Glib::ustring &uri,
Inkscape::Extension::Extension *key,
bool add_to_recent = true,
- bool replace_empty = true
+ bool replace_empty = true,
+ bool interaction = true
+ );
+
+bool sp_file_open(
+ const Glib::ustring &uri,
+ Inkscape::Extension::Extension *key,
+ bool add_to_recent,
+ bool replace_empty,
+ bool interaction,
+ const char *& msg
);
/**
diff --git a/src/inkscape-application.cpp b/src/inkscape-application.cpp
index 4c39ee51c750b6519aaba575a5d73c4c770f604f..3fb90cb6f7c69644f2995cb21df673f2a95b0f44 100644
--- a/src/inkscape-application.cpp
+++ b/src/inkscape-application.cpp
@@ -21,6 +21,8 @@
#include "io/file.h" // File open (command line).
#include "desktop.h" // Access to window
+#include "emb_python.h"
+
#include "actions/actions-base.h" // Actions
#include "actions/actions-output.h" // Actions
#include "actions/actions-selection.h" // Actions
@@ -140,11 +142,18 @@ ConcreteInkscapeApplication::ConcreteInkscapeApplication()
this->add_main_option_entry(T::OPTION_TYPE_FILENAME, "xverbs", '\0', N_("Process: xverb command file."), N_("XVERBS-FILENAME"));
#endif // WITH_YAML
+#if WITH_PYTHON
+ this->add_main_option_entry(T::OPTION_TYPE_STRING, "python-script", '\0', N_("Process: Python script to execute"), N_("PYTHON-FILENAME")); /// @fixme: OPTION_TYPE_FILENAME would be more correct, but then lookup_value only returns an empty string.
+ this->add_main_option_entry(T::OPTION_TYPE_STRING, "python-script-argument", '\0', N_("Process: Parameter for Python script"), N_("PYTHON-SCRIPT-ARGUMENT"));
+#endif
+
+
#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
@@ -153,6 +162,26 @@ ConcreteInkscapeApplication::ConcreteInkscapeApplication()
T::register_application();
}
+
+template
+void
+ConcreteInkscapeApplication::handlePythonScript()
+{
+#if WITH_PYTHON
+ if(!_pyScriptName.empty()) {
+ if(!_pyScriptArg.empty()) {
+ Inkscape::pyInstance::inkscapeScriptParam = _pyScriptArg.c_str();
+ }
+
+ Inkscape::pyInstance *ipy = Inkscape::pyInstance::getInstance("");
+
+ if(!ipy->runFile(_pyScriptName)) {
+ g_error("Could not successfully execute Python script %s.", _pyScriptName.c_str());
+ }
+ }
+#endif
+}
+
SPDocument*
InkscapeApplication::get_active_document()
{
@@ -194,7 +223,15 @@ ConcreteInkscapeApplication::on_startup2()
// This should be completely rewritten.
Inkscape::Application::create(nullptr, _with_gui); // argv appears to not be used.
+#ifdef WITH_PYTHON
+ // Initialize Python
+ Inkscape::pyInstance *ipy = Inkscape::pyInstance::getInstance(Glib::get_prgname().c_str());
+ ipy->runInitFile();
+#endif
+
if (!_with_gui) {
+ handlePythonScript();
+
return;
}
@@ -227,6 +264,8 @@ ConcreteInkscapeApplication::on_startup2()
} else {
set_app_menu(menu);
}
+
+ handlePythonScript();
}
// Open document window with default document. Either this or on_open() is called.
@@ -554,6 +593,10 @@ ConcreteInkscapeApplication::on_handle_local_options(const Glib::RefPtrcontains("export-id") ||
options->contains("export-plain-svg") ||
options->contains("export-text-to_path")
+#if WITH_PYTHON
+ || options->contains("python-script") || /// @todo: Scripts directly from command line only without GUI for now. (Startup and redirection initialization sequence, etc. must be sorted out.)
+ options->contains("python-script-argument")
+#endif
) {
_with_gui = false;
}
@@ -632,6 +675,29 @@ ConcreteInkscapeApplication::on_handle_local_options(const Glib::RefPtrcontains("python-script-argument")) {
+ Glib::ustring arg;
+ options->lookup_value("python-script-argument", _pyScriptArg);
+
+// if (!arg.empty()) {
+// _command_line_actions.push_back(
+// std::make_pair("python-script-argument", Glib::Variant::create(arg)));
+// }
+ }
+
+ if (options->contains("python-script")) {
+ Glib::ustring pyfile;
+ options->lookup_value("python-script", _pyScriptName);
+
+// if (!pyfile.empty()) {
+// _command_line_actions.push_back(
+// std::make_pair("python-script", Glib::Variant::create(pyfile)));
+// }
+ }
+#endif
// ==================== EXPORT =====================
if (options->contains("export-file")) {
@@ -700,7 +766,6 @@ ConcreteInkscapeApplication::on_handle_local_options(const Glib::RefPtrlookup_value("export-background-opacity", _file_export.export_background_opacity);
}
-
// ==================== D-BUS ======================
#ifdef WITH_DBUS
diff --git a/src/inkscape-application.h b/src/inkscape-application.h
index e8cf12ee9e434769a2e20d28400f977f356dd78a..951cc6e509d4ace349a964f1cc04e67920b4ddae 100644
--- a/src/inkscape-application.h
+++ b/src/inkscape-application.h
@@ -55,6 +55,10 @@ protected:
// Actions from the command line or file.
action_vector_t _command_line_actions;
+
+ // Members for command line Python script handling.
+ Glib::ustring _pyScriptName;
+ Glib::ustring _pyScriptArg;
};
// T can be either:
@@ -91,6 +95,7 @@ private:
Glib::RefPtr _builder;
+ void handlePythonScript();
};
#endif // INKSCAPE_APPLICATION_H
diff --git a/src/io/resource.cpp b/src/io/resource.cpp
index 96a110098274ee38bb2fd35f8add6fd3b1399dbb..1a7075658caa935d14a1ec9e2837147e0648beb8 100644
--- a/src/io/resource.cpp
+++ b/src/io/resource.cpp
@@ -54,6 +54,7 @@ gchar *_get_path(Domain domain, Type type, char const *filename)
case NONE: g_assert_not_reached(); break;
case PALETTES: temp = INKSCAPE_PALETTESDIR; break;
case PATTERNS: temp = INKSCAPE_PATTERNSDIR; break;
+ case PYTHON: temp = INKSCAPE_PYTHONDIR; break;
case SCREENS: temp = INKSCAPE_SCREENSDIR; break;
case SYMBOLS: temp = INKSCAPE_SYMBOLSDIR; break;
case TEMPLATES: temp = INKSCAPE_TEMPLATESDIR; break;
diff --git a/src/io/resource.h b/src/io/resource.h
index c0f8cb22616f95d0fb6358af3cc466c63e584a9f..04af80c6994b1d7079a566f2f19ee92265acc1c0 100644
--- a/src/io/resource.h
+++ b/src/io/resource.h
@@ -37,6 +37,7 @@ enum Type {
NONE,
PALETTES,
PATTERNS,
+ PYTHON,
SCREENS,
TEMPLATES,
TUTORIALS,
diff --git a/src/object/sp-line.cpp b/src/object/sp-line.cpp
index a095d8514495b0d962b890f474e3958e7f533cea..3b08a4eefc79e099fea3e53bd1be51e3a5a90144 100644
--- a/src/object/sp-line.cpp
+++ b/src/object/sp-line.cpp
@@ -158,6 +158,18 @@ void SPLine::set_shape() {
c->unref();
}
+
+void SPLine::setPosition(gdouble x1, gdouble y1, gdouble x2, gdouble y2)
+{
+ this->x1 = x1;
+ this->y1 = y1;
+ this->x2 = x2;
+ this->y2 = y2;
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+
/*
Local Variables:
mode:c++
diff --git a/src/object/sp-line.h b/src/object/sp-line.h
index 5861d5b887b71ca0e0982a3d74a5c215021ef0ca..263649fbdd80d05dc2201a97c4986288ca3d01b4 100644
--- a/src/object/sp-line.h
+++ b/src/object/sp-line.h
@@ -31,6 +31,8 @@ public:
SVGLength x2;
SVGLength y2;
+ void setPosition(gdouble x1, gdouble y1, gdouble x2, gdouble y2);
+
void build(SPDocument *document, Inkscape::XML::Node *repr) override;
Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override;
void set(SPAttributeEnum key, char const* value) override;
diff --git a/src/path-prefix.h b/src/path-prefix.h
index 37cb83ef461106e84d65394fdd8ae7d7d86d52d5..269c36265becdf86ec83361f4737e8b114321c60 100644
--- a/src/path-prefix.h
+++ b/src/path-prefix.h
@@ -58,6 +58,7 @@ char *get_extensions_path();
# define INKSCAPE_MARKERSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/markers" )
# define INKSCAPE_PALETTESDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/palettes" )
# define INKSCAPE_PATTERNSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/patterns" )
+# define INKSCAPE_PYTHONDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/python" )
# define INKSCAPE_SCREENSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/screens" )
# define INKSCAPE_SYMBOLSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/symbols" )
# define INKSCAPE_THEMEDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/themes" )
@@ -82,6 +83,7 @@ char *get_extensions_path();
# define INKSCAPE_KEYSDIR append_inkscape_datadir("keys")
# define INKSCAPE_ICONSDIR append_inkscape_datadir("icons")
# define INKSCAPE_PIXMAPSDIR append_inkscape_datadir("pixmaps")
+# define INKSCAPE_PYTHONDIR append_inkscape_datadir("python")
# define INKSCAPE_MARKERSDIR append_inkscape_datadir("markers")
# define INKSCAPE_PALETTESDIR append_inkscape_datadir("palettes")
# define INKSCAPE_PATTERNSDIR append_inkscape_datadir("patterns")
@@ -111,6 +113,7 @@ char *get_extensions_path();
# define INKSCAPE_MARKERSDIR "Contents/Resources/share/inkscape/markers"
# define INKSCAPE_PALETTESDIR "Contents/Resources/share/inkscape/palettes"
# define INKSCAPE_PATTERNSDIR "Contents/Resources/share/inkscape/patterns"
+# define INKSCAPE_PYTHONDIR "Contents/Resources/share/inkscape/python"
# define INKSCAPE_SCREENSDIR "Contents/Resources/share/inkscape/screens"
# define INKSCAPE_SYMBOLSDIR "Contents/Resources/share/inkscape/symbols"
# define INKSCAPE_THEMEDIR "Contents/Resources/share/inkscape/themes"
@@ -137,6 +140,7 @@ char *get_extensions_path();
# define INKSCAPE_MARKERSDIR append_inkscape_datadir("inkscape/markers")
# define INKSCAPE_PALETTESDIR append_inkscape_datadir("inkscape/palettes")
# define INKSCAPE_PATTERNSDIR append_inkscape_datadir("inkscape/patterns")
+# define INKSCAPE_PYTHONDIR append_inkscape_datadir("inkscape/python")
# define INKSCAPE_SCREENSDIR append_inkscape_datadir("inkscape/screens")
# define INKSCAPE_SYMBOLSDIR append_inkscape_datadir("inkscape/symbols")
# define INKSCAPE_THEMEDIR append_inkscape_datadir("inkscape/themes")
diff --git a/src/pyhelper.cpp b/src/pyhelper.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4793f3a183736332b85dc02027620c52edeef719
--- /dev/null
+++ b/src/pyhelper.cpp
@@ -0,0 +1,458 @@
+/** @file
+ * @brief Glue layer between the Python embedding itself and Inkscape internals.
+ *
+ * Interface functions for embedding the Python interpreter are located in emb_python.cpp. However, to separate
+ * Inkscape internal and Python as much as possible, functions that require more than a few lines of Inkscape specific
+ * code should be located in this helper file.
+ *
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+/*
+ * This file contains some code copied/modified from other Inkscape source files since it is a kind of glue
+ * layer. However, I may have failed to place the correct notice on every location. (I don't know if this is even
+ * necessary.)
+ */
+
+#if WITH_PYTHON
+
+#include "pyhelper.h"
+
+namespace Inkscape {
+
+/**
+ * @brief Set or clear Inkscape's waiting cursor
+ * @return Nothing.
+ *
+ * @param[in] wait True enables the waiting cursor, false disables it.
+ *
+ * @todo Ideally, enabling the waiting cursor would ensure that the XML tree can only be changed from Python.
+ * Not sure if the implemented functionality is sufficient.
+ */
+void
+pyHelper::setWaitingCursor(bool wait) {
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ if(desktop) {
+ if(wait) {
+ desktop->setWaitingCursor();
+ desktop->disableInteraction();
+ } else {
+ desktop->clearWaitingCursor();
+ desktop->enableInteraction();
+ }
+ }
+}
+
+/**
+ * @brief Tries to get a verb and run it.
+ * @return true on successful execution.
+ *
+ * @param[in] verbString String of the verb to run.
+ */
+bool
+pyHelper::runVerb(const char *verbString) {
+ Inkscape::Verb *verb;
+ ActionContext context;
+ SPAction *action;
+
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ if(!desktop) {
+ std::cerr << _("runVerb: No active desktop.") << std::endl;
+ return false;
+ }
+
+ context = ActionContext(desktop);
+ verb = Inkscape::Verb::getbyid(verbString);
+
+ if (!verb) {
+ return false;
+ }
+
+ action = verb->get_action(context);
+ sp_action_perform(action, NULL);
+
+ return true;
+}
+
+/**
+ * @brief Saves the active desktop to a file.
+ *
+ * Function adapted from main-cmdlinexact.cpp.
+ *
+ * @param[in] verbString String of the verb to run.
+ * @param[in] mime MIME string to use as output file type.
+ * @param[out] msg In case of an error, the pointer is set to an appropriate static error message string.
+ * @param[in] fsm Selects whether the function should behave as "Save file as" or "Save a copy".
+ *
+ * @return true on success. In case of an error, msg is set to point to the error string.
+ */
+// Some code copied/based on file.cpp
+bool pyHelper::fileSaveAs(const char *filename, const char *mime, const char *& msg, Inkscape::Extension::FileSaveMethod fsm)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ bool success = true;
+
+ std::map extensionsMap = buildSaveExtensionMap();
+ std::map::iterator emi;
+
+ // Some sanity checks. Not sure if they are necessary.
+ if(desktop == nullptr) {
+ msg = "Desktop is nullptr";
+ return false;
+ }
+
+ SPDocument *doc = desktop->getDocument();
+
+ if(doc == nullptr) {
+ msg = "Document is nullptr";
+ return false;
+ }
+
+ emi = extensionsMap.find(Glib::ustring(mime));
+
+ if(emi == extensionsMap.end()) {
+ msg = "Unknown mime type";
+ return false;
+ }
+
+ Inkscape::Extension::Extension *ext = emi->second;
+
+ Inkscape::Version save = doc->getRoot()->version.inkscape;
+ doc->getReprRoot()->setAttribute("inkscape:version", Inkscape::version_string);
+
+ try {
+ Inkscape::Extension::save(ext, doc, filename,
+ false, // setextension
+ false, // check_overwrite
+ fsm == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS, // official
+ fsm);
+ } catch (Inkscape::Extension::Output::no_extension_found &e) {
+ msg = _("No Inkscape extension found to save the document.");
+ success = false;
+ } catch (Inkscape::Extension::Output::file_read_only &e) {
+ msg = _("File is write protected.");
+ success = false;
+ } catch (Inkscape::Extension::Output::save_failed &e) {
+ msg = _("File could not be saved.");
+ success = false;
+ } catch (Inkscape::Extension::Output::save_cancelled &e) {
+ msg = _("Save cancelled.");
+ success = false;
+ } catch (Inkscape::Extension::Output::export_id_not_found &e) {
+ msg = _("File could not be saved: No object with matching ID found.");
+ success = false;
+ } catch (std::exception &e) {
+ msg = _("File could not be saved.");
+ success = false;
+ } catch (...) {
+ g_critical("Extension '%s' threw an unspecified exception.", ext->get_id());
+ msg = _("File could not be saved. Unspecified exception");
+ success = false;
+ }
+
+ if(!success) {
+ // Restore Inkscape version
+ doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save ));
+ }
+
+ return success;
+}
+
+/**
+ * @brief Gets the output file type description string for a given MIME type
+ *
+ * @param[in] mime MIME string
+ *
+ * @example Querying "image/x-inkscape-svg" yields "Inkscape SVG (*.svg)".
+ *
+ * @return Pointer to description string, nullptr on error.
+ */
+const char *pyHelper::getOutputMIMEFileTypeName(const char *mime)
+{
+ std::map extensionsMap = pyHelper::buildSaveExtensionMap();
+ std::map::iterator emi;
+
+ emi = extensionsMap.find(Glib::ustring(mime));
+
+ if(emi == extensionsMap.end()) {
+ return nullptr;
+ }
+
+ Inkscape::Extension::Output *ext = emi->second;
+ return(ext->get_filetypename());
+}
+
+/**
+ * @brief Gets the output file type extension including the extension dot for a given MIME type.
+ *
+ * @param[in] mime MIME string
+ *
+ * @example Querying "image/x-inkscape-svg" yields ".svg".
+ *
+ * @return Pointer to description string, nullptr on error.
+ */
+const char *pyHelper::getOutputMIMEExtension(const char *mime)
+{
+ std::map extensionsMap = pyHelper::buildSaveExtensionMap();
+ std::map::iterator emi;
+
+ emi = extensionsMap.find(Glib::ustring(mime));
+
+ if(emi == extensionsMap.end()) {
+ return nullptr;
+ }
+
+ Inkscape::Extension::Output *ext = emi->second;
+ return(ext->get_extension());
+}
+
+/**
+ * @brief Gets the tooltip string for a given MIME type.
+ *
+ * @param[in] mime MIME string
+ *
+ * @example Querying "image/x-inkscape-svg" yields "SVG format with Inkscape extensions".
+ *
+ * @return Pointer to description string, nullptr on error.
+ */
+const char *pyHelper::getOutputMIMETooltip(const char *mime)
+{
+ std::map extensionsMap = pyHelper::buildSaveExtensionMap();
+ std::map::iterator emi;
+
+ emi = extensionsMap.find(Glib::ustring(mime));
+
+ if(emi == extensionsMap.end()) {
+ return nullptr;
+ }
+
+ Inkscape::Extension::Output *ext = emi->second;
+ return(ext->get_filetypetooltip());
+}
+
+/**
+ * @brief Creates a map for translating MIME type strings to Inkscape extensions
+ *
+ * Used FileSaveDialogImplGtk::createFileTypeMenu() as template.
+ *
+ * @param[in] mime MIME type string
+ *
+ * @return Map from MIME type string to extension.
+ */
+std::map pyHelper::buildSaveExtensionMap(void)
+{
+ std::map otypes;
+
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+
+ for (Inkscape::Extension::DB::OutputList::iterator current_item = extension_list.begin();
+ current_item != extension_list.end(); ++current_item) {
+ Inkscape::Extension::Output *omod = *current_item;
+
+ if (omod->deactivated())
+ continue;
+
+ Glib::ustring mime = omod->get_mimetype();
+ otypes.insert(std::pair(mime.casefold(), omod));
+ }
+
+ return otypes;
+}
+
+/**
+ * @brief Opens a file.
+ *
+ * @param[in] filename Name of file to load
+ * @param[out] msg In case of an error, the pointer is set to an appropriate static error message string.
+ *
+ * @return true on success
+ */
+bool pyHelper::fileOpen(const char *filename, const char *& msg)
+{
+ Glib::ustring fn(filename);
+
+ return sp_file_open(fn, // URI
+ nullptr, // key
+ false, // add_to_recent
+ true, // replace_empty
+ false, // interaction
+ msg);
+}
+
+/**
+ * @brief Export drawing to PNG file.
+ *
+ * @param[in] filename Name of the file to write
+ * @param[in] eArea Selects whether the drawing or page is the area to export
+ * @param[in] dpi Export resolution. If set to <= 0.0, the default settings are used
+ * @param[in] areaSnap Snaps the export area outwards to the next integer pixel values if true.
+ * @param[in] useDocumentBGColor The document background color is used if this is set to true. Otherwise @p bgColor is used a
+ * output background color.
+ * @param[in] bgColor PNG background color to use if @p useDocumentBGColor is set to false.
+ * Format: `bgColor = (red << 24) | (green << 16) | (blue << 8) | opacity` where
+ * `red`, `green`, `blue` and `opacity` are `uint8_t`.
+ * @param[out] msg In case of an error, the pointer is set to an appropriate static error message string.
+ *
+ * @return true on success
+ */
+// Based on main.cpp/do_export_png()
+bool pyHelper::exportPNG(const char *filename, PNGExportArea eArea, double dpi, bool areaSnap, bool useDocumentBGColor, guint32 bgColor, const char*& msg)
+{
+ SPDocument *doc = INKSCAPE.active_document();
+
+ bool filename_from_hint = false;
+
+ if(!doc) {
+ msg = _("No active document");
+ return false;
+ }
+
+ Geom::Rect area;
+
+ // Check if filename is empty?
+ if(strlen(filename) < 1) {
+ msg = _("Empty filename");
+ return false;
+ }
+
+ /** @fixme Oh well. This is now the third implementation of PNG exporting
+ * (the other ones are the GUI dialog and command line export). The functionality should really be unified into
+ * one configurable PNG exporter to remove duplicate code.
+ */
+ if(eArea == pyHelper::DRAWING) {
+ SPObject *o_area = nullptr;
+
+ o_area = doc->getRoot();
+
+ // write object bbox to area
+ doc->ensureUpToDate();
+ Geom::OptRect areaMaybe = static_cast(o_area)->desktopVisualBounds();
+ if(areaMaybe) {
+ area = *areaMaybe;
+ } else {
+ msg = _("Unable to determine a valid bounding box.");
+ return false;
+ }
+ }
+
+ if(eArea == pyHelper::PAGE) {
+ /* Export the whole page: note: Inkscape uses 'page' in all menus and dialogs, not 'canvas' */
+ doc->ensureUpToDate();
+ Geom::Point origin(doc->getRoot()->x.computed, doc->getRoot()->y.computed);
+ area = Geom::Rect(origin, origin + doc->getDimensions());
+ }
+
+ if(dpi <= 0.0) {
+ // default dpi
+ dpi = Inkscape::Util::Quantity::convert(1, "in", "px");
+ }
+
+ // set filename and dpi
+ if(dpi < pyHelper::PNG_EXPORT_DPI_MIN || dpi > pyHelper::PNG_EXPORT_DPI_MAX) {
+ msg = _("DPI value out of range.");
+ return false;
+ }
+
+ if (areaSnap) {
+ area = area.roundOutwards();
+ }
+
+ unsigned long int width = 0;
+ unsigned long int height = 0;
+
+ width = (unsigned long int) (Inkscape::Util::Quantity::convert(area.width(), "px", "in") * dpi + 0.5);
+ height = (unsigned long int) (Inkscape::Util::Quantity::convert(area.height(), "px", "in") * dpi + 0.5);
+
+ if(useDocumentBGColor) {
+ // read from namedview
+ Inkscape::XML::Node *nv = sp_repr_lookup_name (doc->rroot, "sodipodi:namedview");
+ if (nv && nv->attribute("pagecolor")){
+ bgColor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00);
+ }
+ if (nv && nv->attribute("inkscape:pageopacity")){
+ double opacity = 1.0;
+ sp_repr_get_double (nv, "inkscape:pageopacity", &opacity);
+ bgColor |= SP_COLOR_F_TO_U(opacity);
+ }
+ }
+
+ if ((width >= 1) && (height >= 1) && (width <= PNG_UINT_31_MAX) && (height <= PNG_UINT_31_MAX)) {
+ if( sp_export_png_file(doc, filename, area, width, height, dpi,
+ dpi, bgColor, nullptr, nullptr, true, std::vector()) == 1 ) {
+ } else {
+ msg = _("Failed to save bitmap");
+ return false;
+ }
+ } else {
+ g_warning("Calculated bitmap dimensions %lu %lu are out of range (1 - %lu). Nothing exported.", width, height, (unsigned long int)PNG_UINT_31_MAX);
+ msg = _("Calculated bitmap dimensions are out of range. Nothing exported.");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief Returns a vector with the URI strings of all desktops.
+ *
+ * @return Vector with uri strings of all desktops
+ */
+std::vector pyHelper::getDesktops(void)
+{
+ std::list desktops;
+ INKSCAPE.get_all_desktops(desktops);
+
+ std::vector v;
+ const char *uri;
+
+ for(std::list::iterator i = desktops.begin();
+ i != desktops.end(); ++i) {
+ SPDesktop * desktop = *i;
+ uri = desktop->getDocument()->getURI();
+ if(uri) {
+ v.push_back(uri);
+ } else {
+ v.push_back("");
+ }
+ }
+
+ return v;
+}
+
+/**
+ * @brief Set the active desktop.
+ *
+ * @param[in] n Index of the list returned by get_all_desktops to set active.
+ *
+ * @return true on success.
+ */
+bool pyHelper::setActiveDekstop(int n)
+{
+ std::list desktops;
+ INKSCAPE.get_all_desktops(desktops);
+
+ if(n > desktops.size() || n < 0) {
+ return false;
+ }
+
+ std::list::iterator it = desktops.begin();
+ std::advance(it, n);
+
+ INKSCAPE.activate_desktop(*it);
+
+ return true;
+}
+
+} // namespace Inkscape
+
+#endif // WITH_PYTHON
+
diff --git a/src/pyhelper.h b/src/pyhelper.h
new file mode 100644
index 0000000000000000000000000000000000000000..2b8ed5093a52f90c797da25260152959ba9592f1
--- /dev/null
+++ b/src/pyhelper.h
@@ -0,0 +1,71 @@
+/** @file
+ * @brief Glue layer between the Python embedding itself and Inkscape internals.
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+#if WITH_PYTHON
+
+#ifndef INKSCAPE_PYHELPER_H
+#define INKSCAPE_PYHELPER_H
+
+#include
+#include
+
+#include "io/resource.h"
+#include "inkscape-version.h"
+#include "verbs.h"
+#include "desktop.h"
+#include "inkscape.h"
+#include "helper/action.h"
+
+#include "extension/db.h"
+#include "extension/system.h"
+#include "file.h"
+#include "object/sp-root.h"
+#include "extension/output.h"
+#include "svg/svg-color.h"
+#include "helper/png-write.h"
+#include "util/units.h"
+
+namespace Inkscape {
+
+class pyHelper {
+public:
+ enum PNGExportArea {
+ PAGE,
+ DRAWING
+ };
+
+ static constexpr double PNG_EXPORT_DPI_MIN = 0.1;
+ static constexpr double PNG_EXPORT_DPI_MAX = 10000.0;
+ static constexpr int RGB_MIN = 0;
+ static constexpr int RGB_MAX = 0xff;
+
+ static bool runVerb(const char *verbString);
+ static void setWaitingCursor(bool wait);
+ static bool fileSaveAs(const char *filename, const char *mime, const char *& msg, Inkscape::Extension::FileSaveMethod fsm = Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS);
+ static bool fileOpen(const char *filename, const char *& msg);
+
+ static std::map buildSaveExtensionMap(void);
+ static const char *getOutputMIMEFileTypeName(const char *mime);
+ static const char *getOutputMIMEExtension(const char *mime);
+ static const char *getOutputMIMETooltip(const char *mime);
+
+ static bool exportPNG(const char *filename, PNGExportArea eArea, double dpi, bool areaSnap, bool useDocumentBGColor, guint32 bgColor, const char*& msg);
+
+ static std::vector getDesktops(void);
+ static bool setActiveDekstop(int n);
+};
+
+} // namespace Inkscape
+
+
+#endif // #ifdef INKSCAPE_PYHELPER_H
+
+#endif // WITH_PYTHON
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
index 699f85304d4bad263f2c43bf24fb3569f274e043..c4442b431e93e14b806c777c25e6207748248c80 100644
--- a/src/ui/CMakeLists.txt
+++ b/src/ui/CMakeLists.txt
@@ -81,6 +81,7 @@ set(ui_SRC
dialog/aboutbox.cpp
dialog/align-and-distribute.cpp
+ dialog/console.cpp
dialog/calligraphic-profile-rename.cpp
dialog/clonetiler.cpp
dialog/color-item.cpp
diff --git a/src/ui/dialog/console.cpp b/src/ui/dialog/console.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..87c1a966beda1e6ba7ba80c28ba877afcb4481ca
--- /dev/null
+++ b/src/ui/dialog/console.cpp
@@ -0,0 +1,527 @@
+/**
+ * @file
+ * Command line dialog - implementation.
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+/*
+ * Templates used for this file:
+ * src/ui/dialog/prototype.[cpp,h]
+ * src/ui/dialog/align-and-distribute.[cpp,h]
+ * src/ui/dialog/fill-and-stroke.[cpp,h]
+ * src/ui/dialog/filedialog.[cpp,h]
+ * src/ui/dialog/filedialogimpl-*
+ * ... and probably others I cannot remember.
+ */
+
+#if WITH_PYTHON
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "console.h"
+#include "desktop.h"
+#include "document.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/interface.h"
+
+
+#include "object/sp-line.h"
+#include "context-fns.h"
+#include "desktop-style.h"
+#include "attributes.h"
+#include "xml/attribute-record.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+Console::Console() :
+ UI::Widget::Panel("/dialogs/console", SP_VERB_DIALOG_CONSOLE),
+ _btnClear(_("Clear")),
+ _pg_console(Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL))),
+ _pg_scriptlist(Gtk::manage(new Gtk::ScrolledWindow())),
+ _desktopChangeConn(),
+ _deskTrack()
+{
+
+ // Set Fonts
+ Pango::FontDescription fd(_CMD_FONT);
+ _cmd.override_font(fd);
+ _output.override_font(fd);
+
+ // Create tags for TextView
+ Glib::RefPtr tag;
+
+ _outBuf = _output.get_buffer();
+
+ _tagInput = _outBuf->create_tag();
+ _tagInput->property_foreground() = "#0000ff";
+ _tagInput->set_property("weight", Pango::WEIGHT_BOLD);
+
+ _tagOutput = _outBuf->create_tag();
+
+ _tagError = _outBuf->create_tag();
+ _tagError->property_foreground() = "#ff0000";
+
+ _tagComment = _outBuf->create_tag();
+ _tagComment->property_foreground() = "#808080";
+
+ _outBuf->create_mark("scroll", _outBuf->end(), TRUE);
+
+ // Set tooltips
+ _cmd.set_tooltip_text(_("Enter command here. Use up/down-arrows keys to access last command."));
+ _output.set_tooltip_text(_("Previous commands and output"));
+ _btnClear.set_tooltip_text(_("Clear previous commands and output window."));
+
+ // Add the widgets
+ // Command line page
+ _output.set_editable(false);
+ _output.set_border_width(_BORDER_WIDTH);
+ _scrWin.add(_output);
+
+ _pg_console->pack_start(_scrWin);
+ _pg_console->pack_start(_cmd, Gtk::PACK_SHRINK);
+ _pg_console->pack_start(_btnClear, Gtk::PACK_SHRINK);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Script file page
+ for(int i = 0; i < Console::nscripts; i++) {
+ std::string ppath = "/dialogs/console/scriptFile" + std::to_string(i);
+
+ Gtk::Label *scriptFile = new Gtk::Label(prefs->getString(ppath));
+ Gtk::Button *btnRun = new Gtk::Button(_("Run!"));
+ Gtk::Button *btnSet = new Gtk::Button(_("..."));
+
+ scriptFile->set_hexpand(true);
+ scriptFile->set_halign(Gtk::ALIGN_START);
+
+ btnRun->set_tooltip_text(_("Run Python file"));
+ btnSet->set_tooltip_text(_("Set Python file"));
+ scriptFile->set_tooltip_text("Python file");
+
+ _scriptFiles.push_back(scriptFile);
+ _scriptRun.push_back(btnRun);
+ _scriptSet.push_back(btnSet);
+
+ // Attach events
+ btnRun->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Console::onBtnScriptRun), i));
+ btnSet->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Console::onBtnScriptSet), i));
+
+ _scriptGrid.attach(*scriptFile, 0, i, 1, 1);
+ _scriptGrid.attach(*btnRun, 2, i, 1, 1);
+ _scriptGrid.attach(*btnSet, 3, i, 1, 1);
+ }
+ _pg_scriptlist->add(_scriptGrid);
+
+ // Add notebook pages
+ _notebook.append_page(*_pg_console, _createPageTabLabel(_("_Console"), INKSCAPE_ICON("dialog-console")));
+ _notebook.append_page(*_pg_scriptlist, _createPageTabLabel(_("Scripts"), INKSCAPE_ICON("dialog-console-script")));
+
+ _getContents()->pack_start(_notebook);
+
+ // Add widget signals
+ //_notebook.signal_switch_page().connect(sigc::mem_fun(this, &Console::_onSwitchPage));
+
+ _cmd.signal_activate().connect(sigc::mem_fun(*this, &Console::onCmdEnter));
+
+ set_events(Gdk::KEY_PRESS_MASK);
+ _cmd.signal_key_press_event().connect(sigc::mem_fun(*this, &Console::onKeypress));
+
+ _btnClear.signal_clicked().connect(sigc::mem_fun(*this, &Console::onBtnClear));
+
+ // Program name already be initialized by main(), so the argument does not matter.
+ _python = Inkscape::pyInstance::getInstance("");
+
+ // Connect to signals for the stdout and stderr redirection
+ _python->signal_stdout().connect(mem_fun(*this, &Console::onStdOut));
+ _python->signal_stderr().connect(mem_fun(*this, &Console::onStdErr));
+ _python->signal_clear().connect(mem_fun(*this, &Console::onClear));
+
+ // Setup desktop connection
+ _desktopChangeConn = _deskTrack.connectDesktopChanged(sigc::mem_fun(*this, &Console::setTargetDesktop));
+ _deskTrack.connect(GTK_WIDGET(gobj()));
+}
+
+Console::~Console()
+{
+ for(std::vector::iterator it = _scriptFiles.begin(); it != _scriptFiles.end(); ++it) {
+ delete(*it);
+ }
+ for(std::vector::iterator it = _scriptSet.begin(); it != _scriptSet.end(); ++it) {
+ delete(*it);
+ }
+ for(std::vector::iterator it = _scriptRun.begin(); it != _scriptRun.end(); ++it) {
+ delete(*it);
+ }
+
+ _desktopChangeConn.disconnect();
+ _deskTrack.disconnect();
+}
+
+/*
+ * Called when a dialog is displayed, including when a dialog is reopened.
+ * (When a dialog is closed, it is not destroyed so the contructor is not called.
+ * This function can handle any reinitialization needed.)
+ */
+void
+Console::present()
+{
+ UI::Widget::Panel::present();
+}
+
+
+void
+Console::onCmdEnter() {
+ Glib::ustring str;
+
+ str = _cmd.get_text();
+
+ history.push_back(str);
+
+ _cmd.set_text("");
+
+ str.append("\n");
+
+ appendInput(str);
+
+ _python->runString(str.c_str());
+}
+
+bool
+Console::onKeypress(GdkEventKey *key_event) {
+ if(key_event->keyval == GDK_KEY_Up) {
+ history.previous_pos();
+ _cmd.set_text(history.get());
+
+ return true; // Don't propagate upwards to prevent selection of Notebook pane.
+ }
+ if(key_event->keyval == GDK_KEY_Down) {
+ history.next_pos();
+ _cmd.set_text(history.get());
+
+ return true; // Don't propagate upwards to prevent selection of Notebook pane.
+ }
+
+ return false;
+}
+
+void
+Console::onStdOut(const char *str) {
+ Glib::ustring ustr;
+
+ ustr.append(str);
+
+ appendOutput(ustr);
+}
+
+void
+Console::onStdErr(const char *str) {
+ Glib::ustring ustr;
+
+ ustr.append(str);
+
+ appendError(ustr);
+}
+
+void
+Console::onClear(void) {
+ clearOutput();
+}
+
+void
+Console::onBtnClear() {
+ clearOutput();
+}
+
+void
+Console::clearOutput() {
+ _outBuf->set_text("");
+}
+
+void
+Console::appendWithTag(Glib::ustring &str, Glib::RefPtr &tag)
+{
+ // Insert new text
+ _outBuf->insert_with_tag(_outBuf->end(), str, tag);
+
+ // Scroll to end. See textscroll.c in GTK demos.
+ /**
+ * @fixme If we scroll to the end while the notebook page with the scrolled window is not active, i.e., another
+ * page is selected, the scrollbar disappears. Searched around forever but could not find out what's going wrong.
+ */
+ Gtk::TextBuffer::iterator it = _outBuf->end();
+ it.set_line_offset(0); // Move iterator to beginning of line to avoid horizontal scrolling.
+ Glib::RefPtr mark = _outBuf->get_mark("scroll");
+ _outBuf->move_mark(mark, it);
+ _output.scroll_to(mark);
+}
+
+void
+Console::appendOutput(Glib::ustring &str)
+{
+ appendWithTag(str, _tagOutput);
+}
+
+void
+Console::appendInput(Glib::ustring &str)
+{
+ Glib::ustring str2 = "> " + str;
+ appendWithTag(str2, _tagInput);
+}
+
+void
+Console::appendError(Glib::ustring &str)
+{
+ appendWithTag(str, _tagError);
+}
+
+void
+Console::appendComment(Glib::ustring &str)
+{
+ appendWithTag(str, _tagComment);
+}
+
+void
+Console::onBtnScriptSet(guint nr) {
+ if(nr < 0 || nr >= Console::nscripts) {
+ std::cerr << "Script button index out of bounds. Should not happen." << std::endl; /// @todo Use g_assert?
+ }
+
+ Gtk::FileChooserDialog dlg(_("Please choose a python file"), Gtk::FILE_CHOOSER_ACTION_OPEN);
+
+ sp_transientize(GTK_WIDGET(gobj())); /// @fixme Copy-pasted from Inkscape's file dialog. Don't know what I am doing here.
+
+ dlg.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ dlg.add_button(_("_Select file"), Gtk::RESPONSE_OK);
+
+ auto pyFilter = Gtk::FileFilter::create();
+ pyFilter->set_name("Python files");
+ pyFilter->add_pattern("*.py");
+ dlg.add_filter(pyFilter);
+
+ auto allFilter = Gtk::FileFilter::create();
+ allFilter->set_name("All files");
+ allFilter->add_pattern("*");
+ dlg.add_filter(allFilter);
+
+ int result = dlg.run();
+
+ switch(result) {
+ case Gtk::RESPONSE_OK:
+ break;
+ case Gtk::RESPONSE_CANCEL:
+ break;
+ default:
+ std::cerr << "Unexpected file chooser result. Should actually not happen." << std::endl; // TODO: Use g_assert
+ return;
+ }
+
+ Gtk::Label *lbl = _scriptFiles[nr];
+
+ lbl->set_text(dlg.get_filename());
+
+ // Store the filename in the prefences.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::string ppath = "/dialogs/console/scriptFile" + std::to_string(nr);
+ prefs->setString(ppath, dlg.get_filename());
+}
+
+void
+Console::onBtnScriptRun(guint nr) {
+ Gtk::Label *lbl = _scriptFiles[nr];
+
+ Glib::ustring lblTxt = lbl->get_text();
+
+ if(lblTxt.length() < 1) {
+ sp_ui_error_dialog(_("Script file not set.\nPlease use the \"...\" button to select a file before clicking \"Run!\"."));
+ return;
+ }
+
+ Glib::ustring cStr = "Running " + lblTxt + "\n";
+ appendComment(cStr);
+ _python->runFile(lbl->get_text().c_str());
+}
+
+
+// Copied from fill-and-stroke.cpp
+Gtk::HBox&
+Console::_createPageTabLabel(const Glib::ustring& label, const char *label_image)
+{
+ Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 4));
+
+ auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU));
+ _tab_label_box->pack_start(*img);
+
+ Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true));
+ _tab_label_box->pack_start(*_tab_label);
+ _tab_label_box->show_all();
+
+ return *_tab_label_box;
+}
+
+//void
+//Console::_onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
+//{
+//}
+
+void
+Console::setTargetDesktop(SPDesktop *desktop)
+{
+ if(targetDesktop != desktop) {
+ targetDesktop = desktop;
+ }
+}
+
+entryHistory::entryHistory() {
+}
+
+
+/**
+ * @brief Handles the wrap-around of the ringbuffer index.
+ *
+ * Warning: Does not handle the case of going around the whole buffer more than once!
+ *
+ * @return Wrapped around value of the ring buffer index.
+ *
+ * @param[in] v Ring buffer index to be wrapped around, i.e. exceeding the min/max index of the buffer.
+ *
+ */
+int
+entryHistory::handleWrap(int v) {
+ // Mind the order of the if statements. (Neg. value must be treated first.)
+
+ if(v < 0) {
+ return hEntries.size() + v;
+ }
+
+ if(v >= hEntries.size()) {
+ return v - hEntries.size();
+ }
+
+ return v;
+}
+
+/**
+ * @brief Inserts a new command into the ring buffer and resets the reading position to the this (newest) entry.
+ *
+ * @return Nothing.
+ *
+ * @param[in] s String to insert.
+ *
+ */
+void
+entryHistory::push_back(Glib::ustring s) {
+ hEntries.at(wrPos) = s;
+
+ wrPos = handleWrap(wrPos + 1);
+
+ reset_pos();
+}
+
+/**
+ * @brief Sets the reading position to the last written position.
+ *
+ * @return Nothing.
+ *
+ */
+void
+entryHistory::reset_pos(void) {
+ rdPos = wrPos;
+}
+
+/**
+ * @brief Apply a change of the reading position in the buffer, but only if the selected buffer slot is not empty.
+ *
+ * This function is called internally to select a new reading position in the history ring buffer.
+ * Since it is useless to let the user scroll though empty history slots (slots that haven't been filled with
+ * commands), the new position is only accepted if it actually contains some data. Otherwise, the old index @p idx is
+ * returned without change.
+ *
+ * @return New reading pointer position.
+ *
+ * @param[in] idx Index value to apply offset to.
+ * @param[in] offset Offset value to apply to @p idx. Can be positive or negative.
+ *
+ */
+int
+entryHistory::applyWithCheck(int idx, int offset) {
+ // handleWrap can only handle one wrap around at once at most.
+ if((offset > 0 && offset > hEntries.size()) || (offset < 0 && -offset > hEntries.size())) {
+ std::cout << "entryHistory::applyWithCheck: Offset too large!" << std::endl;
+ return idx;
+ }
+
+ int newIdx = handleWrap(idx + offset);
+
+ if(!hEntries.at(newIdx).empty()) { // Accept the new value only if there is a stored value
+ return newIdx;
+ }
+
+ return idx;
+}
+
+/**
+ * @brief Select the previous position in the history ring buffer if it is non-empty.
+ *
+ * Call this function if the user presses the key to go back (in the direction of older commands) one entry in the history.
+ *
+ */
+void
+entryHistory::previous_pos(void) {
+ rdPos = applyWithCheck(rdPos, -1);
+}
+
+/**
+ * @brief Select the next position in the history ring buffer if it is non-empty.
+ *
+ * Call this function if the user presses the key to go forward (in the direction of newer commands) one entry in the history.
+ *
+ */
+void
+entryHistory::next_pos(void) {
+ rdPos = applyWithCheck(rdPos, 1);
+}
+
+/**
+ * @brief Get the currently selected history entry.
+ *
+ * @return Currently selected hisory entry.
+ *
+ */
+Glib::ustring
+entryHistory::get(void) {
+ return hEntries.at(rdPos);
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif // WITH_PYTHON
+
+/*
+ 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:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/console.h b/src/ui/dialog/console.h
new file mode 100644
index 0000000000000000000000000000000000000000..ff82415222f5db8bfe8f226921d7097fe08518cf
--- /dev/null
+++ b/src/ui/dialog/console.h
@@ -0,0 +1,196 @@
+/** @file
+ * @brief Command line dialog
+ */
+/* Authors:
+ * Thomas Wiesner
+ *
+ * Copyright (C) 2018 Authors
+ *
+ * Released under GNU GPL. Read the file 'COPYING' for more information.
+ */
+
+/*
+ * Templates used for this file:
+ * src/ui/dialog/prototype.[cpp,h]
+ * src/ui/dialog/align-and-distribute.[cpp,h]
+ * src/ui/dialog/fill-and-stroke.[cpp,h]
+ * src/ui/dialog/filedialog.[cpp,h]
+ * src/ui/dialog/filedialogimpl-*
+ * ... and probably others I cannot remember.
+ */
+
+
+#if WITH_PYTHON
+
+#ifndef INKSCAPE_UI_DIALOG_CONSOLE_H
+#define INKSCAPE_UI_DIALOG_CONSOLE_H
+
+#include
+#include
+#include "ui/widget/panel.h"
+#include "ui/widget/frame.h"
+#include "ui/widget/notebook-page.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "2geom/rect.h"
+#include "ui/dialog/desktop-tracker.h"
+#include "ui/dialog-events.h"
+
+#include "emb_python.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * @class entryHistory
+ * @brief Manages the history of commands entered in the Python console.
+ */
+class entryHistory {
+public:
+ entryHistory();
+
+ void push_back(Glib::ustring s); // Add a new history entry
+ void reset_pos(void); // Reset read position to last written
+ void previous_pos(void); // Move to previous item in history
+ void next_pos(void); // Move to get item in history
+ Glib::ustring get(void); // Get current item in history
+
+protected:
+
+private:
+ // Noncopyable, nonassignable
+ entryHistory(entryHistory const &d);
+ entryHistory operator=(entryHistory const &d);
+
+ static const int historyDepth = 32;
+ int rdPos = 0;
+ int wrPos = 0;
+
+ std::array hEntries;
+
+ int handleWrap(int v); // Handles ring buffer index wrap around
+ int applyWithCheck(int idx, int offset); // Apply offset to index, but only if the new index points to a non-empty slot.
+};
+
+/**
+ * @class Console
+ * @brief Dialog implementing a Python console. Also has a tab for runnnng script files.
+ */
+class Console : public Widget::Panel {
+public:
+ virtual ~Console();
+
+ static Console &getInstance() { return *new Console(); }
+
+ virtual void present();
+
+protected:
+
+private:
+ // No default constructor, noncopyable, nonassignable
+ Console();
+ Console(Console const &d);
+ Console operator=(Console const &d);
+
+ const guint _BORDER_WIDTH = 5;
+ const Glib::ustring _CMD_FONT = "Monospace";
+
+ const int nscripts = 16;
+
+ entryHistory history;
+
+ // Widgets
+ Gtk::Notebook _notebook;
+ Gtk::Box *_pg_console;
+ Gtk::ScrolledWindow *_pg_scriptlist;
+
+
+ Gtk::ScrolledWindow _scrWin;
+ Gtk::TextView _output;
+ Glib::RefPtr _outBuf;
+ Gtk::Entry _cmd;
+ Gtk::Button _btnClear;
+
+ // Script file widgets
+ Gtk::Grid _scriptGrid;
+ std::vector _scriptFiles;
+ std::vector _scriptSet;
+ std::vector _scriptRun;
+
+ Glib::RefPtr _tagInput;
+ Glib::RefPtr _tagOutput;
+ Glib::RefPtr _tagError;
+ Glib::RefPtr _tagComment;
+
+ void onBtnScriptSet(guint nr);
+ void onBtnScriptRun(guint nr);
+
+ void _updateLabel();
+
+ void onCmdEnter();
+ bool onKeypress(GdkEventKey *key_event);
+
+
+ void onBtnClear();
+
+ void onStdOut(const char *);
+ void onStdErr(const char *);
+ void onClear(void);
+
+ // Misc member functions
+ void clearOutput();
+
+ void appendWithTag(Glib::ustring &str, Glib::RefPtr &tag);
+
+ void appendOutput(Glib::ustring &str);
+ void appendInput(Glib::ustring &str);
+ void appendError(Glib::ustring &str);
+ void appendComment(Glib::ustring &str);
+
+ pyInstance *_python;
+
+ // Widgets helpers
+ Gtk::HBox& _createPageTabLabel(const Glib::ustring& label, const char *label_image);
+ void _onSwitchPage(Gtk::Widget * /*page*/, guint pagenum);
+// void _savePagePref(guint page_num);
+
+ // Desktop connection
+ DesktopTracker _deskTrack;
+ sigc::connection _desktopChangeConn;
+ SPDesktop *targetDesktop;
+
+ void setTargetDesktop(SPDesktop *desktop);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_COMMANDLINE_H
+
+#endif // WITH_PYTHON
+
+/*
+ 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:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp
index 7ff0968d05df5d5dd9468b6481ead85f056e1090..9fedd9eb255d15cdcd60c981d991a6beca5aeaba 100644
--- a/src/ui/dialog/dialog-manager.cpp
+++ b/src/ui/dialog/dialog-manager.cpp
@@ -18,6 +18,7 @@
#include "ui/dialog/prototype.h"
#include "ui/dialog/align-and-distribute.h"
+#include "ui/dialog/console.h"
#include "ui/dialog/document-metadata.h"
#include "ui/dialog/document-properties.h"
#include "ui/dialog/extension-editor.h"
@@ -107,6 +108,9 @@ DialogManager::DialogManager() {
registerFactory("Prototype", &create);
registerFactory("AlignAndDistribute", &create);
registerFactory("DocumentMetadata", &create);
+#if WITH_PYTHON
+ registerFactory("Console", &create);
+#endif
registerFactory("DocumentProperties", &create);
registerFactory("ExtensionEditor", &create);
registerFactory("FillAndStroke", &create);
@@ -150,6 +154,9 @@ DialogManager::DialogManager() {
registerFactory("Prototype", &create);
registerFactory("AlignAndDistribute", &create);
registerFactory("DocumentMetadata", &create);
+#if WITH_PYTHON
+ registerFactory("Console", &create);
+#endif
registerFactory("DocumentProperties", &create);
registerFactory("ExtensionEditor", &create);
registerFactory("FillAndStroke", &create);
diff --git a/src/verbs.cpp b/src/verbs.cpp
index 8ee8d671dc014dd7d1f82f0c189b6e78b373b288..afff8f98902e617dc62c13659f641921b9ae6c8b 100644
--- a/src/verbs.cpp
+++ b/src/verbs.cpp
@@ -816,7 +816,7 @@ Verb *Verb::get_search(unsigned int code)
/**
* Find a Verb using it's ID.
*
- * This function uses the \c _verb_ids has table to find the
+ * This function uses the \c _verb_ids hash table to find the
* verb by it's id. Should be much faster than previous
* implementations.
*
@@ -2216,6 +2216,9 @@ void DialogVerb::perform(SPAction *action, void *data)
case SP_VERB_DIALOG_ALIGN_DISTRIBUTE:
dt->_dlg_mgr->showDialog("AlignAndDistribute");
break;
+ case SP_VERB_DIALOG_CONSOLE:
+ dt->_dlg_mgr->showDialog("Console");
+ break;
case SP_VERB_DIALOG_SPRAY_OPTION:
dt->_dlg_mgr->showDialog("SprayOptionClass");
break;
@@ -3215,7 +3218,11 @@ Verb *Verb::_base_verbs[] = {
N_("Select which color separations to render in Print Colors Preview rendermode"), nullptr),
new DialogVerb(SP_VERB_DIALOG_EXPORT, "DialogExport", N_("_Export PNG Image..."),
N_("Export this document or a selection as a PNG image"), INKSCAPE_ICON("document-export")),
- // Help
+#if WITH_PYTHON
+ new DialogVerb(SP_VERB_DIALOG_CONSOLE, "DialogConsole", N_("Python console..."),
+ N_("Python console"), INKSCAPE_ICON("dialog-console")),
+#endif
+ // Help
new HelpVerb(SP_VERB_HELP_ABOUT_EXTENSIONS, "HelpAboutExtensions", N_("About E_xtensions"),
N_("Information on Inkscape extensions"), nullptr),
new HelpVerb(SP_VERB_HELP_MEMORY, "HelpAboutMemory", N_("About _Memory"), N_("Memory usage information"),
diff --git a/src/verbs.h b/src/verbs.h
index 595d2aacfbf7e8c7134e455af0e3a436e28cf765..e004a22e4b1ef35f5d5bcb50ac137489a108a322 100644
--- a/src/verbs.h
+++ b/src/verbs.h
@@ -343,6 +343,7 @@ enum {
SP_VERB_DIALOG_SVG_FONTS,
SP_VERB_DIALOG_PRINT_COLORS_PREVIEW,
SP_VERB_DIALOG_EXPORT,
+ SP_VERB_DIALOG_CONSOLE,
/* Help */
SP_VERB_HELP_ABOUT_EXTENSIONS,
SP_VERB_HELP_MEMORY,
@@ -604,6 +605,7 @@ public:
return get_search(code);
}
}
+
static Verb * getbyid (gchar const * id, bool verbose = true);
/**