Tuesday, July 28, 2009

Highlevel Mouse Event Converter (wxPython)
















I really needed to make a good one (not a school assignment :) that convers lowlevel mouse events to highlevel ones. When the user left double clicks on a window, the window gets leftdown, leftup, leftdoubleclick, leftup events in this order. It is too low level. In this case all we want is a single double click event.

- A double click event doesn't come with friends.
- Whenever a mouse down event comes, the application can except a following mouse up event (it may be after dragging event stuff or enter/leave window event).
- A series of dragging events are always wrapped by a startdragging and an enddragging event.
- Capture mouse automatically. (See the comment in onButtonDown() method)
- Can set threshold in pixels so that anybody can double click with his trembling hand (e.g. my dad's)

BUG
While dragging, EnterWindow/LeaveWindow occurs when the mouse enter/leaves the parent window on GTK+.

UPDATES
Jul 29: Passing proper event object to mouse down/up handlers.
Jul 31: Get the right mouse position while mouse captured.
Aug 5 : Fixed missing double click detection failure.
Aug 8 : Modified to Register popup menu functions.

"""Low level -> high level mouse event converter."""
import wx

NORMAL = 0
FIRSTWAIT = 1
SECONDWAIT = 2
AFTERDCLICK = 3
MOUSEDOWN = 4
DRAGGING = 5

ID_FIRSTTIMER = wx.NewId()
ID_SECONDTIMER = wx.NewId()

class HighLevMouseEventConv(object):
    def __init__(self, eventWindow, t1 = 200, t2 = 100, dragThreshPixel = 3):
        """HighLevelEvent(eventWindow, t1 = 200, t2 = 100, dragThreshPixel = 3)

        eventWindow: The window that receives mouse events.
        t1, t2: How long it waits to detect is a mouse down is a double click in mili seconds.
        dragThreshPixel: Drag moves within this thresh do not produce drag related events.

        When you want a popup menu associated with the window create a func to show the menu, register
        it using set(Left, Middle, Right)PopupMenuFunc(). These functions should never return value.
        """
        self.t1 = t1
        self.t2 = t2
        self.dragThreshPixel = dragThreshPixel
        self.state = NORMAL
        self.evwin = eventWindow

        self.firstTimer = wx.Timer(eventWindow, ID_FIRSTTIMER)
        self.secondTimer = wx.Timer(eventWindow, ID_SECONDTIMER)

        dh = self.defaultHandler
        l, r, m = wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_RIGHT, wx.MOUSE_BTN_MIDDLE

        self.onHLVMouseDown          = {l:dh, r:dh, m:dh}
        self.onHLVMouseUp            = {l:dh, r:dh, m:dh}
        self.onHLVMouseDClick        = {l:dh, r:dh, m:dh}
        self.onHLVMouseStartDragging = {l:dh, r:dh, m:dh}
        self.onHLVMouseDragging      = {l:dh, r:dh, m:dh}
        self.onHLVMouseEndDragging   = {l:dh, r:dh, m:dh}
        self.popupMenuFunc           = {l:dh, r:dh, m:dh}

        self.onHLVMotion      = dh
        self.onHLVEnterWindow = dh
        self.onHLVLeaveWindow = dh

        eventWindow.Bind(wx.EVT_MOUSE_EVENTS, self.onMouseEvents)
        eventWindow.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onMouseCaptureLost)
        eventWindow.Bind(wx.EVT_TIMER, self.onFirstTimer, id = ID_FIRSTTIMER)
        eventWindow.Bind(wx.EVT_TIMER, self.onSecondTimer, id = ID_SECONDTIMER)


    def setOnHLVLeftDown(self, handler):
        self.onHLVMouseDown[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVLeftUp(self, handler):
        self.onHLVMouseUp[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVLeftDClick(self, handler):
        self.onHLVMouseDClick[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVLeftStartDragging(self, handler):
        self.onHLVMouseStartDragging[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVLeftDragging(self, handler):
        self.onHLVMouseDragging[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVLeftEndDragging(self, handler):
        self.onHLVMouseEndDragging[wx.MOUSE_BTN_LEFT] = handler

    def setOnHLVMiddleDown(self, handler):
        self.onHLVMouseDown[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVMiddleUp(self, handler):
        self.onHLVMouseUp[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVMiddleDClick(self, handler):
        self.onHLVMouseDClick[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVMiddleStartDragging(self, handler):
        self.onHLVMouseStartDragging[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVMiddleDragging(self, handler):
        self.onHLVMouseDragging[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVMiddleEndDragging(self, handler):
        self.onHLVMouseEndDragging[wx.MOUSE_BTN_MIDDLE] = handler

    def setOnHLVRightDown(self, handler):
        self.onHLVMouseDown[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVRightUp(self, handler):
        self.onHLVMouseUp[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVRightDClick(self, handler):
        self.onHLVMouseDClick[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVRightStartDragging(self, handler):
        self.onHLVMouseStartDragging[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVRightDragging(self, handler):
        self.onHLVMouseDragging[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVRightEndDragging(self, handler):
        self.onHLVMouseEndDragging[wx.MOUSE_BTN_RIGHT] = handler

    def setOnHLVMotion(self, handler):
        self.onHLVMotion = handler

    def setOnHLVEnterWindow(self, handler):
        """It can detect the mouse enter/leaves from, to the parent window when the mouse is dragging.
        There's no clear way but detecting mouse motion event to enter/leave the window itself
        while the mouse is captured to the parent"""
        self.onHLVEnterWindow = handler

    def setOnHLVLeaveWindow(self, handler):
        """See setOnHLVLeaveWindow doc."""
        self.onHLVLeaveWindow = handler

    #popupMenu
    def setLeftPopupMenuFunc(self, func):
        self.popupMenuFunc[wx.MOUSE_BTN_LEFT] = func

    def setMiddlePopupMenuFunc(self, func):
        self.popupMenuFunc[wx.MOUSE_BTN_MIDDLE] = func

    def setRightPopupMenuFunc(self, func):
        self.popupMenuFunc[wx.MOUSE_BTN_RIGHT] = func


    def defaultHandler(self, ev):
        return True

    def onButtonDown(self, ev):
        if not self.popupMenuFunc[ev.GetButton()](ev):
            return

        if self.state == AFTERDCLICK:
            self.state = NORMAL
        self.evwin.CaptureMouse()

        #On GTK, I knoticed if the window is a control, its parent gets mouse events when the window
        #captures mouse events with CaptureMouse(). To call ReleaseMouse() properly this object receives
        #the mouse leftup events sent to the parent window temporarily.
        parent = self.evwin.GetParent()
        if parent:
            parent.Bind(wx.EVT_LEFT_UP, self.onParentButtonUp)
            parent.Bind(wx.EVT_MIDDLE_UP, self.onParentButtonUp)
            parent.Bind(wx.EVT_RIGHT_UP, self.onParentButtonUp)
            parent.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onMouseCaptureLost)
            parent.Bind(wx.EVT_MOTION, self.onParentMouseEvents)
            parent.Bind(wx.EVT_ENTER_WINDOW, self.onParentEntering)
            parent.Bind(wx.EVT_LEAVE_WINDOW, self.onParentLeaving)

        self.secondTimer.Stop()
        state = self.state
        if state == NORMAL:
            self.button = ev.GetButton()
            self.state = FIRSTWAIT
            self.posx, self.posy = ev.GetPosition()
            self.downevattrs = self._getEventAttrs(ev)
            self.firstTimer.Start(self.t1, True)
        elif state == SECONDWAIT:
            self.state = AFTERDCLICK
            self.onHLVMouseDClick[self.button](ev)

    def onButtonUp(self, ev):
        evwin = self.evwin
        if evwin.HasCapture():
             evwin.ReleaseMouse()
        parent = evwin.GetParent()
        if parent:
            parent.Unbind(wx.EVT_LEFT_UP)
            parent.Unbind(wx.EVT_MIDDLE_UP)
            parent.Unbind(wx.EVT_RIGHT_UP)
            parent.Unbind(wx.EVT_MOUSE_CAPTURE_LOST)
            parent.Unbind(wx.EVT_MOTION)
            parent.Unbind(wx.EVT_ENTER_WINDOW)
            parent.Unbind(wx.EVT_LEAVE_WINDOW)

        self.firstTimer.Stop()
        state = self.state
        if state == FIRSTWAIT:
            self.state = SECONDWAIT
            self.upevattrs = self._getEventAttrs(ev)
            self.secondTimer.Start(self.t2, True)
        elif state == AFTERDCLICK:
            self.state = NORMAL
        elif state == MOUSEDOWN:
            self.state = NORMAL
            self.onHLVMouseUp[self.button](ev)
        elif state == DRAGGING:
            self.state = NORMAL
            self.onHLVMouseEndDragging[self.button](ev)
            self.onHLVMouseUp[self.button](ev)

    def onButtonDClick(self, ev):
        self.secondTimer.Stop()
        state = self.state
        if state == NORMAL:
            self.onButtonDown(ev)
        elif state == SECONDWAIT:
            self.state = AFTERDCLICK
            self.onHLVMouseDClick[self.button](ev)

    def onMoving(self, ev):
        if self.state == NORMAL:
            self.onHLVMotion(ev)

    def onDragging(self, ev):
        x, y = ev.GetPosition()
        t = self.dragThreshPixel
        state = self.state
        if state == FIRSTWAIT:
            if -t < x - self.posx < t and -t < y - self.posy < t:
                return
            else:
                self.state = DRAGGING
                self.firstTimer.Stop()
                self.onHLVMouseDown[self.button](ev)
                self.onHLVMouseStartDragging[self.button](ev)
        elif state == MOUSEDOWN:
            if -t < x - self.posx < t and -t < y - self.posy < t:
                return
            else:
                self.state = DRAGGING
                self.onHLVMouseStartDragging[self.button](ev)
        elif state == DRAGGING:
            self.onHLVMouseDragging[self.button](ev)

    def onEntering(self, ev):
        self.onHLVEnterWindow(ev)

    def onLeaving(self, ev):
        self.onHLVLeaveWindow(ev)


    def onMouseEvents(self, ev):
        ev.Skip()
        if ev.Moving():
            return self.onMoving(ev)
        elif ev.Dragging():
            return self.onDragging(ev)
        elif ev.ButtonDown():
            return self.onButtonDown(ev)
        elif ev.ButtonUp():
            return self.onButtonUp(ev)
        elif ev.ButtonDClick():
            return self.onButtonDClick(ev)
        elif ev.Entering():
            return self.onEntering(ev)
        elif ev.Leaving():
            return self.onLeaving(ev)

    def onMouseCaptureLost(self, ev):
        self.evwin.ReleaseMouse()
        ev.Skip()

    def onFirstTimer(self, ev):
        self.state = MOUSEDOWN
        downev = self._createMouseEvent(self.downevattrs)
        self.onHLVMouseDown[self.button](downev)

    def onSecondTimer(self, ev):
        self.state = NORMAL
        downev = self._createMouseEvent(self.downevattrs)
        upev = self._createMouseEvent(self.upevattrs)
        self.onHLVMouseDown[self.button](downev)
        self.onHLVMouseUp[self.button](upev)

    def onParentButtonUp(self, ev):
        self._convetMouseEvent(ev)
        self.onButtonUp(ev)
    def onParentMouseEvents(self, ev):
        self._convetMouseEvent(ev)
        self.onMouseEvents(ev)
    def onParentEntering(self, ev):
        self._convetMouseEvent(ev)
        self.onEntering(ev)
    def onParentLeaving(self, ev):
        self._convetMouseEvent(ev)
        self.onLeaving(ev)

    def _convetMouseEvent(self, ev):
        evwin = self.evwin
        parent = evwin.GetParent()
        x, y = ev.GetPosition()
        offsetx, offsety = evwin.GetPosition()
        ev.m_x = x - offsetx
        ev.m_y = y - offsety
        ev.SetEventObject(evwin)

    def _getEventAttrs(self, ev):
        return [
            ev.GetEventObject(),
            ev.GetEventType(),
            ev.GetId(),
            ev.GetTimestamp(),
            ev.m_altDown,
            ev.m_controlDown,
            ev.m_leftDown,
            ev.m_middleDown,
            ev.m_rightDown,
            ev.m_metaDown,
            ev.m_shiftDown,
            ev.m_x,
            ev.m_y,
            ev.m_wheelRotation,
            ev.m_wheelDelta,
            ev.m_linesPerAction]

    def _createMouseEvent(self, eventAttrs):
        obj, typ, id, time, alt, ctrl, l, m, r, meta, shift, x, y, wr, wd, lpa = eventAttrs
        ev = wx.MouseEvent(typ)
        ev.SetEventObject(obj)
        ev.GetEventObject()
        ev.SetEventType(typ)
        ev.SetId(id)
        ev.SetTimestamp(time)
        ev.m_altDown = alt
        ev.m_controlDown = ctrl
        ev.m_leftDown = l
        ev.m_middleDown = m
        ev.m_rightDown = r
        ev.m_metaDown = meta
        ev.m_shiftDown = shift
        ev.m_x = x
        ev.m_y = y
        ev.m_wheelRotation = wr
        ev.m_wheelDelta = wd
        ev.m_linesPerAction = lpa
        return ev


if __name__ == '__main__':
    class TestWindow(wx.Button):
        def __init__(self, parent):
            super(TestWindow, self).__init__(parent, -1, pos = (50, 50), size = (150, 150))

            hlev = HighLevMouseEventConv(self, dragThreshPixel = 15)

            hlev.setOnHLVLeftDown(self.onHLVLeftDown)
            hlev.setOnHLVLeftUp(self.onHLVLeftUp)
            hlev.setOnHLVLeftDClick(self.onHLVLeftDClick)
            hlev.setOnHLVLeftStartDragging(self.onHLVLeftStartDragging)
            hlev.setOnHLVLeftDragging(self.onHLVLeftDragging)
            hlev.setOnHLVLeftEndDragging(self.onHLVLeftEndDragging)
            hlev.setOnHLVMiddleDown(self.onHLVMiddleDown)
            hlev.setOnHLVMiddleUp(self.onHLVMiddleUp)
            hlev.setOnHLVMiddleDClick(self.onHLVMiddleDClick)
            hlev.setOnHLVMiddleStartDragging(self.onHLVMiddleStartDragging)
            hlev.setOnHLVMiddleDragging(self.onHLVMiddleDragging)
            hlev.setOnHLVMiddleEndDragging(self.onHLVMiddleEndDragging)
            hlev.setOnHLVRightDown(self.onHLVRightDown)
            hlev.setOnHLVRightUp(self.onHLVRightUp)
            hlev.setOnHLVRightDClick(self.onHLVRightDClick)
            hlev.setOnHLVRightStartDragging(self.onHLVRightStartDragging)
            hlev.setOnHLVRightDragging(self.onHLVRightDragging)
            hlev.setOnHLVRightEndDragging(self.onHLVRightEndDragging)
            hlev.setOnHLVMotion(self.onHLVMotion)
            hlev.setOnHLVEnterWindow(self.onHLVEnterWindow)
            hlev.setOnHLVLeaveWindow(self.onHLVLeaveWindow)

            self.lup = True
            self.dragstate = 'normal'

        def onHLVLeftDown(self, ev):
            print 'onHLVLeftDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
            if not self.lup: print 'ERROR ' * 5
            self.lup = False
        def onHLVLeftUp(self, ev):
            print 'onHLVLeftUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
            self.lup = True
        def onHLVLeftDClick(self, ev):
            print 'onHLVLeftDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVLeftStartDragging(self, ev):
            print 'onHLVLeftStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
            if self.dragstate != 'normal': print 'ERROR ' * 5
            self.dragstate = 'started'
        def onHLVLeftDragging(self, ev):
            print 'onHLVLeftDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
            if not self.dragstate in ('started', 'dragging'): print 'ERROR ' * 5
            self.dragstate = 'dragging'
        def onHLVLeftEndDragging(self, ev):
            print 'onHLVLeftEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
            if not self.dragstate in ('started', 'dragging'): print 'ERROR ' * 5
            self.dragstate = 'normal'
        def onHLVMiddleDown(self, ev):
            print 'onHLVMiddleDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMiddleUp(self, ev):
            print 'onHLVMiddleUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMiddleDClick(self, ev):
            print 'onHLVMiddleDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMiddleStartDragging(self, ev):
            print 'onHLVMiddleStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMiddleDragging(self, ev):
            print 'onHLVMiddleDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMiddleEndDragging(self, ev):
            print 'onHLVMiddleEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightDown(self, ev):
            print 'onHLVRightDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightUp(self, ev):
            print 'onHLVRightUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightDClick(self, ev):
            print 'onHLVRightDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightStartDragging(self, ev):
            print 'onHLVRightStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightDragging(self, ev):
            print 'onHLVRightDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVRightEndDragging(self, ev):
            print 'onHLVRightEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVMotion(self, ev):
            print 'onHLVMotion', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVEnterWindow(self, ev):
            print 'onHLVEnterWindow', ev.GetPosition(), type(ev.GetEventObject()).__name__
        def onHLVLeaveWindow(self, ev):
            print 'onHLVLeaveWindow', ev.GetPosition(), type(ev.GetEventObject()).__name__


    app = wx.App()
    frame = wx.Frame(None, -1, "Title", size = (300, 400))
    panel = wx.Panel(frame, -1)
    testwin = TestWindow(panel)
    frame.Show()
    app.MainLoop()

No comments: