Python - Release Event from Canvas

Is it possible to detect the signal of the stylus lifting up from the canvas?
I imagine it would be mouseReleaseEvent or tabletRelease signal.

Maybe this helps hunting of lift event.

from PyQt5.QtCore import (
        Qt,
        QSize)

from PyQt5.QtGui import (
        QInputEvent,
        QTabletEvent,
        QMouseEvent)

from PyQt5.QtWidgets import (
        QMdiArea,
        QAbstractScrollArea,
        QPlainTextEdit)


def get_qview(view):
    window = view.window()
    qwindow = window.qwindow()
    mdi_area = qwindow.centralWidget().findChild(QMdiArea)
    for kis_view, sub_win in zip(window.views(), mdi_area.subWindowList()):
        if view == kis_view:
            return next(c for c in sub_win.children() if c.metaObject().className() == 'KisView')

def get_qcanvas(canvas):
    qview = get_qview(canvas.view())
    children = qview.findChild(QAbstractScrollArea).viewport().children()
    for c in children:
        cls_name = c.metaObject().className()
        # KisOpenGLCanvas2 or KisQPainterCanvas
        if 'Canvas' in cls_name:
            return c


class CanvasEventLogger(QPlainTextEdit):
    def __init__(self):
        super().__init__()
        self.setObjectName('canvas_event_logger')
        self._target_qcanvas = None
        self._window = window = Application.activeWindow()
        window.activeViewChanged.connect(lambda : self.set_filter_state(True))

    def set_filter_state(self, new_state):
        if new_state:
            # remove old
            if self._target_qcanvas is not None:
                self._target_qcanvas.removeEventFilter(self)
                self._target_qcanvas = None
            # install new
            view = self._window.activeView()
            if view.document() is not None:
                # view has actual Krita document.
                self._target_qcanvas = get_qcanvas(view.canvas())
                self._target_qcanvas.installEventFilter(self)
        else:
            if self._target_qcanvas is not None:
                self._target_qcanvas.removeEventFilter(self)
                self._target_qcanvas = None

    def eventFilter(self, obj, e):
        if isinstance(e, QInputEvent):
            # some sort of input event.
            #   QContextMenuEvent, QHoverEvent, QKeyEvent, QMouseEvent,
            #   QNativeGestureEvent, QTabletEvent, QTouchEvent, QWheelEvent
            if isinstance(e, QTabletEvent):
                self.dump_tablet_event(e)
            elif isinstance(e, QMouseEvent):
                self.dump_mouse_event(e)
        return False  # event was NOT consumed.

    def dump_tablet_event(self, tablet_event):
        """
        https://doc.qt.io/qtforpython-5/PySide2/QtGui/QTabletEvent.html
        using PySide2, should be identical to PyQt5
        """
        text = (
            f'{tablet_event}\n'
             '  from QEvent\n'
            f'    {tablet_event.type()=}\n'
            f'    {tablet_event.spontaneous()=}\n'
             '  from QInputEvent\n'
            f'    {tablet_event.timestamp()=}\n'
            f'    {tablet_event.modifiers()=}\n'
             '  from QTableEvent\n'
            f'    {tablet_event.button()=}\n'
            f'    {tablet_event.buttons()=}\n'
            f'    {tablet_event.device()=}\n'
            f'    {tablet_event.deviceType()=}\n'
            f'    {tablet_event.globalPos()=}\n'
            f'    {tablet_event.globalPosF()=}\n'
            f'    {tablet_event.globalX()=}\n'
            f'    {tablet_event.globalY()=}\n'
            f'    {tablet_event.hiResGlobalX()=}\n'
            f'    {tablet_event.hiResGlobalY()=}\n'
            f'    {tablet_event.pointerType()=}\n'
            f'    {tablet_event.pos()=}\n'
            f'    {tablet_event.posF()=}\n'
            f'    {tablet_event.pressure()=}\n'
            f'    {tablet_event.rotation()=}\n'
            f'    {tablet_event.tangentialPressure()=}\n'
            f'    {tablet_event.uniqueId()=}\n'
            f'    {tablet_event.x()=}\n'
            f'    {tablet_event.xTilt()=}\n'
            f'    {tablet_event.y()=}\n'
            f'    {tablet_event.yTilt()=}\n'
            f'    {tablet_event.z()=}\n'
        )
        self.insertPlainText(text)
        self.ensureCursorVisible()

    def dump_mouse_event(self, mouse_event):
        """
        https://doc.qt.io/qtforpython-5/PySide2/QtGui/QMouseEvent.html
        using PySide2, should be identical to PyQt5
        """
        text = (
            f'{mouse_event}\n'
             '  from QEvent\n'
            f'    {mouse_event.type()=}\n'
            f'    {mouse_event.spontaneous()=}\n'
             '  from QInputEvent\n'
            f'    {mouse_event.timestamp()=}\n'
            f'    {mouse_event.modifiers()=}\n'
             '  from QMouseEvent\n'
            f'    {mouse_event.button()=}\n'
            f'    {mouse_event.buttons()=}\n'
            f'    {mouse_event.flags()=}\n'
            f'    {mouse_event.globalPos()=}\n'
            f'    {mouse_event.globalX()=}\n'
            f'    {mouse_event.globalY()=}\n'
            f'    {mouse_event.localPos()=}\n'
            f'    {mouse_event.pos()=}\n'
            f'    {mouse_event.screenPos()=}\n'
            f'    {mouse_event.source()=}\n'
            f'    {mouse_event.windowPos()=}\n'
            f'    {mouse_event.x()=}\n'
            f'    {mouse_event.y()=}\n'
        )
        self.insertPlainText(text)
        self.ensureCursorVisible()

    def sizeHint(self):
        return QSize(800, 400)

