Gui: refactor PythonOnlineHelp
This commit is contained in:
@@ -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
|
||||
|
||||
63
src/Ext/freecad/freecad_doc.py
Normal file
63
src/Ext/freecad/freecad_doc.py
Normal 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 <ping@lfw.org></font>'''
|
||||
|
||||
htmldocument = pydoc.html.page(title, contents)
|
||||
return htmldocument
|
||||
@@ -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 <ping@lfw.org></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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user