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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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); /**