Gui: refactor PythonOnlineHelp

This commit is contained in:
wmayer
2024-05-23 19:54:18 +02:00
committed by wwmayer
parent db48a78d5a
commit b0acb563ea
4 changed files with 184 additions and 197 deletions

View File

@@ -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

View File

@@ -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 '<a href=\"%s.html\">%s</a>' % (name, name)
def createDocumentation():
pydoc.html = FreeCADDoc()
title = 'FreeCAD Python Modules Index'
heading = pydoc.html.heading('<big><big><strong>Python: Index of Modules</strong></big></big>','#ffffff', '#7799ee')
names = list(filter(lambda x: x != '__main__', sys.builtin_module_names))
contents = pydoc.html.multicolumn(names, bltinlink)
indices = ['<p>' + pydoc.html.bigsection('Built-in Modules', '#ffffff', '#ee77aa', contents)]
names = ['FreeCAD', 'FreeCADGui']
contents = pydoc.html.multicolumn(names, bltinlink)
indices.append('<p>' + 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) + '''<p align=right>
<font color=\"#909090\" face=\"helvetica, arial\"><strong>
pydoc</strong> by Ka-Ping Yee &lt;ping@lfw.org&gt;</font>'''
htmldocument = pydoc.html.page(title, contents)
return htmldocument

View File

@@ -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("
"'<big><big><strong>Python: Index of Modules</strong></big></big>',"
"'#ffffff', '#7799ee')\n"
"def bltinlink(name):\n"
" return '<a href=\"%s.html\">%s</a>' % (name, name)\n"
"names = list(filter(lambda x: x != '__main__',\n"
" sys.builtin_module_names))\n"
"contents = pydoc.html.multicolumn(names, bltinlink)\n"
"indices = ['<p>' + 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('<p>' + 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) + '''<p align=right>\n"
"<font color=\"#909090\" face=\"helvetica, arial\"><strong>\n"
"pydoc</strong> by Ka-Ping Yee &lt;ping@lfw.org&gt;</font>'''\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<std::string(Py::Module&)>& 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<std::string(Py::Module&)>& 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<std::string>(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<std::string>(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<QTcpSocket*>(sender());
if (socket->canReadLine()) {
auto socket = qobject_cast<QTcpSocket*>(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<QTcpSocket*>(sender());
socket->deleteLater();
if (auto socket = qobject_cast<QTcpSocket*>(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;
}

View File

@@ -24,6 +24,8 @@
#ifndef GUI_ONLINEDOCUMENTATION_H
#define GUI_ONLINEDOCUMENTATION_H
#include <functional>
#include <CXX/Objects.hxx>
#include <QObject>
#include <QTcpServer>
#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<std::string(Py::Module&)>& func) const;
QByteArray tryInvoke(const std::function<std::string(Py::Module&)>& func) const;
};
/**