# create logger.
# logger needs view changed signal, to be binded to new view.
logger = CanvasEventLogger()
logger.show()

I currently don’t have tablet available, but code works with mouse, so it should be close enough.
One of the following should be event you are looking for.

tablet_event.type() == QEvent.TabletPress
tablet_event.type() == QEvent.TabletRelease
tablet_event.type() == QEvent.TabletEnterProximity
tablet_event.type() == QEvent.TabletLeaveProximity

/AkiR

I was trying to run the script but I must confess I having some issues to run it and see any output.

I recreated the function to see some input:

def insertPlainText(self, text):
    QtCore.qDebug(str(text))

But I think there is a issue with perhaps the trigger connection:

window.activeViewChanged.connect(lambda : self.set_filter_state(True))

Because when the script starts at the startup it gives the error message:

AttributeError
Python 3.8.1: C:\Users\EyeOd\Desktop\krita-x64-5.0.0-beta1\bin\krita.exe
Sat Aug 28 20:27:52 2021

A problem occurred in a Python script.  Here is the sequence of
function calls leading up to the error, in the order they occurred.

 C:\Users\EyeOd\Desktop\krita-x64-5.0.0-beta1\lib\krita-python-libs\krita\dockwidgetfactory.py in createDockWidget(self=<krita.dockwidgetfactory.DockWidgetFactory object>)
   14         super(DockWidgetFactory, self).__init__(_id, _dockPosition)
   15         self.klass = _klass
   16 
   17     def createDockWidget(self):
   18         return self.klass()
self = <krita.dockwidgetfactory.DockWidgetFactory object>
self.klass = <class 'tela.tela_docker.TelaDocker'>

 C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_docker.py in __init__(self=<tela.tela_docker.TelaDocker object>)
  249         self.Connects()
  250         self.Style_Widget()
  251         self.Extension()
  252         self.Pulse()
  253 
self = <tela.tela_docker.TelaDocker object>
self.Extension = <bound method TelaDocker.Extension of <tela.tela_docker.TelaDocker object>>

 C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_docker.py in Extension(self=<tela.tela_docker.TelaDocker object>)
  461     def Extension(self):
  462         # Install Extension for Pigmento Docker
  463         extension = TelaExtension(parent=Krita.instance())
  464         Krita.instance().addExtension(extension)
  465         # Connect Extension Signals
extension undefined
global TelaExtension = <class 'tela.tela_extension.TelaExtension'>
parent undefined
global Krita = <class 'PyKrita.krita.Krita'>
Krita.instance = <built-in function instance>

 C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_extension.py in __init__(self=<tela.tela_extension.TelaExtension object>, parent=<PyKrita.krita.Krita object>)
   16     def __init__(self, parent):
   17         super().__init__(parent)
   18         self.setup()
   19     def setup(self):
   20         # pass
self = <tela.tela_extension.TelaExtension object>
self.setup = <bound method TelaExtension.setup of <tela.tela_extension.TelaExtension object>>

 C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_extension.py in setup(self=<tela.tela_extension.TelaExtension object>)
   22         self._target_qcanvas = None
   23         self._window = window = Application.activeWindow()
   24         window.activeViewChanged.connect(lambda : self.set_filter_state(True))
   25 
   26     #//
