Hoe een Python plug-in te maken in Krita

Wellicht heeft u enkele leuke scripts die u heeft geschreven in de Scripter Python runner, en misschien wilt u er meer mee doen, bijvoorbeeld het automatisch laten uitvoeren. Uw script wrappen in een plugin zal u veel meer flexibiliteit en kracht geven dan als u het vanuit de Scripter editor opstart.

Okay, zelfs als u redelijk bekent bent met python, dan zijn er toch enkele kleine details waar u voor moet zorgen voordat Krita een python plugin zal herkennen. Daarom geeft deze pagina een overzicht voor hoe u de verschillende typen python script voor Krita creëert.

Deze mini-tutorials zijn geschreven voor mensen met een basis kennis van python, en zijn op een dergelijke manier geschreven dat ze aanmoedigen om te experimenteren en niet om gewoon de code alleen maar te kopiëren en te plakken, lees daarom de tekst nauwkeurig.

Zorgen dat Krita uw plug-in herkent

Een script in Krita heeft twee componenten – de script map (waarin uw script’s Python bestanden staan) en een “.desktop” bestand wat Krita gebruikt om uw script te laden en te registreren. Om Krita uw script te kunnen laden moeten ze allebei in de submap file:pykrita in de hulpbronmap van Krita geplaatst zijn (Lees Hulpdatabeheer voor de locatie bij elk besturingssysteem). Om uw hulpbronmap te vinden, start u Krita en opent u het menuitem Instellingen ‣ Hulpbronnen beheren…. Dit opent een dialoogvenster. Klik op de knop Map met hulpbronnen openen. Er zou nu een dialoogvenster voor bestandsbeheer moeten openen met daarin de hulpbronmap van Krita. Zie de API docs onder “Auto starting scripts”. Als er nog geen pykrita submap in de hulpbronmap van Krita aanwezig is, gebruik dan uw bestandsbeheerder om er een te creëren.

Scripts worden geïdentificeerd door een bestand dat eindigt met een ´´.desktop´´- extensie die informatie bevat over het script zelf.

Voor elke juiste plug-in is het nodig dat u een map aanmaakt en een .desktop-bestand.

Het desktop-bestand zou er als volgt uit moeten zien:

