Commit 3de294bc authored by Boudewijn Rempt's avatar Boudewijn Rempt

Create actions per-window instead of per-application

Note that this changes the libkis scripting api. The Extension
class now has two methods: setup and createActions. Old code
was like this:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from krita import *

def hello():
    QMessageBox.information(QWidget(), "Test", "Hello World")

class HelloExtension(Extension):

  def __init__(self, parent):
      super().__init__(parent)

  def setup(self):
      action = Krita.createAction("Hello")
      action.triggered.connect(hello)

Krita.instance().addExtension(HelloExtension(Krita.instance()))

New code is like this:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from krita import *

def hello():
    QMessageBox.information(QWidget(), "Test", "Hello World")

class HelloExtension(Extension):

  def __init__(self, parent):
      super().__init__(parent)

  def setup(self):
      pass

  def createActions(self, window):
      action = window.createAction("Hello")
      action.triggered.connect(hello)

Krita.instance().addExtension(HelloExtension(Krita.instance()))

This also adds a new parameter to createAction: the menu location. This
is a path separated by /, for instance tools/scripts. Note that this
path must exist, otherwise a crash will happen. The paths are defined in
krita4.xmlgui...

BUG:391705

Note: we're still leaking Action objects created in Window::createAction;
that's the next fix.

CCMAIL:kimageshop@kde.org
(cherry picked from commit e9b06616)
parent ac99805e
......@@ -307,7 +307,7 @@ xsi:schemaLocation="http://www.kde.org/standards/kxmlgui/1.0 http://www.kde.org
</Menu>
<Menu name="tools">
<text>&amp;Tools</text>
<Action name="scripts"/>
<Menu name="scripts"><text>Scripting</text></Menu>
<Menu name="Recording">
<text>Recording</text>
<Action name="Recording_Start_Recording_Macro"/>
......
......@@ -156,14 +156,3 @@ void Action::trigger()
{
d->action->trigger();
}
void Action::setMenu(const QString menu)
{
d->action->setProperty("menu", menu);
}
QString Action::menu() const
{
return d->action->property("menu").toString();
}
......@@ -136,18 +136,6 @@ public Q_SLOTS:
*/
void trigger();
/**
* @brief setMenu determines in which menu the action will be placed. The default is tools/scripts
* @param menu the menu where the action should go, / -separated to drill down the hierarchy
*/
void setMenu(const QString menu);
/**
* @return the menu in which this action is to be placed.
*/
QString menu() const;
Q_SIGNALS:
/**
......
......@@ -22,17 +22,18 @@
#include "kritalibkis_export.h"
#include <QObject>
#include <Window.h>
/**
* An Extension is the base for classes that extend Krita. An Extension
* An Extension is the base for classes that extend Krita. An Extension
* is loaded on startup, when the setup() method will be executed.
*
* The extension instance should be added to the Krita Application object
*
* The extension instance should be added to the Krita Application object
* using Krita.instance().addViewExtension or Application.addViewExtension
* or Scripter.addViewExtension.
*
*
* Example:
*
*
* @code
* import sys
* from PyQt5.QtGui import *
......@@ -45,34 +46,41 @@
*
* def hello(self):
* QMessageBox.information(QWidget(), "Test", "Hello! This is Krita " + Application.version())
*
*
* def setup(self):
* qDebug("Hello Setup")
* action = Krita.instance().createAction("hello")
*
* def createActions(self, window)
* action = window.createAction("hello")
* action.triggered.connect(self.hello)
*
* Scripter.addExtension(HelloExtension(Krita.instance()))
*
*
* @endcode
*/
class KRITALIBKIS_EXPORT Extension : public QObject
{
Q_OBJECT
public:
/**
* Create a new extension. The extension will be
* Create a new extension. The extension will be
* owned by @param parent.
*/
explicit Extension(QObject *parent = 0);
~Extension() override;
/**
* Override this function to setup your Extension. You can use it to add
* Actions to the action collection or integrate in any other way with
* the application.
* Override this function to setup your Extension. You can use it to integrate
* with the Krita application instance.
*/
virtual void setup() = 0;
virtual void createActions(Window *window) = 0;
};
#endif
......@@ -38,7 +38,6 @@
#include <KisDocument.h>
#include <kis_image.h>
#include <kis_action.h>
#include <kis_script_manager.h>
#include <KisViewManager.h>
#include <KritaVersionWrapper.h>
#include <kis_filter_registry.h>
......@@ -53,6 +52,7 @@
#include <KoResourceServerProvider.h>
#include <kis_action_registry.h>
#include <kis_icon_utils.h>
#include <KisPart.h>
#include "View.h"
#include "Document.h"
......@@ -77,6 +77,7 @@ Krita::Krita(QObject *parent)
, d(new Private)
{
qRegisterMetaType<Notifier*>();
connect(KisPart::instance(), SIGNAL(sigWindowAdded(KisMainWindow*)), SLOT(mainWindowAdded(KisMainWindow*)));
}
Krita::~Krita()
......@@ -348,25 +349,6 @@ Window* Krita::openWindow()
return new Window(mw);
}
Action *Krita::createAction(const QString &id, const QString &text, bool addToScriptMenu)
{
KisAction *action = new KisAction(text, this);
action->setObjectName(id);
KisActionRegistry *actionRegistry = KisActionRegistry::instance();
actionRegistry->propertizeAction(action->objectName(), action);
bool ok; // We will skip this check
int activationFlags = actionRegistry->getActionProperty(id, "activationFlags").toInt(&ok, 2);
int activationConditions = actionRegistry->getActionProperty(id, "activationConditions").toInt(&ok, 2);
action->setActivationFlags((KisAction::ActivationFlags) activationFlags);
action->setActivationConditions((KisAction::ActivationConditions) activationConditions);
if (addToScriptMenu) {
KisPart::instance()->addScriptAction(action);
}
return new Action(action->objectName(), action);
}
void Krita::addExtension(Extension* extension)
{
d->extensions.append(extension);
......@@ -431,3 +413,11 @@ QObject *Krita::fromVariant(const QVariant& v)
else
return 0;
}
void Krita::mainWindowAdded(KisMainWindow *kisWindow)
{
Q_FOREACH(Extension *extension, d->extensions) {
Window window(kisWindow);
extension->createActions(&window);
}
}
......@@ -273,16 +273,6 @@ add_document_to_window()
*/
Window *openWindow();
/**
* @brief createAction creates an action with the given text and passes it to Krita. Every newly created
* mainwindow will create an instance of this action. This means that actions need to be created in the
* setup phase of the plugin, not on the fly.
* @param id the unique id for this action
* @param text the user-visible text
* @return the Action you can connect a slot to.
*/
Action *createAction(const QString &name, const QString &text, bool addToScriptMenu = true);
/**
* @brief addExtension add the given plugin to Krita. There will be a single instance of each Extension in the Krita process.
* @param extension the extension to add.
......@@ -338,6 +328,10 @@ add_document_to_window()
// Internal only: for use with mikro.py
static QObject *fromVariant(const QVariant& v);
private Q_SLOTS:
void mainWindowAdded(KisMainWindow *window);
private:
struct Private;
Private *const d;
......
......@@ -102,8 +102,11 @@ class HelloExtension(Extension):
def __init__(self, parent):
super().__init__(parent)
def setup(self, viewManager):
action = viewManager.createAction("Hello")
def setup(self):
pass
def createActions(self, window):
action = window.createAction("Hello")
action.triggered.connect(hello)
Krita.instance().addExtension(HelloExtension(Krita.instance()))
......
......@@ -17,13 +17,18 @@
*/
#include "Window.h"
#include <QMenuBar>
#include <QObject>
#include <KisMainWindow.h>
#include <KisPart.h>
#include <KisDocument.h>
#include <KisViewManager.h>
#include <kis_action_manager.h>
#include <Document.h>
#include <View.h>
#include <Action.h>
struct Window::Private {
Private() {}
......@@ -114,4 +119,33 @@ void Window::close()
}
Action *Window::createAction(const QString &id, const QString &text, const QString &menuLocation)
{
KisAction *action = d->window->viewManager()->actionManager()->createAction(id);
action->setText(text);
action->setObjectName(id);
if (!menuLocation.isEmpty()) {
QAction *found = 0;
QList<QAction *> candidates = d->window->menuBar()->actions();
Q_FOREACH(const QString &name, menuLocation.split("/")) {
Q_FOREACH(QAction *candidate, candidates) {
if (candidate->objectName() == name) {
found = candidate;
candidates = candidate->menu()->actions();
break;
}
}
if (candidates.isEmpty()) {
break;
}
}
if (found && found->menu()) {
found->menu()->addAction(action);
}
}
return new Action(action->objectName(), action);
}
......@@ -19,12 +19,16 @@
#define LIBKIS_WINDOW_H
#include <QObject>
#include <QAction>
#include <QMainWindow>
#include "kritalibkis_export.h"
#include "libkis.h"
#include <KisMainWindow.h>
class Action;
/**
* Window represents one Krita mainwindow. A window can have any number
* of views open on any number of documents.
......@@ -82,6 +86,19 @@ public Q_SLOTS:
*/
void close();
/**
* @brief createAction creates an Action object and adds it to the action
* manager for this Window.
* @param id The unique id for the action. This will be used to
* propertize the action if any .action file is present
* @param text The user-visible text of the action. If empty, the text from the
* .action file is used.
* @param menu a /-separated string that describes which menu the action should
* be places in. Default is "tools/scripts"
* @return the new action.
*/
Action *createAction(const QString &id, const QString &text = QString(), const QString &menuLocation = QString("tools/scripts"));
Q_SIGNALS:
/// Emitted when the window is closed.
void windowClosed();
......
......@@ -134,7 +134,6 @@ set(kritaui_LIB_SRCS
kis_popup_palette.cpp
kis_png_converter.cpp
kis_preference_set_registry.cpp
kis_script_manager.cpp
kis_resource_server_provider.cpp
KisSelectedShapesProxy.cpp
kis_selection_decoration.cc
......
......@@ -70,7 +70,6 @@
#include "KisDocument.h"
#include "KoToolManager.h"
#include "KisViewManager.h"
#include "kis_script_manager.h"
#include "KisOpenPane.h"
#include "kis_color_manager.h"
......@@ -101,10 +100,7 @@ public:
QList<QPointer<KisView> > views;
QList<QPointer<KisMainWindow> > mainWindows;
QList<QPointer<KisDocument> > documents;
QList<KisAction*> scriptActions;
KActionCollection *actionCollection{0};
KisIdleWatcher idleWatcher;
KisAnimationCachePopulator animationCachePopulator;
};
......@@ -208,9 +204,6 @@ void KisPart::removeDocument(KisDocument *document)
KisMainWindow *KisPart::createMainWindow()
{
KisMainWindow *mw = new KisMainWindow();
Q_FOREACH(KisAction *action, d->scriptActions) {
mw->viewManager()->scriptManager()->addAction(action);
}
dbgUI <<"mainWindow" << (void*)mw << "added to view" << this;
d->mainWindows.append(mw);
emit sigWindowAdded(mw);
......@@ -352,10 +345,6 @@ KisMainWindow *KisPart::currentMainwindow() const
}
void KisPart::addScriptAction(KisAction *action)
{
d->scriptActions << action;
}
KisIdleWatcher* KisPart::idleWatcher() const
{
......
......@@ -134,17 +134,6 @@ public:
*/
KisMainWindow *currentMainwindow() const;
/**
* Add a given action to the list of dynamically defined actions. On creating
* a mainwindow, all these actions will be added to the script manager.
*/
void addScriptAction(KisAction *);
/**
* Load actions for currently active main window into KisActionRegistry.
*/
void loadActions();
/**
* @return the application-wide KisIdleWatcher.
*/
......
......@@ -127,7 +127,6 @@
#include "kis_zoom_manager.h"
#include "widgets/kis_floating_message.h"
#include "kis_signal_auto_connection.h"
#include "kis_script_manager.h"
#include "kis_icon_utils.h"
#include "kis_guides_manager.h"
#include "kis_derived_resources.h"
......@@ -192,7 +191,6 @@ public:
, actionCollection(_actionCollection)
, mirrorManager(_q)
, inputManager(_q)
, scriptManager(_q)
, actionAuthor(0)
, showPixelGrid(0)
{
......@@ -247,7 +245,6 @@ public:
KisInputManager inputManager;
KisSignalAutoConnectionsStore viewConnections;
KisScriptManager scriptManager;
KSelectAction *actionAuthor; // Select action for author profile.
KisAction *showPixelGrid;
......@@ -769,8 +766,6 @@ void KisViewManager::setupManagers()
d->canvasControlsManager.setup(actionManager());
d->mirrorManager.setup(actionCollection());
d->scriptManager.setup(actionCollection(), actionManager());
}
void KisViewManager::updateGUI()
......@@ -812,11 +807,6 @@ KisDocument *KisViewManager::document() const
return 0;
}
KisScriptManager *KisViewManager::scriptManager() const
{
return &d->scriptManager;
}
int KisViewManager::viewCount() const
{
KisMainWindow *mw = qobject_cast<KisMainWindow*>(d->mainWindow);
......
......@@ -50,7 +50,6 @@ class KisUndoAdapter;
class KisZoomManager;
class KisPaintopBox;
class KisActionManager;
class KisScriptManager;
class KisInputManager;
class KoUpdater;
class KoProgressUpdater;
......@@ -164,8 +163,6 @@ public: // Krita specific interfaces
KisDocument *document() const;
KisScriptManager *scriptManager() const;
int viewCount() const;
/**
......
/*
* Copyright (c) 2014 Boudewijn Rempt <boud@valdyas.org>
*
* This program 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 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_script_manager.h"
#include <QMenu>
#include <KisPart.h>
#include <kactionmenu.h>
#include <klocalizedstring.h>
#include <kactionmenu.h>
#include <kactioncollection.h>
#include <kis_action.h>
#include <kis_action_manager.h>
#include "KisViewManager.h"
struct KisScriptManager::Private {
Private()
: actionCollection(0)
, actionManager(0)
, viewManager(0)
, scriptMenu(0)
{
}
KActionCollection *actionCollection;
KisActionManager *actionManager;
KisViewManager *viewManager;
KActionMenu *scriptMenu;
KActionMenu *hiddenMenu;
};
KisScriptManager::KisScriptManager(KisViewManager *view)
: QObject(view)
, d(new Private())
{
d->viewManager = view;
}
KisScriptManager::~KisScriptManager()
{
delete d;
}
void KisScriptManager::setup(KActionCollection * ac, KisActionManager *actionManager)
{
d->actionCollection = ac;
d->actionManager = actionManager;
d->scriptMenu = new KActionMenu(i18n("Scripts"), this);
d->actionCollection->addAction("scripts", d->scriptMenu);
d->hiddenMenu = new KActionMenu("hidden", this);
}
void KisScriptManager::updateGUI()
{
if (!d->viewManager) return;
}
void KisScriptManager::addAction(KisAction *action)
{
d->actionManager->addAction(action->objectName(), action);
if (action->property("menu").toString() != "None") {
// XXX: find the right menu and add it there.
d->scriptMenu->addAction(action);
}
else {
d->hiddenMenu->addAction(action);
action->setShortcutContext(Qt::ApplicationShortcut);
}
}
/*
* Copyright (c) 2014 Boudewijn Rempt <boud@valdyas.org>
*
* This program 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 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef KIS_SCRIPT_MANAGER_H
#define KIS_SCRIPT_MANAGER_H
#include <QObject>
#include <kritaui_export.h>
class KisAction;
class KisActionManager;
class KisViewManager;
class KActionCollection;
/**
* @brief The KisScriptManager class is responsible for adding scripts to the menu
*/
class KRITAUI_EXPORT KisScriptManager : public QObject
{
Q_OBJECT
public:
explicit KisScriptManager(KisViewManager * view);
~KisScriptManager() override;
void setup(KActionCollection * ac, KisActionManager *actionManager);
void updateGUI();
void addAction(KisAction *action);
private:
struct Private;
Private * const d;
};
#endif // KIS_SCRIPT_MANAGER_H
......@@ -82,8 +82,8 @@ KritaPyQtPlugin::KritaPyQtPlugin(QObject *parent, const QVariantList &)
dbgScript << "Cannot load pykrita module";
}
Q_FOREACH (Extension* ext, Krita::instance()->extensions()) {
ext->setup();
Q_FOREACH (Extension *extension, Krita::instance()->extensions()) {
extension->setup();
}
}
......
......@@ -28,8 +28,7 @@ public Q_SLOTS:
void setToolTip(QString tooltip);
QString tooltip() const;
void trigger();
void setMenu(const QString menu);
QString menu() const;
Q_SIGNALS:
void triggered(bool);
private:
......
......@@ -8,4 +8,5 @@ class Extension : QObject
public:
explicit Extension(QObject *parent /TransferThis/ = 0);
virtual void setup() = 0;
virtual void createActions(Window *window) = 0;
};
......@@ -34,7 +34,6 @@ public Q_SLOTS:
QList<Extension*> extensions() /Factory/;
Document * openDocument(const QString &filename) /Factory/;
Window * openWindow();
Action * createAction(const QString &id, const QString & text, bool addToScriptMenu = true);
QIcon icon(QString &iconName) const;
void addExtension(Extension* _extension /GetWrapper/);
......
......@@ -16,6 +16,7 @@ public Q_SLOTS:
View *activeView() const /Factory/;
void activate();
void close();
QAction *createAction(const QString &id, const QString &text = QString(), const QString &menuLocation = QString("tools/scripts"));
Q_SIGNALS:
void windowClosed();
private:
......
......@@ -50,7 +50,10 @@ class AssignProfileDialog(Extension):
doc.setColorProfile(self.cmbProfile.currentText())
def setup(self):
action = Application.createAction("assing_profile_to_image", "Assign Profile to Image")
pass
def createActions(self, window):
action = window.createAction("assing_profile_to_image", "Assign Profile to Image")
action.triggered.connect(self.assignProfile)
Scripter.addExtension(AssignProfileDialog(Application))
......@@ -19,7 +19,10 @@ class ColorSpaceExtension(krita.Extension):
super(ColorSpaceExtension, self).__init__(parent)
def setup(self):
action = krita.Krita.instance().createAction("color_space", "Color Space")
pass
def createActions(self, window):
action = window.createAction("color_space", "Color Space")
action.setToolTip("Plugin to change color space to selected documents")
action.triggered.connect(self.initialize)
......
......@@ -19,7 +19,10 @@ class DocumentToolsExtension(krita.Extension):
super(DocumentToolsExtension, self).__init__(parent)
def setup(self):
action = krita.Krita.instance().createAction("document_tools", "Document Tools")
pass
def createActions(self, window):
action = window.createAction("document_tools", "Document Tools")
action.setToolTip("Plugin to manipulate properties of selected documents")
action.triggered.connect(self.initialize)
......
......@@ -19,7 +19,10 @@ class ExportLayersExtension(krita.Extension):
super(ExportLayersExtension, self).__init__(parent)
def setup(self):
action = krita.Krita.instance().createAction("export_layers", "Export Layers")
pass
def createActions(self, window):
action = window.createAction("export_layers", "Export Layers")
action.setToolTip("Plugin to export layers from a document")
action.triggered.connect(self.initialize)
......
......@@ -19,7 +19,10 @@ class FilterManagerExtension(krita.Extension):
super(FilterManagerExtension, self).__init__(parent)
def setup(self):
action = krita.Krita.instance().createAction("filter_manager", "Filter Manager")
pass
def createActions(self, window):
action = window.createAction("filter_manager", "Filter Manager")
action.setToolTip("Plugin to filters management")
action.triggered.connect(self.initialize)
......
......@@ -42,11 +42,13 @@ class HelloExtension(Extension):
super().__init__(parent)
def setup(self):
pass
def createActions(self, window):
"""
This is where most of the setup takes place!
"""
#qDebug("Hello Setup")
action = Krita.instance().createAction("hello_python", "hello", False)
action = window.createAction("hello_python", "hello")
action.triggered.connect(hello)
......
......@@ -21,7 +21,10 @@ class HighpassExtension(Extension):
super().__init__(parent)
def setup(self):
action = Application.createAction("high_pass_filter", "High Pass")
pass
def createActions(self, window):
action = window.createAction("high_pass_filter", "High Pass")
action.triggered.connect(self.showDialog)
def showDialog(self):
......
......@@ -27,7 +27,10 @@ class ScripterExtension(Extension):
super().__init__(parent)
def setup(self):
action = Krita.instance().createAction("python_scripter", "Scripter")
pass
def createActions(self, window):
action = window.createAction("python_scripter", "Scripter")
action.triggered.connect(self.initialize)
def initialize(self):
......
......@@ -23,12 +23,13 @@ class TenBrushesExtension(krita.Extension):
self.selectedPresets = []