20 #include <QDesktopServices>
21 #include <QMessageBox>
23 #include <pybind11/pytypes.h>
26 #define signals Q_SIGNALS
29 #include <QFileDialog>
46 bool isDefaultPythonEnv;
51 isDefaultPythonEnv =
false;
52 plgPrint() <<
"ACloudViewer was loaded from within a " << config.
type() <<
"env. "
53 <<
"Will try to use it";
59 if (isDefaultPythonEnv)
61 plgPrint() <<
"ACloudViewer was loaded from Bundled env. "
62 <<
"Will try to use it";
66 plgPrint() <<
"ACloudViewer was loaded from " << config.
type() <<
"env. "
67 <<
"Will try to use it";
71 if (!isDefaultPythonEnv)
77 <<
"Falling back to default bundled Python configuration due to previous errors";
86 catch (
const std::exception &e)
88 plgPrint() <<
"Current Python config: " << config;
89 plgError() <<
"Failed to initialize Python: " << e.what();
94 m_pluginsMenu =
new QMenu(
"Plugins");
95 m_pluginsMenu->setToolTip(
"Python Plugins");
96 m_pluginsMenu->setEnabled(
false);
102 &PythonPlugin::handlePythonExecutionStarted);
107 &PythonPlugin::handlePythonExecutionFinished);
123 return std::make_unique<QSettings>(
124 QCoreApplication::organizationName(),
125 QCoreApplication::applicationName().append(
":PythonRuntime.Settings"));
133 settings->setValue(QStringLiteral(
"RegisterListPath"), m_savedPath);
142 m_showEditor =
new QAction(
"Show Editor",
this);
143 m_showEditor->setToolTip(
"Show the code editor window");
145 connect(m_showEditor, &QAction::triggered,
this, &PythonPlugin::showEditor);
146 m_showEditor->setEnabled(isPythonProperlyInitialized);
151 m_showRepl =
new QAction(
"Show REPL",
this);
152 m_showRepl->setToolTip(
"Show the Python REPL");
154 connect(m_showRepl, &QAction::triggered,
this, &PythonPlugin::showRepl);
155 m_showRepl->setEnabled(isPythonProperlyInitialized);
160 m_showDoc =
new QAction(
"Show Documentation",
this);
161 m_showDoc->setToolTip(
"Open online documentation in your web browser");
163 connect(m_showDoc, &QAction::triggered, &PythonPlugin::showDocumentation);
164 m_showDoc->setEnabled(isPythonProperlyInitialized);
167 if (!m_showAboutDialog)
169 m_showAboutDialog =
new QAction(
"About",
this);
170 m_showAboutDialog->setToolTip(
"About this plugin");
172 connect(m_showAboutDialog, &QAction::triggered,
this, &PythonPlugin::showAboutDialog);
173 m_showAboutDialog->setEnabled(isPythonProperlyInitialized);
176 if (!m_showFileRunner)
178 m_showFileRunner =
new QAction(
"File Runner",
this);
179 m_showFileRunner->setToolTip(
"Small widget to select and run a script");
181 connect(m_showFileRunner, &QAction::triggered,
this, &PythonPlugin::showFileRunner);
182 m_showFileRunner->setEnabled(isPythonProperlyInitialized);
185 if (!m_showPackageManager)
187 m_showPackageManager =
new QAction(
"Package Manager",
this);
188 m_showPackageManager->setToolTip(
"Manage packages with pip");
190 connect(m_showPackageManager, &QAction::triggered,
this, &PythonPlugin::showPackageManager);
191 m_showPackageManager->setEnabled(isPythonProperlyInitialized);
194 if (!m_showActionLauncher)
196 m_showActionLauncher =
new QAction(
"Show Action Launcher",
this);
198 m_showActionLauncher->setToolTip(
"Launch actions of custom Python plugins");
199 connect(m_showActionLauncher,
202 &PythonPlugin::showPythonActionLauncher);
203 m_showActionLauncher->setEnabled(isPythonProperlyInitialized);
208 m_showSettings =
new QAction(
"Show Settings",
this);
210 m_showSettings->setToolTip(
"Show some settings");
211 connect(m_showSettings, &QAction::triggered,
this, &PythonPlugin::showSettings);
213 m_showSettings->setEnabled(
true);
215 if (!m_drawScriptRegister)
217 m_drawScriptRegister =
new QMenu(
"Script Register");
218 m_drawScriptRegister->setToolTip(
"Show all registered script");
219 m_drawScriptRegister->setEnabled(
true);
222 m_addScript =
new QAction(
"Add Script");
223 m_addScript->setToolTip(
"Add Script");
225 connect(m_addScript, &QAction::triggered,
this, &PythonPlugin::addScriptAction);
226 m_addScript->setEnabled(
true);
228 m_removeScript =
new QMenu(
"Remove Script");
230 m_removeScript->setToolTip(
"Remove Script");
231 m_removeScript->setEnabled(
false);
233 m_drawScriptRegister->addAction(m_addScript);
234 m_drawScriptRegister->addMenu(m_removeScript);
235 m_drawScriptRegister->addSeparator();
238 QStringList loaded_paths =
239 settings->value(QStringLiteral(
"RegisterListPath")).value<QStringList>();
241 for (QString
path : loaded_paths)
245 plgPrint() <<
"Script registered \"" <<
path <<
"\" doesn't exist.";
257 m_showPackageManager,
258 m_showActionLauncher,
260 m_drawScriptRegister->menuAction(),
261 m_pluginsMenu->menuAction(),
265 void PythonPlugin::showRepl()
269 QMessageBox::critical(
271 "Python Interpreter Not Initialized",
272 "The Python interpreter failed to initialize. Cannot open the interactive REPL "
273 "window.\nPlease check your Python environment configuration or logs.");
281 m_repl->activateWindow();
290 void PythonPlugin::showEditor()
const
296 m_editor->activateWindow();
300 void PythonPlugin::addScriptAction()
302 if (m_scriptList.empty())
303 m_removeScript->setEnabled(
true);
305 QString filePath = QFileDialog::getOpenFileName(m_drawScriptRegister,
306 QStringLiteral(
"Select Python Script"),
308 QStringLiteral(
"Python Script (*.py)"));
312 void PythonPlugin::addScript(QString
path)
314 if (m_scriptList.empty())
315 m_removeScript->setEnabled(
true);
320 if (!fi.exists() || m_savedPath.contains(
path))
323 QString fileName = fi.baseName();
325 auto *newScript =
new QAction(fileName);
326 newScript->setToolTip(fileName);
328 auto *removeNewScript =
new QAction(fileName);
329 removeNewScript->setToolTip(fileName);
332 connect(newScript, &QAction::triggered, [
this,
path]() { executeScript(
path); });
333 newScript->setEnabled(
true);
334 connect(removeNewScript,
336 [
this, fileName, removeNewScript,
path]()
338 removeScript(fileName, removeNewScript);
341 auto pos = std::find(m_savedPath.begin(), m_savedPath.end(),
path);
342 if (pos != m_savedPath.end())
344 m_savedPath.erase(pos);
347 removeNewScript->setEnabled(
true);
349 m_scriptList.insert({fileName, newScript});
350 m_drawScriptRegister->addAction(newScript);
351 m_removeScript->addAction(removeNewScript);
354 m_savedPath.push_back(
path);
357 void PythonPlugin::executeScript(QString
path)
359 const std::string path_str =
path.toStdString();
363 void PythonPlugin::removeScript(QString
name, QAction *
self)
365 QAction *script = m_scriptList[
name];
366 m_drawScriptRegister->removeAction(script);
367 m_removeScript->removeAction(
self);
368 m_scriptList.erase(
name);
371 if (m_scriptList.empty())
372 m_removeScript->setEnabled(
false);
375 void PythonPlugin::handlePluginActionClicked(
bool)
377 const auto *qAction =
static_cast<QAction *
>(sender());
380 auto action = m_pluginActions.at(qAction);
383 catch (
const std::exception &e)
385 plgError() <<
"Failed to launch plugin action: '" << e.what() <<
"'";
389 void PythonPlugin::showFileRunner()
const
391 m_fileRunner->show();
394 void PythonPlugin::showDocumentation()
396 const QUrl url(QString(
"https://tmontaigu.github.io/CloudCompare-PythonRuntime/index.html"));
397 QDesktopServices::openUrl(url);
400 void PythonPlugin::showAboutDialog()
const
406 void PythonPlugin::showPackageManager()
408 if (m_packageManager ==
nullptr)
412 m_packageManager->show();
414 m_editor->activateWindow();
417 void PythonPlugin::showPythonActionLauncher()
const
419 m_actionLauncher->show();
422 void PythonPlugin::showSettings()
const
443 cmd.
print(
"[PythonRuntime] Starting");
450 if (!Py_IsInitialized())
452 return cmd.
error(
"[PythonRuntime] Python is not properly initialized");
458 cmd.
print(QString(
"[PythonRuntime] Script %1 executed")
459 .arg(success ?
"successfully" :
"unsuccessfully"));
474 return cmd.
error(QString(
"Missing parameter: parameters filename after \"-%1\"")
475 .arg(
"PYTHON_SCRIPT"));
526 catch (
const std::exception &e)
528 CVLog::Warning(
"[PythonRuntime] Failed to load autodiscovered custom python plugins: %s",
537 catch (
const std::exception &e)
539 CVLog::Warning(
"[PythonRuntime] Failed to load custom python plugins : %s", e.what());
542 populatePluginSubMenu();
557 if (py::isinstance<py::str>(pyIcon))
559 const auto filePath = pyIcon.cast<std::string>();
560 const auto qFilePath = QString::fromStdString(filePath);
561 icon = QIcon(qFilePath);
563 else if (py::isinstance<py::bytes>(pyIcon))
565 auto bytes = pyIcon.cast<std::string>();
568 bool ok = pixmap.loadFromData(
572 plgError() <<
"Failed to load icon from bytes";
574 icon = QIcon(pixmap);
576 else if (py::isinstance<py::tuple>(pyIcon))
578 auto icon_tuple = pyIcon.cast<py::tuple>();
583 bytes = icon_tuple[0].cast<std::string>();
585 catch (
const std::exception &)
587 plgWarning() <<
"Invalid tuple member for icon, expected (bytes, str)";
593 format = icon_tuple[1].cast<std::string>();
595 catch (
const std::exception &)
597 plgWarning() <<
"Invalid tuple member for icon, expected (bytes, str)";
606 bool ok = pixmap.loadFromData(
610 plgError() <<
"Failed to load icon from bytes";
612 icon = QIcon(pixmap);
620 if (pyIcon.is_none())
628 iconReceiver->setIcon(icon);
635 void PythonPlugin::populatePluginSubMenu()
639 if (plugin.actions.size() > 1)
641 auto *menu =
new QMenu(plugin.name);
646 auto *qAction =
new QAction(action.
name);
647 qAction->setParent(menu);
648 menu->addAction(qAction);
653 qAction, &QAction::triggered,
this, &PythonPlugin::handlePluginActionClicked);
654 m_pluginActions[qAction] = &action;
656 m_pluginsMenu->addMenu(menu);
660 auto *qAction =
new QAction(plugin.actions[0].name);
661 if (!plugin.actions[0].icon.is_none())
670 connect(qAction, &QAction::triggered,
this, &PythonPlugin::handlePluginActionClicked);
671 m_pluginsMenu->addAction(qAction);
672 m_pluginActions[qAction] = &plugin.actions[0];
675 m_pluginsMenu->setEnabled(!m_pluginsMenu->isEmpty());
678 void PythonPlugin::handlePythonExecutionStarted()
680 m_pluginsMenu->setEnabled(
false);
682 void PythonPlugin::handlePythonExecutionFinished()
684 m_pluginsMenu->setEnabled(
true);
687 void PythonPlugin::finalizeInterpreter()
filament::Texture::InternalFormat format
static std::unique_ptr< QSettings > LoadSettings()
static QIcon CreateQIconFromPyObject(const py::object &pyIcon)
static bool SetIconFromPyObject(T *iconReceiver, const py::object &pyIcon)
#define PYPLUGIN_ICON_PATH
#define REMOVE_PYSCRIPT_ICON_PATH
#define ADD_PYSCRIPT_ICON_PATH
#define PACKAGE_MANAGER_ICON_PATH
#define ACTION_LAUNCHER_ICON_PATH
#define PYSCRIPT_ICON_PATH
#define PYSCRIPTS_REGISTER_ICON_PATH
#define DOCUMENTATION_ICON_PATH
#define SETTINGS_ICON_PATH
PluginLogger< CVLog::LOG_WARNING > plgWarning
void LogPythonPath()
Logs the PYTHON_PATH the log console of ACloudViewer.
PluginLogger< CVLog::LOG_ERROR > plgError
wchar_t * QStringToWcharArray(const QString &string)
Returns a newly allocated wchar_t array (null terminated) from a QString.
PluginLogger< CVLog::LOG_STANDARD > plgPrint
void LogPythonHome()
Logs the PYTHON_HOME the log console of ACloudViewer.
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
static PythonConfig fromContainingEnvironment()
bool validateAndDisplayErrors(QWidget *parent=nullptr) const
static bool IsInsideEnvironment()
void executeFunction(const pybind11::object &function)
static bool IsInitialized()
bool executeFile(const std::string &filePath)
Execution functions (and slots)
void initialize(const PythonConfig &config)
void loadPluginsFrom(const QStringList &paths)
const std::vector< Runtime::RegisteredPlugin > & plugins() const
Returns the currently loaded plugins.
void loadPluginsFromEntryPoints()
void unloadPlugins()
This MUST be called before finalizing the interpreter.
void setMainAppInterface(ecvMainAppInterface *app) override
Sets application entry point.
QList< QAction * > getActions() override
Get a list of actions for this plugin.
void stop() override
Stops the plugin.
PythonPlugin(QObject *parent=nullptr)
void registerCommands(ccCommandLineInterface *cmd) override
~PythonPlugin() noexcept override
Homemade REPL (Read Print Eval Loop)
bool isDefaultPythonEnv() const
QStringList pluginsPaths() const
PythonConfig pythonEnvConfig() const
virtual QStringList & arguments()=0
Returns the list of arguments.
virtual void print(const QString &message) const =0
virtual bool error(const QString &message) const =0
virtual bool registerCommand(Command::Shared command)=0
Registers a new command.
Standard ECV plugin interface.
virtual void setMainAppInterface(ecvMainAppInterface *app)
Sets application entry point.
ecvMainAppInterface * m_app
Main application interface.
Main application interface (for plugins)
virtual QMainWindow * getMainWindow()=0
Returns main window.
void setMainAppInterfaceInstance(ecvMainAppInterface *appInterface) noexcept(false)
void unsetMainAppInterfaceInstance() noexcept
Unsets the app interface pointer.
void setCmdLineInterfaceInstance(ccCommandLineInterface *cmdLine) noexcept
void unsetCmdLineInterfaceInstance() noexcept
Unsets the pointer to the cmdline app interface.
static const std::string path
bool parseFrom(ccCommandLineInterface &cmd)
std::vector< wchar_t * > pythonArgv
PythonInterpreter * interpreter
PythonPluginCommand(PythonInterpreter *interpreter_)
bool process(ccCommandLineInterface &cmd) override
Main process.
QString name
Name to be displayed in the UI.
pybind11::object icon
Optional path or (bytes, str) where str is the format.
Generic command interface.
QSharedPointer< Command > Shared
Shared type.
Command(const QString &name, const QString &keyword)
Default constructor.