Recognising Mouse Gestures (with PyQt4)

In response to a request from a member on the Qt Project forums I am posting the following which is my Python port of the excellent example of recognising mouse gestures as published in Qt Quarterly by Johan Thelin which can be found here. The plain text source for this example can also be found here.

# This file is a Python port of Johan Thelin's mouse gesture package
# Copyright (C) 2013 Rob Kent <rob@gulon.co.uk>
# All rights reserved.
#
# Original C++ package can be found  here: http://doc.qt.digia.com/qq/qq18-mousegestures.html
# Copyright (C) 2006 Johan Thelin <e8johan@gmail.com>
# All rights reserved.

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class Gesture:
    # Directions
    Up            = 0
    Down          = 1
    Left          = 2
    Right         = 3
    AnyHorizontal = 4
    AnyVertical   = 5
    NoMatch       = 6

class MouseGestureCallback(object):
    def callback(self): raise NotImplementedError

class GestureDefinition(object):
    def __init__(self, directionList, mouseGestureCallback):
        self.directions=directionList
        self.callbackClass=mouseGestureCallback

class MouseGestureRecognizer(object):

    class Private(object):
        def __init__(self):
            self.positions=[]
            self.gestures=[]
            self.minimumMovement2=0
            self.minimumMatch=0.0

    def __init__(self, minimumMovement=5, minimumMatch=0.9):
        self.d=MouseGestureRecognizer.Private()
        self.d.minimumMovement2=minimumMovement*minimumMovement
        self.d.minimumMatch=minimumMatch

    def addGestureDefinition(self, gesture): self.d.gestures.append(gesture)
    def clearGestureDefinitions(self): self.d.gestures=[]

    def startGesture(self, x, y):
        self.d.positions=[]
        self.d.positions.append(Pos(x,y))

    def addPoint(self, x, y):
        dx=x-self.d.positions[-1].x
        dy=y-self.d.positions[-1].y

        if dx*dx+dy*dy >= self.d.minimumMovement2:
            self.d.positions.append(Pos(x,y))

    def endGesture(self, x, y):
        if x!=self.d.positions[-1].x or y!=self.d.positions[-1].y:
            self.d.positions.append(Pos(x,y))
        if len(self.d.positions)>1: self.recognizeGesture()
        self.d.positions=[]

    def abortGesture(self): self.d.positions=[]

    def recognizeGesture(self):
        directions=simplify(limitDirections(self.d.positions))
        minLength=calcLength(directions)*self.d.minimumMatch

        while len(directions)>0 and calcLength(directions)>minLength:
            for gi in self.d.gestures:
                if len(gi.directions)==len(directions):
                    match=True
                    for i,di in enumerate(gi.directions):
                        if not match: break
                        pi=directions[i]

                        if di==Gesture.Up:
                            if pi.y>=0: match=False
                        elif di==Gesture.Down:
                            if pi.y<=0: match=False
                        elif di==Gesture.Left:
                            if pi.x>=0: match=False
                        elif di==Gesture.Right:
                            if pi.x<=0: match=False
                        elif di==Gesture.AnyHorizontal:
                            if pi.x==0: match=False
                        elif di==Gesture.AnyVertical:
                            if pi.y==0: match=False
                        elif di==Gesture.NoMatch: match=False

                    if match:
                        gi.callbackClass.callback()
                        return

            directions=simplify(removeShortest(directions))

        for gi in self.d.gestures:
            if len(gi.directions)==1:
                if gi.directions[-1]==Gesture.NoMatch:
                    gi.callbackClass.callback()
                    return

class Pos(object):
    def __init__(self, x, y):
        self.x=x
        self.y=y

def limitDirections(positions):
    res=[]
    firstTime=True

    for ii in positions:
        if firstTime:
            lastx=ii.x
            lasty=ii.y
            firstTime=False
        else:
            dx=ii.x-lastx
            dy=ii.y-lasty

            if dy>0:
                if dx>dy or -dx>dy: dy=0
                else: dx=0
            else:
                if dx>-dy or -dx>-dy: dy=0
                else: dx=0

            res.append(Pos(dx,dy))
            lastx=ii.x
            lasty=ii.y

    return res

def simplify(positions):
    res=[]
    lastdx=0
    lastdy=0
    firstTime=True

    for ii in positions:
        if firstTime:
            lastdx=ii.x
            lastdy=ii.y
            firstTime=False
        else:
            joined=False
            if (lastdx>0 and ii.x>0) or (lastdx<0 and ii.x<0):
                lastdx+=ii.x
                joined=True
            if (lastdy>0 and ii.y>0) or (lastdy<0 and ii.y<0):
                lastdy+=ii.y
                joined=True
            if not joined:
                res.append(Pos(lastdx,lastdy))
                lastdx=ii.x
                lastdy=ii.y

    if lastdx!=0 or lastdy!=0: res.append(Pos(lastdx,lastdy))

    return res

def removeShortest(positions):
    res=[]
    firstTime=True

    for ii in positions:
        if firstTime:
            shortestSoFar=(ii.x*ii.x)+(ii.y*ii.y)
            shortest=ii
            firstTime=False
        else:
            if ((ii.x*ii.x)+(ii.y*ii.y))<shortestSoFar:
                shortestSoFar=(ii.x*ii.x)+(ii.y*ii.y)
                shortest=ii

    for ii in positions:
        if ii!=shortest: res.append(ii)

    return res

