Active layer not updated in plugin

I am trying to make a copy of a layer and do some work on it.

I ran into an issue similar to the one discussed in this thread where the activeNode is not updated after layer manipulation.

The thread suggests that this is a timing issue; however, after playing a bit with it it’s starting to seem like it’s not.

Here is my first attempt: duplicate the current layer, then print the name of the active node:

from krita import *
    
app = Krita.instance()
doc = app.activeDocument()

app.action('duplicatelayer').trigger()
print(doc.activeNode().name())

I am running this in a document with a single layer called “v1”.
Expected result: “Copy of v1”
Actual result: “v1”

Ok, now let’s throw everything I found that would help with concurrency:

from krita import *
from PyQt5 import QtTest
    
app = Krita.instance()
doc = app.activeDocument()

app.action('duplicatelayer').trigger()

QtTest.QTest.qWait(100)
doc.waitForDone()
qApp.processEvents()

print(doc.activeNode().name())

Result: still “v1”. Setting the wait time to 10 seconds doesn’t help either.

However, based on the previous forum’s solution I tried this - create a dummy selection in between:

from krita import *
from PyQt5 import QtTest
    
app = Krita.instance()
doc = app.activeDocument()

app.action('duplicatelayer').trigger()

newSelection = Selection()
newSelection.select(100, 100, 32, 32, 255)    
doc.setSelection(newSelection)

print(doc.activeNode().name())

Result: “Copy of v1”!!!
Somehow creating a selection gets the activeNode unstuck.

Note that it only works if there is no Selection Mask already present!
If I run the above code when some selection is already active, I get “v1” again.

So there’s something in setSelection that, if there’s no selection already, properly updates the activeNode.

Is this a bug or is there something that I could call explicitly?

1 Like

Generally, you want to use the API when possible and not actions unless there is no other choice.

So get the activeNode() from the activeDocument() and do clone(), then add the cloned layer where you want it via addChildNode()

But if you really must:

from krita import *

    
app = Krita.instance()
doc = app.activeDocument()

app.action('duplicatelayer').trigger()


QTimer.singleShot(100, lambda: print(doc.activeNode().name()) )
4 Likes

Thanks for the quick reply!

I’ve tried using the API, all of my examples behave the same, for example:

from krita import *
    
app = Krita.instance()
doc = app.activeDocument()
root = doc.rootNode()

layer = doc.activeNode().duplicate()
layer.setName("My Copy of " + layer.name())
root.addChildNode(layer, None)

print(doc.activeNode().name())

prints “v1”

It seems the code you provided works, but in a strange way.
Running it directly doesn’t print anything because the plugin finishes (and presumably closes the output stream) before the lambda got a chance to run.

The lambda still runs and is successful. If I replace it with another “duplicate” call, we can see that it sees the correct layer:

from krita import *
from PyQt5 import QtTest

app = Krita.instance()
doc = app.activeDocument()
root = doc.rootNode()

layer = doc.activeNode().clone()
layer.setName("My Copy of " + layer.name())
root.addChildNode(layer, None)

QTimer.singleShot(100, Krita.instance().action('duplicatelayer').trigger )

Creates layers v1, “My Copy of v1” and “Copy of My Copy of v1”.

I suspect that when the plugin finishes, whatever bookkeeping needs to happen is triggered, so the function that runs after the plugin finishes sees the correct value for the active layer.

If I prevent the plugin from finishing though, the action inside sees the wrong layer too. Consider:

from krita import *
from PyQt5 import QtTest

app = Krita.instance()
doc = app.activeDocument()
root = doc.rootNode()

layer = doc.activeNode().clone()
layer.setName("My Copy of " + layer.name())
root.addChildNode(layer, None)

QTimer.singleShot(100,  lambda: print("Inside: ", doc.activeNode().name()) )

print("outside: ", doc.activeNode().name())
QtTest.QTest.qWait(200)
print("outside2: ", doc.activeNode().name())

It prints:

outside:  v1
Inside:  v1
outside2:  v1

It doesn’t look like a timing issue, it looks like some bookkeeping that needs to happen as part of “duplicate” doesn’t, and I’m unsure how to trigger it (other than making a dummy selection ^_^; )
Or maybe it’s a bug?

When you do a timer or event, order is not guaranteed. That is normal.

qWait probably locks the thread. QTest doesn’t work on my appimage version for some reason, so I can’t test it myself.

If you want something to guarantee that it processes when layer changes, then:

from krita import *



app = Krita.instance()
doc = app.activeDocument()
root = doc.rootNode()

layer = doc.activeNode().clone()
layer.setName("My Copy of " + layer.name())
root.addChildNode(layer, None)




qwin = app.activeWindow().qwindow()
layerBox = qwin.findChild(QtWidgets.QDockWidget, "KisLayerBox")
layerList = layerBox.findChild(QtWidgets.QTreeView,"listLayers")

def layerChanged(selected,deselected):
    print("inside: ", doc.activeNode().name())
    layerList.selectionModel().selectionChanged.disconnect(layerChanged)

layerList.selectionModel().selectionChanged.connect(layerChanged)
2 Likes

Ooh, thanks! That’s a neat debugging tool!

I’ve also read up a bit on using the “Log Viewer” docker together with QtCore.qDebug, so I can now reliably get logs from the parts of the script that execute after the main script is done.

I think this confirms my suspicions:

from krita import *
from PyQt5 import QtTest

app = Krita.instance()
doc = app.activeDocument()
root = doc.rootNode()

QtCore.qDebug("============================ New run ====")
layer = doc.activeNode().clone()
layer.setName("My Copy of " + layer.name())
root.addChildNode(layer, None)

qwin = app.activeWindow().qwindow()
layerBox = qwin.findChild(QtWidgets.QDockWidget, "KisLayerBox")
layerList = layerBox.findChild(QtWidgets.QTreeView,"listLayers")
QtCore.qDebug("(out1) -> Active layer " + doc.activeNode().name())

def layerChanged(selected,deselected):
    print("inside: ", doc.activeNode().name())
    QtCore.qDebug("-> got a layer changed event")
    layerList.selectionModel().selectionChanged.disconnect(layerChanged)
    QtCore.qDebug("( in1) -> Active layer" + doc.activeNode().name())

layerList.selectionModel().selectionChanged.connect(layerChanged)
QtCore.qDebug("(out2) -> Active layer " + doc.activeNode().name())

QtTest.QTest.qWait(500)

QtCore.qDebug("== Setting selection " + doc.activeNode().name())
newSelection = Selection()
newSelection.select(100, 100, 32, 32, 255) 
QtCore.qDebug("== New selection " + doc.activeNode().name())   
doc.setSelection(newSelection)

QtTest.QTest.qWait(500)

QtCore.qDebug("==== Ending the script " + doc.activeNode().name())

I played around with this script a bit and here are my observations:
The layerChanged reliably triggers after “setSelection”. If I remove “setSelection”, it only ever triggers after the script ends. I tried adding “wait” in different places in the script but it doesn’t help.

I’m trying to get Krita set up with debugging and see what in setSelection actually triggers the layerChanged

I have had this issue for the longest time but I think what you got to do is after you create the layer explicitly state which one is the active node or do the operation to the node you just created by using its name as a pointer instead of relying on the active node change from the command done. I do agree the layer stack updates after the script runs.