diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt
index bd08153797..2d42991bda 100644
--- a/src/Gui/CMakeLists.txt
+++ b/src/Gui/CMakeLists.txt
@@ -666,6 +666,7 @@ SET(Editor_CPP_SRCS
PythonConsole.cpp
PythonConsolePy.cpp
PythonDebugger.cpp
+ PythonTracing.cpp
PythonEditor.cpp
SyntaxHighlighter.cpp
TextEdit.cpp
@@ -677,6 +678,7 @@ SET(Editor_HPP_SRCS
PythonConsole.h
PythonConsolePy.h
PythonDebugger.h
+ PythonTracing.h
PythonEditor.h
SyntaxHighlighter.h
TextEdit.h
diff --git a/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp b/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp
index 6e59fad4ca..3619463805 100644
--- a/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp
+++ b/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp
@@ -44,6 +44,7 @@ void DlgSettingsPythonConsole::saveSettings()
ui->PythonWordWrap->onSave();
ui->PythonBlockCursor->onSave();
ui->PythonSaveHistory->onSave();
+ ui->ProfilerInterval->onSave();
}
void DlgSettingsPythonConsole::loadSettings()
@@ -51,6 +52,7 @@ void DlgSettingsPythonConsole::loadSettings()
ui->PythonWordWrap->onRestore();
ui->PythonBlockCursor->onRestore();
ui->PythonSaveHistory->onRestore();
+ ui->ProfilerInterval->onRestore();
}
void DlgSettingsPythonConsole::changeEvent(QEvent *e)
diff --git a/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui b/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui
index e72f80f3cf..b7f2ac20b6 100644
--- a/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui
+++ b/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui
@@ -78,6 +78,35 @@ horizontal space in Python console
+ -
+
+
+ Python profiler interval (milliseconds):
+
+
+
+ -
+
+
+ The interval at which the profiler runs when there's Python code running (to keep the GUI responding). Set to 0 to disable.
+
+
+ 0
+
+
+ 5000
+
+
+ 200
+
+
+ ProfilerInterval
+
+
+ PythonConsole
+
+
+
@@ -102,6 +131,11 @@ horizontal space in Python console
QCheckBox
+
+ Gui::PrefSpinBox
+ QSpinBox
+
+
diff --git a/src/Gui/PythonConsole.cpp b/src/Gui/PythonConsole.cpp
index f48ff4a530..718e79cf73 100644
--- a/src/Gui/PythonConsole.cpp
+++ b/src/Gui/PythonConsole.cpp
@@ -32,6 +32,7 @@
# include
# include
# include
+# include
# include
#endif
@@ -40,6 +41,7 @@
#include "PythonConsole.h"
#include "PythonConsolePy.h"
+#include "PythonTracing.h"
#include "Application.h"
#include "CallTips.h"
#include "FileDialog.h"
@@ -116,12 +118,15 @@ struct PythonConsoleP
colormap[QLatin1String("Python error")] = Qt::red;
}
};
+
struct InteractiveInterpreterP
{
- PyObject* interpreter;
- PyObject* sysmodule;
+ PyObject* interpreter{nullptr};
+ PyObject* sysmodule{nullptr};
QStringList buffer;
+ PythonTracing trace;
};
+
} // namespace Gui
InteractiveInterpreter::InteractiveInterpreter()
@@ -295,6 +300,16 @@ bool InteractiveInterpreter::runSource(const char* source) const
return false;
}
+bool InteractiveInterpreter::isOccupied() const
+{
+ return d->trace.isActive();
+}
+
+bool InteractiveInterpreter::interrupt() const
+{
+ return d->trace.interrupt();
+}
+
/* Execute a code object.
*
* When an exception occurs, a traceback is displayed.
@@ -302,6 +317,13 @@ bool InteractiveInterpreter::runSource(const char* source) const
*/
void InteractiveInterpreter::runCode(PyCodeObject* code) const
{
+ if (isOccupied()) {
+ return;
+ }
+
+ d->trace.fetchFromSettings();
+ PythonTracingLocker tracelock(d->trace);
+
Base::PyGILStateLocker lock;
PyObject *module, *dict, *presult; /* "exec code in d, d" */
module = PyImport_AddModule("__main__"); /* get module, init python */
@@ -478,6 +500,10 @@ PythonConsole::PythonConsole(QWidget *parent)
d->output = d->info;
printPrompt(PythonConsole::Complete);
loadHistory();
+
+ flusher = new QTimer(this);
+ connect(flusher, &QTimer::timeout, this, &PythonConsole::flushOutput);
+ flusher->start(100);
}
/** Destroys the object and frees any allocated resources */
@@ -558,6 +584,12 @@ void PythonConsole::keyPressEvent(QKeyEvent * e)
QTextCursor cursor = this->textCursor();
QTextCursor inputLineBegin = this->inputBegin();
+ if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) {
+ if (d->interpreter->interrupt()) {
+ return;
+ }
+ }
+
if (!cursorBeyond( cursor, inputLineBegin ))
{
/**
@@ -733,6 +765,15 @@ void PythonConsole::onFlush()
printPrompt(PythonConsole::Flush);
}
+void PythonConsole::flushOutput()
+{
+ if (d->interpreter->isOccupied()) {
+ if (d->output.length() > 0 || d->error.length() > 0) {
+ printPrompt(PythonConsole::Complete);
+ }
+ }
+}
+
/** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for
* incomplete commands to the console window.
*/
@@ -823,6 +864,11 @@ void PythonConsole::runSource(const QString& line)
return;
}
+ if (d->interpreter->isOccupied()) {
+ insertPythonError(QString::fromLatin1("Previous command still running!"));
+ return;
+ }
+
bool incomplete = false;
Base::PyGILStateLocker lock;
PyObject* default_stdout = PySys_GetObject("stdout");
diff --git a/src/Gui/PythonConsole.h b/src/Gui/PythonConsole.h
index 1db269840e..1375334cd5 100644
--- a/src/Gui/PythonConsole.h
+++ b/src/Gui/PythonConsole.h
@@ -24,6 +24,7 @@
#define GUI_PYTHONCONSOLE_H
#include
+#include
#include "PythonEditor.h"
@@ -42,6 +43,9 @@ public:
InteractiveInterpreter();
~InteractiveInterpreter();
+ bool isOccupied() const;
+ bool interrupt() const;
+
bool push(const char*);
int compileCommand(const char*) const;
bool hasPendingInput( ) const;
@@ -151,6 +155,7 @@ private:
void appendOutput(const QString&, int);
void loadHistory() const;
void saveHistory() const;
+ void flushOutput();
Q_SIGNALS:
void pendingSource( );
@@ -158,13 +163,13 @@ Q_SIGNALS:
private:
struct PythonConsoleP* d;
+ PythonConsoleHighlighter* pythonSyntax{nullptr};
+ QString *_sourceDrain{nullptr};
+ QString _historyFile;
+ QTimer *flusher{nullptr};
+
friend class PythonStdout;
friend class PythonStderr;
-
-private:
- PythonConsoleHighlighter* pythonSyntax;
- QString *_sourceDrain;
- QString _historyFile;
};
/**
diff --git a/src/Gui/PythonTracing.cpp b/src/Gui/PythonTracing.cpp
new file mode 100644
index 0000000000..d128f1d308
--- /dev/null
+++ b/src/Gui/PythonTracing.cpp
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+/***************************************************************************
+ * Copyright (c) 2023 Werner Mayer *
+ * *
+ * This file is part of FreeCAD. *
+ * *
+ * FreeCAD is free software: you can redistribute it and/or modify it *
+ * under the terms of the GNU Lesser General Public License as *
+ * published by the Free Software Foundation, either version 2.1 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * FreeCAD is distributed in the hope that it will be useful, but *
+ * WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with FreeCAD. If not, see *
+ * . *
+ * *
+ **************************************************************************/
+
+#include "PreCompiled.h"
+#ifndef _PreComp_
+#include
+#include
+#endif
+
+#include "PythonTracing.h"
+#include
+
+using namespace Gui;
+
+struct PythonTracing::Private
+{
+ bool active{false};
+ int timeout{200}; //NOLINT
+
+ // NOLINTBEGIN
+ static int profilerInterval;
+ static bool profilerDisabled;
+ // NOLINTEND
+};
+
+// NOLINTBEGIN
+int PythonTracing::Private::profilerInterval = 200;
+bool PythonTracing::Private::profilerDisabled = false;
+// NOLINTEND
+
+PythonTracing::PythonTracing()
+ : d{std::make_unique()}
+{
+}
+
+PythonTracing::~PythonTracing() = default;
+
+bool PythonTracing::isActive() const
+{
+ return d->active;
+}
+
+void PythonTracing::activate()
+{
+ d->active = true;
+ setPythonTraceEnabled(true);
+}
+
+void PythonTracing::deactivate()
+{
+ d->active = false;
+ setPythonTraceEnabled(false);
+}
+
+void PythonTracing::fetchFromSettings()
+{
+ const long defaultTimeout = 200;
+
+ auto parameterGroup = App::GetApplication().GetParameterGroupByPath(
+ "User parameter:BaseApp/Preferences/PythonConsole");
+ int interval = static_cast(parameterGroup->GetInt("ProfilerInterval", defaultTimeout));
+ setTimeout(interval);
+}
+
+bool PythonTracing::interrupt() const
+{
+ if (isActive()) {
+ PyErr_SetInterrupt();
+ return true;
+ }
+
+ return false;
+}
+
+void PythonTracing::setTimeout(int ms)
+{
+ d->timeout = ms;
+}
+
+int PythonTracing::timeout() const
+{
+ return d->timeout;
+}
+
+void PythonTracing::setPythonTraceEnabled(bool enabled) const
+{
+ Py_tracefunc trace = nullptr;
+ if (enabled && timeout() > 0) {
+ Private::profilerInterval = timeout();
+ trace = &tracer_callback;
+ }
+ else {
+ Private::profilerDisabled = true;
+ }
+
+ PyEval_SetTrace(trace, nullptr);
+}
+
+/*
+ * This callback ensures that Qt runs its event loop (i.e. updates the GUI, processes keyboard and
+ * mouse events, etc.) at least every 200 ms, even when there is long-running Python code on the
+ * main thread. It is registered as the global trace function of the Python environment.
+ */
+int PythonTracing::tracer_callback(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg)
+{
+ Q_UNUSED(obj)
+ Q_UNUSED(frame)
+ Q_UNUSED(what)
+ Q_UNUSED(arg)
+
+ static QTime lastCalledTime = QTime::currentTime();
+ QTime currTime = QTime::currentTime();
+
+ // if previous code object was executed
+ if (Private::profilerDisabled) {
+ Private::profilerDisabled = false;
+ lastCalledTime = currTime;
+ }
+
+ int ms = lastCalledTime.msecsTo(currTime);
+
+ if (ms >= Private::profilerInterval) {
+ lastCalledTime = currTime;
+ QGuiApplication::processEvents();
+ }
+
+ return 0;
+}
+
+// ------------------------------------------------------------------------------------------------
+
+PythonTracingLocker::PythonTracingLocker(PythonTracing& trace)
+ : trace{trace}
+{
+ trace.activate();
+}
+
+PythonTracingLocker::~PythonTracingLocker()
+{
+ trace.deactivate();
+}
diff --git a/src/Gui/PythonTracing.h b/src/Gui/PythonTracing.h
new file mode 100644
index 0000000000..8f09cef301
--- /dev/null
+++ b/src/Gui/PythonTracing.h
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+/***************************************************************************
+ * Copyright (c) 2023 Werner Mayer *
+ * *
+ * This file is part of FreeCAD. *
+ * *
+ * FreeCAD is free software: you can redistribute it and/or modify it *
+ * under the terms of the GNU Lesser General Public License as *
+ * published by the Free Software Foundation, either version 2.1 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * FreeCAD is distributed in the hope that it will be useful, but *
+ * WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with FreeCAD. If not, see *
+ * . *
+ * *
+ **************************************************************************/
+
+#ifndef GUI_PYTHONTRACING_H
+#define GUI_PYTHONTRACING_H
+
+#include
+#include
+#include
+#include
+
+
+namespace Gui {
+
+class GuiExport PythonTracing
+{
+public:
+ PythonTracing();
+ ~PythonTracing();
+
+ PythonTracing(const PythonTracing&) = delete;
+ PythonTracing(PythonTracing&&) = delete;
+ PythonTracing& operator = (const PythonTracing&) = delete;
+ PythonTracing& operator = (PythonTracing&&) = delete;
+
+ /*!
+ * \brief isActive
+ * Returns true if the tracing is active, false otherwise.
+ * \return
+ */
+ bool isActive() const;
+ /*!
+ * \brief activate
+ * Activates the Python tracing.
+ */
+ void activate();
+ /*!
+ * \brief deactivate
+ * Deactivates the Python tracing.
+ */
+ void deactivate();
+ /*!
+ * \brief fetchFromSettings
+ * Fetch parameters from user settings.
+ */
+ void fetchFromSettings();
+ /*!
+ * \brief interrupt
+ * If the tracing is enabled it interrupts the Python interpreter.
+ * True is returned if the tracing is active, false otherwise.
+ */
+ bool interrupt() const;
+ /*!
+ * \brief setTimeout
+ * Sets the interval after which the Qt event loop will be processed.
+ */
+ void setTimeout(int ms);
+ /*!
+ * \brief timeout
+ * \return the timeout of processing the event loop.
+ */
+ int timeout() const;
+
+private:
+ void setPythonTraceEnabled(bool enabled) const;
+ static int tracer_callback(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg);
+
+private:
+ struct Private;
+ std::unique_ptr d;
+};
+
+class GuiExport PythonTracingLocker
+{
+public:
+ /*!
+ * \brief PythonTracingLocker
+ * Activates the passed tracing object.
+ * \param trace
+ */
+ explicit PythonTracingLocker(PythonTracing& trace);
+ /*!
+ * Deactivates the tracing object.
+ */
+ ~PythonTracingLocker();
+
+ PythonTracingLocker(const PythonTracingLocker&) = delete;
+ PythonTracingLocker(PythonTracingLocker&&) = delete;
+ PythonTracingLocker& operator = (const PythonTracingLocker&) = delete;
+ PythonTracingLocker& operator = (PythonTracingLocker&&) = delete;
+
+private:
+ PythonTracing& trace;
+};
+
+} // namespace Gui
+
+#endif // GUI_PYTHONTRACING_H