def calcLength(positions):
    res=0

    for ii in positions:
        if ii.x>0: res+=ii.x
        elif ii.x<0: res-=ii.x
        elif ii.y>0: res+=ii.y
        else: res-=ii.y

    return res

class GestureCallbackToSignal(MouseGestureCallback):
    def __init__(self, obj):
        MouseGestureCallback.__init__(self)

        self._object=obj

    def callback(self): self._object.emitGestured()

class QjtMouseGesture(QObject):

    gestured=pyqtSignal()

    def __init__(self, directions, parent=None):
        QObject.__init__(self, parent)

        self._directions=directions

    def directions(self): return self._directions
    def emitGestured(self): self.gestured.emit()

class QjtMouseGestureFilter(QObject):

    class Private(object):
        def __init__(self):
            self.gestureButton=Qt.RightButton
            self.tracing=False
            self.mgr=MouseGestureRecognizer()
            self.gestures=[]
            self.bridges=[]

    def __init__(self, gestureButton=Qt.RightButton, parent=None):
        QObject.__init__(self, parent)

        self.d=QjtMouseGestureFilter.Private()
        self.d.gestureButton=gestureButton
        self.d.tracing=False

    def addGesture(self, gesture):
        dl=

        self.d.bridges.append(GestureCallbackToSignal(gesture))
        self.d.gestures.append(gesture)

        self.d.mgr.addGestureDefinition(GestureDefinition(dl, self.d.bridges[-1]))

    def clearGestures(self, deleteGestures=False):
        self.d.gestures=[]
        self.d.bridges=[]
        self.d.mgr.clearGestureDefinitions()

    def eventFilter(self, obj, e):
        if e.type()==QEvent.MouseButtonPress:
            if self.mouseButtonPressEvent(obj,e): return True
        elif e.type()==QEvent.MouseButtonRelease:
            if self.mouseButtonReleaseEvent(obj,e): return True
        elif e.type()==QEvent.MouseMove:
            if self.mouseMoveEvent(obj,e): return True

        return QObject.eventFilter(self, obj, e)

    def mouseButtonPressEvent(self, obj, e):
        if e.button()==self.d.gestureButton:
            self.d.mgr.startGesture(e.pos().x(), e.pos().y())
            self.d.tracing=True
            return True
        return False

    def mouseButtonReleaseEvent(self, obj, e):
        if self.d.tracing and e.button()==self.d.gestureButton:
            self.d.mgr.endGesture(e.pos().x(), e.pos().y())
            self.d.tracing=False
            return True
        return False

    def mouseMoveEvent(self, obj, e):
        if self.d.tracing:
            self.d.mgr.addPoint(e.pos().x(), e.pos().y())
            return True
        return False

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self._checkboxes=[]

        base=QWidget(self)
        vl=QVBoxLayout(base)

        for i in range(5):
            self._checkboxes.append(QCheckBox("No. %d" % (i+1), self))
            vl.addWidget(self._checkboxes[i])

        hl=QHBoxLayout()
        hl.addWidget(QPushButton("Check", self, clicked=self.setAll))
        hl.addWidget(QPushButton("Clear", self, clicked=self.clearAll))
        vl.addLayout(hl)

        self.setCentralWidget(base)

    def clearAll(self):
        for cb in self._checkboxes: cb.setChecked(False)

    def setAll(self):
        for cb in self._checkboxes: cb.setChecked(True)

    def noMatch(self):
        QMessageBox.warning(self,
                            "Error",
                            "Failed to recognize the performed mouse gesture<br>" \
                                "<br>" \
                                "<b>Supported gestures</><br>" \
                                "- Sideways * 3 - clear all<br>" \
                                "- Up-Left - set all<br>"
                            )

    def secretMessage(self):
        QMessageBox.information(self,
                                "Congratulations",
                                "<b>Congratulations!</b><br>" \
                                    "You found the secret message!"
                                )

if __name__=="__main__":
    from sys import argv, exit

    app=QApplication(argv)
    filter_=QjtMouseGestureFilter()
    mw=MainWindow()

    dl=[Gesture.AnyHorizontal, Gesture.AnyHorizontal, Gesture.AnyHorizontal]
    g=QjtMouseGesture(dl, filter_)
    filter_.addGesture(g)
    g.gestured.connect(mw.clearAll)

    dl=[Gesture.Up, Gesture.Left]
    g=QjtMouseGesture(dl, filter_)
    filter_.addGesture(g)
    g.gestured.connect(mw.setAll)

    dl=[Gesture.NoMatch]
    g=QjtMouseGesture(dl, filter_)
    filter_.addGesture(g)
    g.gestured.connect(mw.noMatch)

    corners=[
        [Gesture.Up, Gesture.Right, Gesture.Down, Gesture.Left],
        [Gesture.Right, Gesture.Down, Gesture.Left, Gesture.Up],
        [Gesture.Down, Gesture.Left, Gesture.Up, Gesture.Right],
        [Gesture.Left, Gesture.Up, Gesture.Right, Gesture.Down]
    ]
    for dl in corners:
        g=QjtMouseGesture(dl, filter_)
        filter_.addGesture(g)
        g.gestured.connect(mw.secretMessage)

    mw.installEventFilter(filter_)
    mw.show()
    mw.raise_()

    exit(app.exec_())
Share:
Facebook
Twitter
Google+
http://www.gulon.co.uk/2013/01/09/recognising-mouse-gestures-with-pyqt4/
RSS
Follow by Email
SHARE

Leave a Reply

Your email address will not be published. Required fields are marked *