window = None
window.activeViewChanged undefined
self = <tela.tela_extension.TelaExtension object>
self.set_filter_state = <bound method TelaExtension.set_filter_state of <tela.tela_extension.TelaExtension object>>
AttributeError: 'NoneType' object has no attribute 'activeViewChanged'
    __cause__ = None
    __class__ = <class 'AttributeError'>
    __context__ = None
    __delattr__ = <method-wrapper '__delattr__' of AttributeError object>
    __dict__ = {}
    __dir__ = <built-in method __dir__ of AttributeError object>
    __doc__ = 'Attribute not found.'
    __eq__ = <method-wrapper '__eq__' of AttributeError object>
    __format__ = <built-in method __format__ of AttributeError object>
    __ge__ = <method-wrapper '__ge__' of AttributeError object>
    __getattribute__ = <method-wrapper '__getattribute__' of AttributeError object>
    __gt__ = <method-wrapper '__gt__' of AttributeError object>
    __hash__ = <method-wrapper '__hash__' of AttributeError object>
    __init__ = <method-wrapper '__init__' of AttributeError object>
    __init_subclass__ = <built-in method __init_subclass__ of type object>
    __le__ = <method-wrapper '__le__' of AttributeError object>
    __lt__ = <method-wrapper '__lt__' of AttributeError object>
    __ne__ = <method-wrapper '__ne__' of AttributeError object>
    __new__ = <built-in method __new__ of type object>
    __reduce__ = <built-in method __reduce__ of AttributeError object>
    __reduce_ex__ = <built-in method __reduce_ex__ of AttributeError object>
    __repr__ = <method-wrapper '__repr__' of AttributeError object>
    __setattr__ = <method-wrapper '__setattr__' of AttributeError object>
    __setstate__ = <built-in method __setstate__ of AttributeError object>
    __sizeof__ = <built-in method __sizeof__ of AttributeError object>
    __str__ = <method-wrapper '__str__' of AttributeError object>
    __subclasshook__ = <built-in method __subclasshook__ of type object>
    __suppress_context__ = False
    __traceback__ = <traceback object>
    args = ("'NoneType' object has no attribute 'activeViewChanged'",)
    with_traceback = <built-in method with_traceback of AttributeError object>

The above is a description of an error in a Python program.  Here is
the original traceback:

Traceback (most recent call last):
  File "C:\Users\EyeOd\Desktop\krita-x64-5.0.0-beta1\lib\krita-python-libs\krita\dockwidgetfactory.py", line 18, in createDockWidget
    return self.klass()
  File "C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_docker.py", line 251, in __init__
    self.Extension()
  File "C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_docker.py", line 463, in Extension
    extension = TelaExtension(parent=Krita.instance())
  File "C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_extension.py", line 18, in __init__
    self.setup()
  File "C:\Users\EyeOd\AppData\Roaming\krita\pykrita\tela\tela_extension.py", line 24, in setup
    window.activeViewChanged.connect(lambda : self.set_filter_state(True))
AttributeError: 'NoneType' object has no attribute 'activeViewChanged'

I think this error happens because the canvas still not exists on start up.
But what is on my mind as a question is how do extensions update to make a new check on it to connect and disconnect considering the situation.

Hmm…

Example code is meant to be run directly from Krita’s Scripter plugin, inside of Krita.
There is something strange happening in Krita, so first instance of CanvasEventLogger is destroyed when Krita’s context menu is shown (just re-run script to get second one). And also sometimes it loses currently active view (activating some other view will fix this)

Extension.setup() is called before first window is created so Application.activeWindow() is None!
You can try to delay setup… something like:

from PyQt5.QtCore import QTimer

class MyExtension(Extension):
    ...

    def setup(self):
        def _later():
            # this happens later, so now activeWindow should already be there.
            # just to be safe keep handle to window alive in python.
            self._window = Application.activeWindow()
            self._window.activeViewChanged.connect(self.on_active_view_changed)
        QTimer.singleShot(0, _later)

    def on_active_view_changed(self):
        """
        first uninstall event filter from old qcanvas (if old qcanvas is not None)
        second install event filter to currently active view's canvas
        """
        if self._target_canvas is not None:
            self._target_qcanvas.removeEventFilter(self)
            self._target_qcanvas = None
        view = self._window.activeView()
        if view.document() is not None:
            # bound only to views that have documents.
            self._target_qcanvas = get_qcanvas(view.canvas())
            self._target_qcanvas.installEventFilter(self)

/AkiR

Ps. I have not tested example script in Krita 5.0

That was the first thing I did actually and it only open a PlainTextEdit and would remain innactive and the log viewer too.

But then tried to place it on a empty extension to see if that made any difference, that is when I found that issue.

I am not having any luck with the setup either.

In Krita 5 you can use Notifier windowCreated() to be sure to get a window before doing stuff

(The event already exist in Krita 4 but it doesn’t work properly, it seems it was a choice to fix it in Krita 5 only)

Grum999

Ah, PlainTextEdit starts as unbinded, it binds it self first time view changes, after it is created.

So create two documents in Krita, run example code, and change currently activate view.

/AkiR

that made it work~ thank you Akir.
Now I am tinkering with it.