diff --git a/src/Ext/freecad/CMakeLists.txt b/src/Ext/freecad/CMakeLists.txt index c39e5e07eb..d1d660f337 100644 --- a/src/Ext/freecad/CMakeLists.txt +++ b/src/Ext/freecad/CMakeLists.txt @@ -27,6 +27,7 @@ endif() configure_file(__init__.py.template ${NAMESPACE_INIT}) set(EXT_FILES + freecad_doc.py part.py partdesign.py project_utility.py diff --git a/src/Ext/freecad/freecad_doc.py b/src/Ext/freecad/freecad_doc.py new file mode 100644 index 0000000000..8ca2bb2b7c --- /dev/null +++ b/src/Ext/freecad/freecad_doc.py @@ -0,0 +1,63 @@ +# (c) 2024 Werner Mayer LGPL + +__title__="FreeCAD Python documentation" +__author__ = "Werner Mayer" +__url__ = "https://www.freecad.org" +__doc__ = "Helper module to use pydoc" + + +import os, sys, pydoc, pkgutil + +class FreeCADDoc(pydoc.HTMLDoc): + def index(self, dir, shadowed=None): + """ Generate an HTML index for a directory of modules.""" + modpkgs = [] + if shadowed is None: shadowed = {} + for importer, name, ispkg in pkgutil.iter_modules([dir]): + if name == 'Init': + continue + if name == 'InitGui': + continue + if name[-2:] == '_d': + continue + modpkgs.append((name, '', ispkg, name in shadowed)) + shadowed[name] = 1 + + if len(modpkgs) == 0: + return None + + modpkgs.sort() + contents = self.multicolumn(modpkgs, self.modpkglink) + return self.bigsection(dir, '#ffffff', '#ee77aa', contents) + +def bltinlink(name): + return '%s' % (name, name) + +def createDocumentation(): + pydoc.html = FreeCADDoc() + title = 'FreeCAD Python Modules Index' + + heading = pydoc.html.heading('Python: Index of Modules','#ffffff', '#7799ee') + + + names = list(filter(lambda x: x != '__main__', sys.builtin_module_names)) + contents = pydoc.html.multicolumn(names, bltinlink) + indices = ['

' + pydoc.html.bigsection('Built-in Modules', '#ffffff', '#ee77aa', contents)] + + names = ['FreeCAD', 'FreeCADGui'] + contents = pydoc.html.multicolumn(names, bltinlink) + indices.append('

' + pydoc.html.bigsection('Built-in FreeCAD Modules', '#ffffff', '#ee77aa', contents)) + + seen = {} + for dir in sys.path: + dir = os.path.realpath(dir) + ret = pydoc.html.index(dir, seen) + if ret != None: + indices.append(ret) + + contents = heading + ' '.join(indices) + '''

+ +pydoc by Ka-Ping Yee <ping@lfw.org>''' + + htmldocument = pydoc.html.page(title, contents) + return htmldocument diff --git a/src/Gui/OnlineDocumentation.cpp b/src/Gui/OnlineDocumentation.cpp index 7d786601be..e6c70cfb93 100644 --- a/src/Gui/OnlineDocumentation.cpp +++ b/src/Gui/OnlineDocumentation.cpp @@ -39,6 +39,7 @@ using namespace Gui; // the favicon +// NOLINTBEGIN static const unsigned int navicon_data_len = 318; static const unsigned char navicon_data[] = { 0x00,0x00,0x01,0x00,0x01,0x00,0x10,0x10,0x10,0x00,0x01,0x00,0x04,0x00, @@ -64,6 +65,7 @@ static const unsigned char navicon_data[] = { 0x00,0x00,0x93,0xfd,0x00,0x00,0x81,0xd8,0x00,0x00,0x99,0x9d,0x00,0x00, 0x9c,0x3d,0x00,0x00,0x9f,0xfd,0x00,0x00,0x80,0xfd,0x00,0x00,0xff,0x7d, 0x00,0x00,0xfe,0x01,0x00,0x00,0xff,0x7f,0x00,0x00}; +// NOLINTEND PythonOnlineHelp::PythonOnlineHelp() = default; @@ -71,146 +73,99 @@ PythonOnlineHelp::~PythonOnlineHelp() = default; QByteArray PythonOnlineHelp::loadResource(const QString& filename) const { - QString fn; - fn = filename.mid(1); - QByteArray res; - - if (fn == QLatin1String("favicon.ico")) { - // Return a resource icon in ico format - QBuffer buffer; - buffer.open(QBuffer::WriteOnly); - QImageWriter writer; - writer.setDevice(&buffer); - writer.setFormat("ICO"); - if (writer.canWrite()) { - QPixmap px = qApp->windowIcon().pixmap(24,24); - writer.write(px.toImage()); - buffer.close(); - res = buffer.data(); - } - else { - // fallback - res.reserve(navicon_data_len); - for (int i=0; i<(int)navicon_data_len;i++) { - res[i] = navicon_data[i]; - } - } + if (filename == QLatin1String("/favicon.ico")) { + return loadFavicon(); } - else if (filename == QLatin1String("/")) { - // get the global interpreter lock otherwise the app may crash with the error - // 'PyThreadState_Get: no current thread' (see pystate.c) - Base::PyGILStateLocker lock; - PyObject* main = PyImport_AddModule("__main__"); - PyObject* dict = PyModule_GetDict(main); - dict = PyDict_Copy(dict); - QByteArray cmd = - "import os, sys, pydoc, pkgutil\n" - "\n" - "class FreeCADDoc(pydoc.HTMLDoc):\n" - " def index(self, dir, shadowed=None):\n" - " \"\"\"Generate an HTML index for a directory of modules.\"\"\"\n" - " modpkgs = []\n" - " if shadowed is None: shadowed = {}\n" - " for importer, name, ispkg in pkgutil.iter_modules([dir]):\n" - " if name == 'Init': continue\n" - " if name == 'InitGui': continue\n" - " if name[-2:] == '_d': continue\n" - " modpkgs.append((name, '', ispkg, name in shadowed))\n" - " shadowed[name] = 1\n" - "\n" - " if len(modpkgs) == 0: return None\n" - " modpkgs.sort()\n" - " contents = self.multicolumn(modpkgs, self.modpkglink)\n" - " return self.bigsection(dir, '#ffffff', '#ee77aa', contents)\n" - "\n" - "pydoc.html=FreeCADDoc()\n" - "title='FreeCAD Python Modules Index'\n" - "\n" - "heading = pydoc.html.heading(" - "'Python: Index of Modules'," - "'#ffffff', '#7799ee')\n" - "def bltinlink(name):\n" - " return '%s' % (name, name)\n" - "names = list(filter(lambda x: x != '__main__',\n" - " sys.builtin_module_names))\n" - "contents = pydoc.html.multicolumn(names, bltinlink)\n" - "indices = ['

' + pydoc.html.bigsection(\n" - " 'Built-in Modules', '#ffffff', '#ee77aa', contents)]\n" - "\n" - "names = ['FreeCAD', 'FreeCADGui']\n" - "contents = pydoc.html.multicolumn(names, bltinlink)\n" - "indices.append('

' + pydoc.html.bigsection(\n" - " 'Built-in FreeCAD Modules', '#ffffff', '#ee77aa', contents))\n" - "\n" - "seen = {}\n" - "for dir in sys.path:\n" - " dir = os.path.realpath(dir)\n" - " ret = pydoc.html.index(dir, seen)\n" - " if ret != None:\n" - " indices.append(ret)\n" - "contents = heading + ' '.join(indices) + '''

\n" - "\n" - "pydoc by Ka-Ping Yee <ping@lfw.org>'''\n" - "htmldocument=pydoc.html.page(title,contents)\n"; + if (filename == QLatin1String("/")) { + return loadIndexPage(); + } - PyObject* result = PyRun_String(cmd.constData(), Py_file_input, dict, dict); - if (result) { - Py_DECREF(result); - result = PyDict_GetItemString(dict, "htmldocument"); - const char* contents = PyUnicode_AsUTF8(result); - res.append("HTTP/1.0 200 OK\n"); - res.append("Content-type: text/html\n"); - res.append(contents); - return res; - } - else { - // load the error page - Base::PyException e; - res = loadFailed(QString::fromUtf8(e.what())); - } + return loadHelpPage(filename); +} - Py_DECREF(dict); +QByteArray PythonOnlineHelp::loadFavicon() const +{ + // Return a resource icon in ico format + QByteArray res; + QBuffer buffer; + buffer.open(QBuffer::WriteOnly); + QImageWriter writer; + writer.setDevice(&buffer); + writer.setFormat("ICO"); + if (writer.canWrite()) { + const int size = 24; + QPixmap px = qApp->windowIcon().pixmap(size, size); // NOLINT + writer.write(px.toImage()); + buffer.close(); + res = buffer.data(); } else { - // get the global interpreter lock otherwise the app may crash with the error - // 'PyThreadState_Get: no current thread' (see pystate.c) - Base::PyGILStateLocker lock; - QString name = fn.left(fn.length()-5); - PyObject* main = PyImport_AddModule("__main__"); - PyObject* dict = PyModule_GetDict(main); - dict = PyDict_Copy(dict); - QByteArray cmd = - "import pydoc\n" - "object, name = pydoc.resolve(\""; - cmd += name.toUtf8(); - cmd += "\")\npage = pydoc.html.page(pydoc.describe(object), pydoc.html.document(object, name))\n"; - PyObject* result = PyRun_String(cmd.constData(), Py_file_input, dict, dict); - if (result) { - Py_DECREF(result); - result = PyDict_GetItemString(dict, "page"); - const char* page = PyUnicode_AsUTF8(result); - res.append("HTTP/1.0 200 OK\n"); - res.append("Content-type: text/html\n"); - res.append(page); + // fallback + res.reserve(navicon_data_len); + for (int i=0; i<(int)navicon_data_len;i++) { + res[i] = navicon_data[i]; } - else { - // get information about the error - Base::PyException e; - //Base::Console().Error("loadResource: %s\n", e.what()); - // load the error page - //res = fileNotFound(); - res = loadFailed(QString::fromUtf8(e.what())); - } - - Py_DECREF(dict); } return res; } +QByteArray PythonOnlineHelp::invoke(const std::function& func) const +{ + // get the global interpreter lock otherwise the app may crash with the error + // 'PyThreadState_Get: no current thread' (see pystate.c) + Base::PyGILStateLocker lock; + try { + return tryInvoke(func); + } + catch (const Py::Exception&) { + // load the error page + Base::PyException e; + return loadFailed(QString::fromUtf8(e.what())); + } +} + +QByteArray PythonOnlineHelp::tryInvoke(const std::function& func) const +{ + PyObject* module = PyImport_ImportModule("freecad.freecad_doc"); + if (!module) { + throw Py::Exception(); + } + + Py::Module mod(module, true); + std::string contents = func(mod); + QByteArray res; + res.append("HTTP/1.0 200 OK\n"); + res.append("Content-type: text/html\n"); + res.append(contents.c_str()); + return res; +} + +QByteArray PythonOnlineHelp::loadIndexPage() const +{ + return invoke([](Py::Module& mod) { + Py::String output(mod.callMemberFunction("getIndex")); + return static_cast(output); + }); +} + +QByteArray PythonOnlineHelp::loadHelpPage(const QString& filename) const +{ + return invoke([filename](Py::Module& mod) { + QString fn = filename.mid(1); + QString name = fn.left(fn.length() - 5); + + Py::Tuple args(1); + args.setItem(0, Py::String(name.toStdString())); + Py::String output(mod.callMemberFunction("getPage", args)); + return static_cast(output); + }); +} + QByteArray PythonOnlineHelp::fileNotFound() const { + const int pageNotFound = 404; QString contentType = QString::fromLatin1( "text/html\r\n" "\r\n" @@ -234,7 +189,7 @@ QByteArray PythonOnlineHelp::fileNotFound() const QString header = QString::fromLatin1("content-type: %1\r\n").arg(contentType); QString http(QLatin1String("HTTP/1.1 %1 %2\r\n%3\r\n")); - QString httpResponseHeader = http.arg(404).arg(QString::fromLatin1("File not found"), header); + QString httpResponseHeader = http.arg(pageNotFound).arg(QString::fromLatin1("File not found"), header); QByteArray res = httpResponseHeader.toLatin1(); return res; @@ -242,6 +197,7 @@ QByteArray PythonOnlineHelp::fileNotFound() const QByteArray PythonOnlineHelp::loadFailed(const QString& error) const { + const int pageNotFound = 404; QString contentType = QString::fromLatin1( "text/html\r\n" "\r\n" @@ -263,7 +219,7 @@ QByteArray PythonOnlineHelp::loadFailed(const QString& error) const QString header = QString::fromLatin1("content-type: %1\r\n").arg(contentType); QString http(QLatin1String("HTTP/1.1 %1 %2\r\n%3\r\n")); - QString httpResponseHeader = http.arg(404).arg(QString::fromLatin1("File not found"), header); + QString httpResponseHeader = http.arg(pageNotFound).arg(QString::fromLatin1("File not found"), header); QByteArray res = httpResponseHeader.toLatin1(); return res; @@ -276,8 +232,9 @@ HttpServer::HttpServer(QObject* parent) void HttpServer::incomingConnection(qintptr socket) { - if (disabled) + if (disabled) { return; + } // When a new client connects the server constructs a QTcpSocket and all // communication with the client is done over this QTcpSocket. QTcpSocket @@ -301,14 +258,16 @@ void HttpServer::resume() void HttpServer::readClient() { - if (disabled) + if (disabled) { return; + } // This slot is called when the client sent data to the server. The // server looks if it was a GET request and sends back the // corresponding HTML document from the ZIP file. - auto socket = static_cast(sender()); - if (socket->canReadLine()) { + auto socket = qobject_cast(sender()); + if (socket && socket->canReadLine()) { + // NOLINTBEGIN QString httpRequestHeader = QString::fromLatin1(socket->readLine()); QStringList lst = httpRequestHeader.simplified().split(QLatin1String(" ")); QString method; @@ -327,6 +286,7 @@ void HttpServer::readClient() } } } + // NOLINTEND if (method == QLatin1String("GET")) { socket->write(help.loadResource(path)); @@ -341,8 +301,9 @@ void HttpServer::readClient() void HttpServer::discardClient() { - auto socket = static_cast(sender()); - socket->deleteLater(); + if (auto socket = qobject_cast(sender())) { + socket->deleteLater(); + } } // -------------------------------------------------------------------- @@ -372,49 +333,17 @@ void StdCmdPythonHelp::activated(int iMsg) { Q_UNUSED(iMsg); // try to open a connection over this port - qint16 port = 7465; - if (!this->server) + const qint16 port = 7465; + if (!this->server) { this->server = new HttpServer(); + } // if server is not yet running try to open one if (this->server->isListening() || this->server->listen(QHostAddress(QHostAddress::LocalHost), port)) { - // okay the server is running, now we try to open the system internet browser - bool failed = true; - - // The webbrowser Python module allows to start the system browser in an - // OS-independent way - Base::PyGILStateLocker lock; - PyObject* module = PyImport_ImportModule("webbrowser"); - if (module) { - // get the methods dictionary and search for the 'open' method - PyObject* dict = PyModule_GetDict(module); - PyObject* func = PyDict_GetItemString(dict, "open"); - if (func) { - char szBuf[201]; - snprintf(szBuf, 200, "http://localhost:%d", port); - PyObject* args = Py_BuildValue("(s)", szBuf); -#if PY_VERSION_HEX < 0x03090000 - PyObject* result = PyEval_CallObject(func,args); -#else - PyObject* result = PyObject_CallObject(func,args); -#endif - if (result) - failed = false; - - // decrement the args and module reference - Py_XDECREF(result); - Py_DECREF(args); - Py_DECREF(module); - } - } - - // print error message on failure - if (failed) { - QMessageBox::critical(Gui::getMainWindow(), QObject::tr("No Browser"), - QObject::tr("Unable to open your browser.\n\n" - "Please open a browser window and type in: http://localhost:%1.").arg(port)); - } + std::string url = "http://localhost:"; + url += std::to_string(port); + OpenURLInBrowser(url.c_str()); } else { QMessageBox::critical(Gui::getMainWindow(), QObject::tr("No Server"), @@ -425,38 +354,23 @@ void StdCmdPythonHelp::activated(int iMsg) bool Gui::OpenURLInBrowser(const char * URL) { // The webbrowser Python module allows to start the system browser in an OS-independent way - bool failed = true; Base::PyGILStateLocker lock; - PyObject* module = PyImport_ImportModule("webbrowser"); - if (module) { - // get the methods dictionary and search for the 'open' method - PyObject* dict = PyModule_GetDict(module); - PyObject* func = PyDict_GetItemString(dict, "open"); - if (func) { - PyObject* args = Py_BuildValue("(s)", URL); -#if PY_VERSION_HEX < 0x03090000 - PyObject* result = PyEval_CallObject(func,args); -#else - PyObject* result = PyObject_CallObject(func,args); -#endif - if (result) - failed = false; - - // decrement the args and module reference - Py_XDECREF(result); - Py_DECREF(args); - Py_DECREF(module); + try { + PyObject* module = PyImport_ImportModule("webbrowser"); + if (module) { + Py::Module mod(module, true); + mod.callMemberFunction("open", Py::TupleN(Py::String(URL))); + return true; } - } - // print error message on failure - if (failed) { + throw Py::Exception(); + } + catch (Py::Exception& e) { + e.clear(); QMessageBox::critical(Gui::getMainWindow(), QObject::tr("No Browser"), QObject::tr("Unable to open your system browser.")); return false; } - - return true; } diff --git a/src/Gui/OnlineDocumentation.h b/src/Gui/OnlineDocumentation.h index d55b386494..32a43144fe 100644 --- a/src/Gui/OnlineDocumentation.h +++ b/src/Gui/OnlineDocumentation.h @@ -24,6 +24,8 @@ #ifndef GUI_ONLINEDOCUMENTATION_H #define GUI_ONLINEDOCUMENTATION_H +#include +#include #include #include #include "Command.h" @@ -48,8 +50,15 @@ public: ~PythonOnlineHelp() override; QByteArray loadResource(const QString& filename) const; + +private: QByteArray fileNotFound() const; QByteArray loadFailed(const QString& error) const; + QByteArray loadFavicon() const; + QByteArray loadIndexPage() const; + QByteArray loadHelpPage(const QString& filename) const; + QByteArray invoke(const std::function& func) const; + QByteArray tryInvoke(const std::function& func) const; }; /**