Commit 026e268a authored by Jouni Pentikäinen's avatar Jouni Pentikäinen

Refactor PyKrita plugin

The plugin's code was refactored closer to Krita's usual coding style.

Also fixes saving/loading Python plugin enable flags.
parent 767dcf15
......@@ -5,10 +5,12 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../plugin
)
set(kritarunner_SRCS main.cpp
../plugin/engine.cpp
../plugin/plugin.cpp
../plugin/pyqtpluginsettings.cpp
../plugin/utilities.cpp
../plugin/PykritaModule.cpp
../plugin/PythonPluginManager.cpp
../plugin/PythonPluginsModel.cpp
)
add_executable(kritarunner ${kritarunner_SRCS})
......
......@@ -26,11 +26,9 @@
#include <KoGlobal.h>
#include <resources/KoHashGeneratorProvider.h>
#include "kis_md5_generator.h"
#include "PythonPluginManager.h"
#include <opengl/kis_opengl.h>
#include <engine.h>
#include <utilities.h>
extern "C" int main(int argc, char **argv)
{
// The global initialization of the random generator
......@@ -80,12 +78,26 @@ extern "C" int main(int argc, char **argv)
qDebug() << "\tPython path:" << pythonPath;
qDebug() << "Creating engine";
PyKrita::Engine engine;
QString r = engine.tryInitializeGetFailureReason();
if (!r.isEmpty()) {
qDebug("Could not initialize the Python engine");
return 1;
// TODO: refactor to share common parts with plugin.cpp
PyKrita::InitResult initResult = PyKrita::initialize();
switch (initResult) {
case PyKrita::INIT_OK:
break;
case PyKrita::INIT_CANNOT_LOAD_PYTHON_LIBRARY:
qWarning() << i18n("Cannot load Python library");
return 1;
case PyKrita::INIT_CANNOT_SET_PYTHON_PATHS:
qWarning() << i18n("Cannot set Python paths");
return 1;
case PyKrita::INIT_CANNOT_LOAD_PYKRITA_MODULE:
qWarning() << i18n("Cannot load built-in pykrita module");
return 1;
default:
qWarning() << i18n("Unexpected error initializing python plugin.");
return 1;
}
qDebug() << "Try to import the pykrita module";
......
......@@ -7,7 +7,9 @@ set(SOURCES
plugin.cpp
pyqtpluginsettings.cpp
utilities.cpp
engine.cpp
PykritaModule.cpp
PythonPluginManager.cpp
PythonPluginsModel.cpp
)
ki18n_wrap_ui(SOURCES
......
// This file is part of PyKrita, Krita' Python scripting plugin.
//
// Copyright (C) 2006 Paul Giannaros <paul@giannaros.org>
// Copyright (C) 2012, 2013 Shaheed Haque <srhaque@theiet.org>
// Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
//
// This library 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) version 3, or any
// later version accepted by the membership of KDE e.V. (or its
// successor approved by the membership of KDE e.V.), which shall
// act as a proxy defined in Section 6 of version 3 of the license.
//
// This library 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 this library. If not, see <http://www.gnu.org/licenses/>.
//
#include "PykritaModule.h"
#include "kis_debug.h"
#define PYKRITA_INIT PyInit_pykrita
/// \note Namespace name written in uppercase intentionally!
/// It will appear in debug output from Python plugins...
namespace PYKRITA
{
PyObject* debug(PyObject* /*self*/, PyObject* args)
{
const char* text;
if (PyArg_ParseTuple(args, "s", &text))
dbgScript << text;
Py_INCREF(Py_None);
return Py_None;
}
} // namespace PYKRITA
namespace
{
PyMethodDef pykritaMethods[] = {
{
"qDebug"
, &PYKRITA::debug
, METH_VARARGS
, "True KDE way to show debug info"
}
, { 0, 0, 0, 0 }
};
} // anonymous namespace
//BEGIN Python module registration
PyMODINIT_FUNC PyInit_pykrita()
{
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT
, "pykrita"
, "The pykrita module"
, -1
, pykritaMethods
, 0
, 0
, 0
, 0
};
PyObject *pykritaModule = PyModule_Create(&moduledef);
PyModule_AddStringConstant(pykritaModule, "__file__", __FILE__);
return pykritaModule;
}
//END Python module registration
// krita: space-indent on; indent-width 4;
#undef PYKRITA_INIT
/*
* This file is part of PyKrita, Krita' Python scripting plugin.
*
* Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
* Copyright (C) 2014-2016 Boudewijn Rempt <boud@valdyas.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) version 3.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PYKRITA_MODULE_H__
#define __PYKRITA_MODULE_H__
#include <Python.h>
/**
* Initializer for the built-in Python module.
*/
PyMODINIT_FUNC PyInit_pykrita();
#endif
/*
* This file is part of PyKrita, Krita' Python scripting plugin.
*
* Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
* Copyright (C) 2014-2016 Boudewijn Rempt <boud@valdyas.org>
* Copyright (C) 2017 Jouni Pentikäinen (joupent@gmail.com)
​ *
​ * This library is free software; you can redistribute it and/or
​ * modify it under the terms of the GNU Library General Public
​ * License as published by the Free Software Foundation; either
​ * version 2 of the License, or (at your option) any later version.
​ *
​ * This library 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
​ * Library General Public License for more details.
​ *
​ * You should have received a copy of the GNU Library General Public License
​ * along with this library; see the file COPYING.LIB. If not, write to
​ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
​ * Boston, MA 02110-1301, USA.
​ */
#ifndef PYTHONMODULEMANAGER_H
#define PYTHONMODULEMANAGER_H
#include <QObject>
#include "version_checker.h"
#include "PythonPluginsModel.h"
class PythonPluginsModel;
/**
* Represents a Python described in the plugin's .desktop file.
*/
class PythonPlugin
{
public:
/**
* Transforms the Python module name into a file path part
*/
QString moduleFilePathPart() const;
bool isValid() const;
inline const QString& errorReason() const
{
return m_errorReason;
}
inline bool isEnabled() const
{
return m_enabled;
}
inline bool isBroken() const
{
return m_broken;
}
inline bool isUnstable() const
{
return m_unstable;
}
QString name() const
{
return m_name;
}
QString moduleName() const
{
return m_moduleName;
}
QVariant property(const QString &name) const
{
return m_properties.value(name, "");
}
QString comment() const
{
return m_comment;
}
private:
friend class PythonPluginManager;
PythonPlugin() {
m_properties["X-Python-Dependencies"] = QStringList();
m_properties["X-Python-2-Dependencies"] = QStringList();
}
QString m_errorReason;
bool m_enabled{false};
bool m_broken{false};
bool m_unstable{false};
bool m_loaded{false};
QString m_name;
QString m_moduleName;
QString m_comment;
QMap<QString, QVariant> m_properties;
};
/**
* The Python plugin manager handles discovery, loading and unloading of Python plugins.
* To get a reference to the manager, use PyKrita::pluginManager().
*/
class PythonPluginManager : public QObject
{
Q_OBJECT
public:
PythonPluginManager();
~PythonPluginManager() override;
const QList<PythonPlugin>& plugins() const;
PythonPlugin *plugin(int index);
void scanPlugins();
void tryLoadEnabledPlugins();
void setPluginEnabled(PythonPlugin &plugin, bool enabled);
PythonPluginsModel *model();
public Q_SLOTS:
void unloadAllModules();
private:
void loadModule(PythonPlugin &plugin);
void unloadModule(PythonPlugin &plugin);
static bool verifyModuleExists(PythonPlugin &);
static void verifyDependenciesSetStatus(PythonPlugin&);
static QPair<QString, PyKrita::version_checker> parseDependency(const QString&);
QList<PythonPlugin> m_plugins;
PythonPluginsModel m_model;
};
#endif //PYTHONMODULEMANAGER_H
/*
* This file is part of PyKrita, Krita' Python scripting plugin.
*
* Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
* Copyright (C) 2014-2016 Boudewijn Rempt <boud@valdyas.org>
* Copyright (C) 2017 Jouni Pentikäinen (joupent@gmail.com)
​ *
​ * This library is free software; you can redistribute it and/or
​ * modify it under the terms of the GNU Library General Public
​ * License as published by the Free Software Foundation; either
​ * version 2 of the License, or (at your option) any later version.
​ *
​ * This library 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
​ * Library General Public License for more details.
​ *
​ * You should have received a copy of the GNU Library General Public License
​ * along with this library; see the file COPYING.LIB. If not, write to
​ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
​ * Boston, MA 02110-1301, USA.
​ */
#include "PythonPluginsModel.h"
#include <kcolorscheme.h>
#include <KI18n/KLocalizedString>
#include "PythonPluginManager.h"
PythonPluginsModel::PythonPluginsModel(QObject *parent, PythonPluginManager *pluginManager)
: QAbstractTableModel(parent)
, m_pluginManager(pluginManager)
{
}
int PythonPluginsModel::columnCount(const QModelIndex&) const
{
return COLUMN_COUNT;
}
int PythonPluginsModel::rowCount(const QModelIndex&) const
{
return m_pluginManager->plugins().size();
}
QModelIndex PythonPluginsModel::index(const int row, const int column, const QModelIndex& parent) const
{
if (!parent.isValid() && column < COLUMN_COUNT) {
auto *plugin = m_pluginManager->plugin(row);
if (plugin) {
return createIndex(row, column, plugin);
}
}
return QModelIndex();
}
QVariant PythonPluginsModel::headerData(const int section, const Qt::Orientation orientation, const int role) const
{
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
switch (section) {
case COl_NAME:
return i18nc("@title:column", "Name");
case COL_COMMENT:
return i18nc("@title:column", "Comment");
default:
break;
}
}
return QVariant();
}
QVariant PythonPluginsModel::data(const QModelIndex& index, const int role) const
{
if (index.isValid()) {
PythonPlugin *plugin = static_cast<PythonPlugin*>(index.internalPointer());
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(plugin, QVariant());
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
case COl_NAME:
return plugin->name();
case COL_COMMENT:
return plugin->comment();
default:
break;
}
break;
case Qt::CheckStateRole:
if (index.column() == COl_NAME) {
const bool checked = plugin->isEnabled();
return checked ? Qt::Checked : Qt::Unchecked;
}
break;
case Qt::ToolTipRole:
{
auto error = plugin->errorReason();
if (!error.isEmpty()) {
return error;
}
}
break;
case Qt::ForegroundRole:
if (plugin->isUnstable()) {
KColorScheme scheme(QPalette::Inactive, KColorScheme::View);
return scheme.foreground(KColorScheme::NegativeText).color();
}
break;
default:
break;
}
}
return QVariant();
}
Qt::ItemFlags PythonPluginsModel::flags(const QModelIndex& index) const
{
PythonPlugin *plugin = static_cast<PythonPlugin*>(index.internalPointer());
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(plugin, Qt::ItemIsSelectable);
int result = Qt::ItemIsSelectable;
if (index.column() == COl_NAME) {
result |= Qt::ItemIsUserCheckable;
}
// Disable UI for broken modules
if (!plugin->isBroken()) {
result |= Qt::ItemIsEnabled;
}
return static_cast<Qt::ItemFlag>(result);
}
bool PythonPluginsModel::setData(const QModelIndex& index, const QVariant& value, const int role)
{
PythonPlugin *plugin = static_cast<PythonPlugin*>(index.internalPointer());
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(plugin, false);
if (role == Qt::CheckStateRole) {
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!plugin->isBroken(), false);
const bool enabled = value.toBool();
m_pluginManager->setPluginEnabled(*plugin, enabled);
}
return true;
}
/*
* This file is part of PyKrita, Krita' Python scripting plugin.
*
* Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
* Copyright (C) 2014-2016 Boudewijn Rempt <boud@valdyas.org>
* Copyright (C) 2017 Jouni Pentikäinen (joupent@gmail.com)
​ *
​ * This library is free software; you can redistribute it and/or
​ * modify it under the terms of the GNU Library General Public
​ * License as published by the Free Software Foundation; either
​ * version 2 of the License, or (at your option) any later version.
​ *
​ * This library 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
​ * Library General Public License for more details.
​ *
​ * You should have received a copy of the GNU Library General Public License
​ * along with this library; see the file COPYING.LIB. If not, write to
​ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
​ * Boston, MA 02110-1301, USA.
​ */
#ifndef KRITA_PYTHONPLUGINSMODEL_H
#define KRITA_PYTHONPLUGINSMODEL_H
#include <QtCore/QAbstractTableModel>
class PythonPluginManager;
class PythonPluginsModel : public QAbstractTableModel
{
public:
PythonPluginsModel(QObject *parent, PythonPluginManager *pluginManager);
private:
enum Column {COl_NAME, COL_COMMENT, COLUMN_COUNT};
int columnCount(const QModelIndex&) const override;
int rowCount(const QModelIndex&) const override;
QModelIndex index(int row, int column, const QModelIndex& parent) const override;
QVariant headerData(int, Qt::Orientation, int) const override;
QVariant data(const QModelIndex&, int) const override;
Qt::ItemFlags flags(const QModelIndex&) const override;
bool setData(const QModelIndex&, const QVariant&, int) override;
private:
PythonPluginManager *m_pluginManager;
};
#endif //KRITA_PYTHONPLUGINSMODEL_H
......@@ -19,3 +19,6 @@
#define PYKRITA_PYTHON_LIBRARY "${PYTHON_LIBRARY}"
#define PYKRITA_PYTHON_SITE_PACKAGES_INSTALL_DIR "${PYTHON_SITE_PACKAGES_INSTALL_DIR}"
/// Name of the file where per-plugin configuration is stored
#define CONFIG_FILE "kritapykritarc"
/*
* This file is part of PyKrita, Krita' Python scripting plugin.
*
* Copyright (C) 2013 Alex Turbov <i.zaufi@gmail.com>
* Copyright (C) 2014-2016 Boudewijn Rempt <boud@valdyas.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) version 3.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PYKRITA_ENGINE_H__
# define __PYKRITA_ENGINE_H__
#include <cmath>
#include <Python.h>
#include "version_checker.h"
#include <QAbstractItemModel>
#include <QList>
#include <QStringList>
namespace PyKrita
{
/**
* @brief The PyPlugin class describes a plugin written in Python and loaded into the system
*/
class PyPlugin {
public:
PyPlugin()
{
m_properties["X-Python-Dependencies"] = QStringList();
m_properties["X-Python-2-Dependencies"] = QStringList();
}
QString name() const
{
return m_name;
}
QString library() const
{
return m_libraryPath;
}
QVariant property(const QString &name) const
{
return m_properties.value(name, "");
}
QString comment() const
{
return m_comment;
}
QString m_name;
QString m_libraryPath;
QMap<QString, QVariant> m_properties;
QString m_comment;
};
class Python; // fwd decl
/**
* The Engine class hosts the Python interpreter, loading
* it into memory within Krita, and then with finding and
* loading all of the PyKrita plugins.
*
* \attention Qt/KDE does not use exceptions (unfortunately),
* so this class must be initialized in two steps:
* - create an instance (via constructor)
* - try to initialize the rest (via \c Engine::tryInitializeGetFailureReason())
* If latter returns a non empty (failure reason) string, the only member
* can be called is conversion to boolean! (which is implemented as safe-bool idiom [1])
* Calling others leads to UB!
*
* \sa [1] http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool
*/
class Engine : public QAbstractItemModel
{
Q_OBJECT
typedef void (Engine::*bool_type)() const;
void unspecified_true_bool_type() const {}
public:
/// \todo Turn into a class w/ accessors
class PluginState
{
public:
/// \name Immutable accessors
//@{
QString pythonModuleName() const;
const QString& errorReason() const;
bool isEnabled() const;
bool isBroken() const;
bool isUnstable() const;
//@}
private:
friend class Engine;
PluginState();
/// Transfort Python module name into a file path part
QString moduleFilePathPart() const;
PyPlugin m_pythonPlugin;
QString m_pythonModule;
QString m_errorReason;
bool m_enabled;
bool m_broken;
bool m_unstable;
bool m_isDir;
};
/// Default constructor: initialize Python interpreter
Engine();
/// Cleanup everything on unload
~Engine();