[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=meinplugin
X-Python-2-Compatible=false
X-Krita-Manual=meinPluginHandleiding.html
Name=Mijn eigen Plugin
Comment=Onze heel eigen plugin.
Type

Dit zou altijd service moeten zijn.

ServiceTypes

Dit zou altijd Krita/PythonPlugin moeten zijn voor python plugins.

X-KDE-Library

Dit zou de naam moeten zijn van de plugin-map die u net heeft gecreëerd.

X-Python-2-Compatible

Of het compatibel is met python 2. Als Krita is gebouwd met python 2 in plaats van met 3 (-DENABLE_PYTHON_2=ON in de cmake configuratie), dan zou deze plugin in de lijst niet zichtbaar moeten zijn.

X-Krita-Manual

Een optionele waarde die wijst naar de handleiding. Deze is zichtbaar in de Python Plugin manager. Als het een HTML-bestand is, dan is het zichtbaar als rich text <https://doc.qt.io/qt-5/richtext-html-subset.html>`_, als dat niet het geval is, dan is het zichtbaar als gewone tekst.

Naam

De naam die zichtbaar zal zijn in de Python Plugin Manager.

Comment

De omschrijving die zichtbaar is in de Python Plugin Manager.

Krita python plugins moeten python modules zijn, zorg er daarom voor dat er een __init__.py script aanwezig is, waarin iets voorkomt als…

from .meinplugin import *

Waar mijnplugin de naam is van het hoofd-bestand van uw plugin. Als u Krita opnieuw start, dan zou het nu in de Python Plugin Manager in de instellingen zichtbaar moeten zijn, maar het zal grijs zijn, omdat er geen mijnplugin.py aanwezig is. Als u met de muis boven de gedeactiveerde plugin zweeft, dan kunt de foutmelding zien.

Notitie

U moet expliciet uw plugin inschakelen. Ga daarvoor naar de Instellingen-menu, open het dialoogvenster Krita instellen en ga naar de pagina voor de Python Plugin Manager en schakel uw plugin in.

Samenvatting

Samenvattend, als u een script genaamd meinplugin wilt creëren:

  • creëert u in de hulpbronmap van Krita resources/pykrita map
    • een map genaamd meinplugin

    • een bestand genaamd meinplugin.desktop

  • in de map meinplugin creëert u
    • een bestand genaamd __init__.py

    • een bestand genaamd meinplugin.py

  • in het bestand:__init__.py plaatst u deze code:

from .meinplugin import *
  • in het desktop-bestand plaatst u deze code:

    [Desktop Entry]
    Type=Service
    ServiceTypes=Krita/PythonPlugin
    X-KDE-Library=meinplugin
    X-Python-2-Compatible=false
    X-Krita-Manual=meinPluginHandleiding.html
    Name=Mijn eigen Plugin
    Comment=Onze heel eigen plugin.
    
  • schrijf uw script in het bestand file:myplugin/meinplugin.py.

Een extension creëren

Extensions zijn relatief eenvoudige python scripts die worden uitgevoerd bij de start van Krita. Ze worden gemaakt door de Extension class uit te breiden, en de meest eenvoudige extension ziet er als volgt uit:

from krita import *

class MyExtension(Extension):

    def __init__(self, parent):
        # Dit initialiseert de parent, altijd belangrijk bij het subclassing.
        super().__init__(parent)

    def setup(self):
        pass

    def createActions(self, window):
        pass

# En voegt de extensie toe aan de lijst in Krita met extensies:
Krita.instance().addExtension(MyExtension(Krita.instance()))

Deze code doet natuurlijk niets. In createActions voegen we standaard actions toe aan Krita, zodat we vanuit Hulpmiddelen menu toegang hebben tot ons script..

Laten we eerst een action creëren. We kunnen dat heel makkelijk doen met Window.createAction(). Krita zal createActions aanroepen voor elke Window die is gecreëerd en het juiste window object dat we moeten gebruiken doorgeven.

Dus…

def createActions(self, window):
    action = window.createAction("meinActie", "Mein Script", "tools/scripts")
“meinActie”

Dit zal worden vervangen door een uniek ID dat Krita zal gebruiken om de actie te vinden.

“Mein Script”

Dit is wat u ziet in het Menu Hulpmiddelen.

Als u nu Krita opnieuw start, dan zal u een action hebben genaamd “Mein Script”. Dat doet nog steeds niets, omdat we het nog niet verbonden hebben met een script.

Laten we daarom een eenvoudig document-export script maken. Voeg het volgende toe aan de extension class, zorg er voor dat het boven de plek waar u de extension aan Krita toevoegde:

def exportDocument(self):
    # Haal het document op:
    doc =  Krita.instance().activeDocument()
    # Het opslaan van een niet bestaand document veroorzaakt crashes, daarom controleren dat eerst.
    if doc is not None:
        # Dit roept aan het save dialog. Het save dialog geeft een tuple.
        fileName = QFileDialog.getSaveFileName()[0]
        # En exporteer het document naar de fileName locatie.
        # InfoObject is een woordenboek met specifieke export opties, maar als we een lege creëren dan zal Krita de export defaults gebruiken.
        doc.exportImage(fileName, InfoObject())

En voeg de import toe voor de QFileDialog van hierboven met de imports:

from krita import *
from PyQt5.QtWidgets import QFileDialog

Om dan de action met het nieuwe export document te verbinden:

def createActions(self, window):
    action = window.createAction("meinActie", "Mein Script")
    action.triggered.connect(self.exportDocument)

Dit is een voorbeeld van een signal/slot connection, wat Qt-programma’s zoals Krita veel gebruiken. We zullen later meer vertellen over hoe we onze eigen signals en slots creëren.

Start Krita opnieuw op en uw nieuwe action zou nu het document moeten exporteren.

Instelbare sneltoetsen creëren

Nu is uw nieuwe action nog niet te zien in Instellingen ‣ Krita instellen ‣ Sneltoetsen.

Om verschillende redenen voegt Krita alleen actions toe aan de Instellingen van sneltoetsen als ze aanwezig zijn in een .action-bestand. Het action-bestand waar we onze action vandaan krijgen om het toe te voegen aan de sneltoetsen, zou er als volgt uit moeten zien:

<?xml version="1.0" encoding="UTF-8"?>
<ActionCollection version="2" name="Scripts">
    <Actions category="Scripts">
        <text>Mein Scripts</text>

        <Action name="myAction">
        <icon></icon>
        <text>Mein Script</text>
        <whatsThis></whatsThis>
        <toolTip></toolTip>
        <iconText></iconText>
        <activationFlags>10000</activationFlags>
        <activationConditions>0</activationConditions>
        <shortcut>ctrl+alt+shift+p</shortcut>
        <isCheckable>false</isCheckable>
        <statusTip></statusTip>
        </Action>
    </Actions>
</ActionCollection>
<text>Mein Scripts</text>

Dit creëert onder scripts een een subcategorie genaamd “Mein Scripts” waaraan uw sneltoetsen worden toegevoegd.

name

Dit zou het unieke ID moeten zijn dat u maakte voor uw action bij het creëren daarvan in de setup van de extension.

icon

De naam van een mogelijk icoon. Deze is wordt alleen gebruikt bij KDE plasma, omdat Gnome en Windows-gebruikers klaagden dat ze er lelijk uit zagen.

text

De tekst die zichtbaar is in de sneltoets editor.

whatsThis

Deze tekst wordt zichtbaar als een Qt-programma een specifieke oproep doet voor ‘what is this’, wat een help actie is.

toolTip

Dee tool tip, dit wordt zichtbaar als u met uw muis er boven zweeft.

iconText

Deze tekst wordt getoond bij gebruik in een werkbalk. Als voorbeeld dus, “Resize Image to New Size” zou om ruimte besparen kunnen worden afgekort tot “Resize Image”, dus dat is wat we hier plaatsen.

activationFlags

Dit bepaalt of een action is uitgeschakeld.

activationConditions

Dit bepaalt onder welke condities het geactiveerd kan worden (b.v. activeer het alleen als de selectie bewerkbaar is). Zie de code voor voorbeelden.

shortcut

Standaard sneltoets.

isCheckable

Of het een keuzevakje is.

statusTip

De status tip wordt getoond op de statusbalk.

Sla dit bestand op als meinplugin.action waar meinplugin de naam is van uw plugin. Het action-bestand zou moeten worden opgeslagen, niet in de map pykrita, maar in de map genaamd “actions”. (Dus, share/pykrita is waar de python plugins en desktop bestanden gaan, en share/actions is waar de action bestanden gaan). Start Krita opnieuw op. De sneltoets zou nu zichtbaar moeten zijn in de lijst met sneltoetsen.

Een dialoogvenster creëren

De creatie van een dialoogvenster lijkt er op de creatie van een extension. Dialoogvensters zijn op een bepaalde manier een beetje makkelijker, maar ze hebben ook meer het gebruik van widgets nodig. Dit is de code voor een barebone dialoogvenster:

from PyQt5.QtWidgets import *
from krita import *

class MeinDocker(DockWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Mein Docker")

    def canvasChanged(self, canvas):
        pass

Krita.instance().addDockWidgetFactory(DockWidgetFactory("meinDocker", DockWidgetFactoryBase.DockRight, MeinDocker))

De window title bepaalt hoe het verschijnt in de docker lijst van Krita. canvasChanged moet altijd aanwezig zijn, maar als u er niets mee te doen heeft, dan vult u gewoon in ‘pass’.

Voor de addDockWidgetFactory…

“meinDocker”

Vervang dit door een unieke ID voor uw docker zodat Krita het altijd kan aanroepen.

DockWidgetFactoryBase.DockRight

De locatie. Dit kan zijn DockTornOff, DockTop, DockBottom, DockRight, DockLeft, of DockMinimized

MeinDocker

Vervang dit door de class name van de docker die u wilt toevoegen.

Als we onze export document functie die we in de extension sectie hebben gecreëerd toevoegen aan deze docker code, hoe kunnen de gebruiker het laaten activeren? Eerst moeten we wat Qt GUI programmeren: We gaan een knop toevoegen!

Krita gebruikt standaard PyQt, maar de documentatie daarvan is vrij slecht, voornamelijk omdat de normale Qt documentatie zo goed is, en u zal vaak merken dat de PyQt documentatie van een een class, bijvoorbeeld , QWidget is een rare kopie lijkt van de normale Qt documentatie voor die class.

Hoe dan ook, het eerste wat we moeten doen is dat we een QWidget creëren, het is niet erg ingewikkeld, voeg onder setWindowTitle toe:

mainWidget = QWidget(self)
self.setWidget(mainWidget)

Dan gaan we een knop creëren:

buttonExportDocument = QPushButton("Export Document", mainWidget)

Om nu de knop met onze functie te verbinden, moeten we een kijkje nemen in de documentatie voor de signals. QPushButton heeft geen unieke signals van zichzelf, maar er staat dat het 4 signals erft van QAbstractButton, wat betekent dat we die ook kunnen gebruiken. In ons geval willen we clicked.

buttonExportDocument.clicked.connect(self.exportDocument)

Als we nu Krita opnieuw starten, dan hebben we een nieuwe docker en in die docker is er een knop. Als we op die knop klikken dan roepen we de export export functie aan.

Maar, de knop lijkt een beetje raar uitgelijnd. Dat is omdat onze mainWidget geen layout heeft. Laten we er snel eentje maken:

mainWidget.setLayout(QVBoxLayout())
mainWidget.layout().addWidget(buttonExportDocument)

Qt heeft meerdere layouts, maar de QHBoxLayout en de QVBoxLayout zijn het makkelijkst om te gebruiken, ze lijnen de widgets alleen maar horizontaal of verticaal uit.

Start Krita opnieuw op en de knop zou nu netjes uitgelijnd moeten zijn.

PyQt Signals en Slots

We hebben al gebruik gemaakt van PyQt signals en slots, maar er zijn momenten dat we onze eigen signals en slots willen creëren. Omdat de documentatie van PyQt vrij moeilijk te begrijpen is, en de manier waarop signals en slots worden gecreëerd heel erg afwijkt van bij C++ Qt, gaan we het hier uitleggen:

Alle python functies die u in PyQt maakt, kunnen worden begrepen als slots, wat inhoud dat ze kunnen worden verbonden aan signals zoals Action.triggered of QPushButton.clicked. Echter, QCheckBox heeft een signal voor omgeschakeld, wat een boolean zendt. Hoe zorgen we ervoor dat onze functie die boolean accepteert?

Zorg er eerst voor dat u de juiste import heeft voor aangepaste slots:

from PyQt5.QtCore import pyqtSlot

(Als er al een from PyQt5.QtCore import * aanwezig is in de lijst met imports, dan hoeft u dit natuurlijk niet te doen.)

U moet dan een PyQt slot definitie (eerder dan uw) voor uw functie toevoegen:

@pyqtSlot(bool)
def myFunction(self, enabled):
    enabledString = "disabled"
    if (enabled == True):
        enabledString = "enabled"
    print("The checkbox is"+enabledString)

Als u dan uw checkbox heeft gecreëerd, dan kunt u zoiets doen als myCheckbox.toggled.connect(self.myFunction).

Om op dezelfde manier uw eigen PyQt signals te maken, doet u het volgende:

# signal name is added to the member variables of the class
signal_name = pyqtSignal(bool, name='signalName')

def emitMySignal(self):
    # And this is how you trigger the signal to be emitted.
    self.signal_name.emit(True)

En gebruik de juiste import:

from PyQt5.QtCore import pyqtSignal

Om voor objecten die geen standaard python objecten zijn, slots te creëren of te verzenden, hoeft u hun namen alleen maar tussen aanhalingstekens (’) te plaatsen.

Een opmerking over unit tests

Als u unit tests voor uw plugin wilt schrijven, kijk dan eens naar de mock krita module.

Conclusie

Okay, zo dat dekt alle specifieke details wat betreft Krita voor de creatie van python plugins. Het beschrijft niet hoe u pixel data moet hanteren, of de best practices met documenten, maar als u een beetje ervaring heeft met python dan zou u in staat moeten zijn om te beginnen met de creatie van uw eigen plugins.

En zoals altijd, bestudeer de code nauwkeurig en lees de API docs voor python, Krita en Qt nauwkeurig om te ontdekken wat mogelijk is, en u zou een heel eind moeten komen.