# Copyright (C) 2008-2012 Mortal Coil Games # See LICENSE for details. """ Input events and related objects @author: judd """ from mv3d.util.enum import Enum from mv3d.util.math2d import Vector2 class Keys(Enum): """ Defines all available keys """ backspace = 8 tab = 9 return_ = 10 enter = 13 escape = 27 space = 32 apostrophe = 39 plus = 43 comma = 44 minus = 45 period = 46 slash = 47 number0 = 48 number1 = 49 number2 = 50 number3 = 51 number4 = 52 number5 = 53 number6 = 54 number7 = 55 number8 = 56 number9 = 57 semicolon = 59 a = 65 b = 66 c = 67 d = 68 e = 69 f = 70 g = 71 h = 72 i = 73 j = 74 k = 75 l = 76 m = 77 n = 78 o = 79 p = 80 q = 81 r = 82 s = 83 t = 84 u = 85 v = 86 w = 87 x = 88 y = 89 z = 90 openBracket = 91 backSlash = 92 closeBracket = 93 tilde = 96 delete = 127 up = 256 down = 257 left = 258 right = 259 capsLock = 260 leftShift = 261 leftCtrl = 262 leftAlt = 263 f1 = 264 f2 = 265 f3 = 266 f4 = 267 f5 = 268 f6 = 269 f7 = 270 f8 = 271 f9 = 272 f10 = 273 f11 = 274 f12 = 275 rightShift = 276 rightCtrl = 277 rightAlt = 278 insert = 279 home = 280 end = 281 pageUp = 282 pageDown = 283 printScreen = 284 scrollLock = 285 break_ = 286 numLock = 287 shift = 288 ctrl = 289 alt = 290 application = 291 leftWindows = 292 rightWindows = 293 class MouseButton(Enum): left = 0 right = 1 middle = 2 button4 = 3 button5 = 4 class MouseButtonState(Enum): doubleClick = 2 down = 1 up = -1 none = 0 class KeyModifiers(object): _all = ('shift', 'ctrl', 'alt', 'meta', 'cmd') shift = False ctrl = False alt = False meta = False cmd = False def __dict__(self): mods = {} for mod in self._all: mods[mod] = getattr(self, mod) return mods def __getitem__(self, name): if name in self._all: return getattr(self, name) def __setitem__(self, name, value): if name in self._all: setattr(self, name, value) def __setattr__(self, name, value): assert isinstance(value, bool) object.__setattr__(self, name, value) class InputException(Exception): """ Input-related errors """ class InputEvent(object): """ MV3D Generic Input Event """ mods = None @classmethod def fromWxEvent(cls, evt): """ Convert from a wx event to an MV3D event. """ @classmethod def fromPandaEvent(cls, evt): """ Convert from a Panda event to an MV3D event. """ @classmethod def fromOISEvent(cls, evt): """ Convert from an OIS event to an MV3D event. """ def _wxGetMods(self, wx): """ Gets the keyboard modifier data from wxPython """ self.mods = KeyModifiers() self.mods.shift = wx.GetKeyState(wx.WXK_SHIFT) self.mods.ctrl = wx.GetKeyState(wx.WXK_CONTROL) self.mods.alt = wx.GetKeyState(wx.WXK_ALT) self.mods.cmd = wx.GetKeyState(wx.WXK_COMMAND) def _pandaGetMods(self, mods): """ Converts a panda mods prefix string to MV3Ds KeyModifiers object """ self.mods = KeyModifiers() self.mods.shift = "shift" in mods self.mods.ctrl = "control" in mods self.mods.alt = "alt" in mods def _oisGetMods(self, keyboard, OIS): """ Converts ois mods to MV3D KeyModifiers """ self.mods = KeyModifiers() self.mods.ctrl = keyboard.isModifierDown(OIS.Keyboard.Ctrl) self.mods.alt = keyboard.isModifierDown(OIS.Keyboard.Alt) self.mods.shift = keyboard.isModifierDown(OIS.Keyboard.Shift) class CEGUIPosition: def __init__(self, abs_, rel): self.abs = abs_ self.rel = rel def __str__(self): return "a: %d r: %d" % (self.abs, self.rel) class EventSource(Enum): """ Defines sources for event """ constructed = 0 wx = 1 ois = 2 panda = 3 class MouseEvent(InputEvent): """ MV3D Mouse Event For drag events, a mouse button being held is the boolean buttonIsDown. """ pos = None rel = None leftIsDown = False middleIsDown = False rightIsDown = False leftDown = False middleDown = False rightDown = False leftUp = False rightUp = False middleUp = False left = MouseButtonState.none middle = MouseButtonState.none right = MouseButtonState.none wheelDelta = -1 wheelRotation = -1 width = 0 height = 0 source = EventSource.constructed _lastEventID = None _wxDragging = None _lastEvent = {} _buttons = 0 def __str__(self): return "" % (self.pos, MouseButtonState[self.left], MouseButtonState[self.middle], MouseButtonState[self.right], self.wheelDelta) @classmethod def fromWxEvent(cls, wxEvt, previousEvent=None, wx=None): """ Creates a MouseEvent instance from wx.Event data """ evt = cls() evt.source = EventSource.wx if previousEvent is not None: cls._lastEvent[previousEvent.source] = previousEvent evt.pos = Vector2(wxEvt.GetPositionTuple()) obj = wxEvt.GetEventObject() if hasattr(obj, "GetClientSizeTuple"): evt.width, evt.height = obj.GetClientSizeTuple() evt.leftIsDown = wxEvt.LeftIsDown() evt.middleIsDown = wxEvt.MiddleIsDown() evt.rightIsDown = wxEvt.RightIsDown() evt.leftDown = wxEvt.LeftDown() evt.middleDown = wxEvt.MiddleDown() evt.rightDown = wxEvt.RightDown() evt.leftUp = wxEvt.LeftUp() evt.middleUp = wxEvt.MiddleUp() evt.rightUp = wxEvt.RightUp() evt.wheelRotation = wxEvt.GetWheelRotation() evt.wheelDelta = wxEvt.GetWheelDelta() if wxEvt.LeftDown(): evt.left = MouseButtonState.down elif wxEvt.LeftDClick(): evt.left = MouseButtonState.doubleClick elif wxEvt.LeftUp(): evt.left = MouseButtonState.up else: evt.left = MouseButtonState.none if wxEvt.MiddleDown(): evt.middle = MouseButtonState.down elif wxEvt.LeftDClick(): evt.middle = MouseButtonState.doubleClick elif wxEvt.MiddleUp(): evt.middle = MouseButtonState.up else: evt.middle = MouseButtonState.none if wxEvt.RightDown(): evt.right = MouseButtonState.down elif wxEvt.RightDClick(): evt.right = MouseButtonState.doubleClick elif wxEvt.RightUp(): evt.right = MouseButtonState.up else: evt.right = MouseButtonState.none evt._wxGetMods(wx) evt._setRelative() return evt @classmethod def fromPandaEvent(cls, renderer, wheel, buttons, window): """ Creates a MouseEvent instance from panda Event data """ if window is None: window = renderer.base.win evt = cls() evt.source = EventSource.panda mpos = window.getPointer(0) # get the mouse position evt.pos = Vector2(mpos.getX(), mpos.getY()) evt._buttons = buttons lastButtons = 0 if cls._lastEvent.has_key(EventSource.panda): lastButtons = cls._lastEvent[EventSource.panda]._buttons evt.leftIsDown = buttons & 1 evt.middleIsDown = buttons & 2 evt.rightIsDown = buttons & 4 if buttons & 1 and not lastButtons & 1: evt.left = MouseButtonState.down evt.leftDown = True elif not buttons & 1 and lastButtons & 1: evt.left = MouseButtonState.up evt.leftUp = True else: evt.left = MouseButtonState.none if buttons & 2 and not lastButtons & 2: evt.middle = MouseButtonState.down evt.middleDown = True elif not buttons & 2 and lastButtons & 2: evt.middle = MouseButtonState.up evt.middleUp = True else: evt.middle = MouseButtonState.none if buttons & 4 and not lastButtons & 4: evt.right = MouseButtonState.down evt.rightDown = True elif not buttons & 4 and lastButtons & 4: evt.right = MouseButtonState.up evt.rightUp = True else: evt.right = MouseButtonState.none # wheel rotation should be + for wheel up and - for wheel down # (panda does the reverse) evt.wheelRotation = -wheel evt.wheelDelta = abs(wheel) evt._setRelative() return evt @classmethod def fromOISEvent(cls, event, renderer): """ Creates a MouseEvent instance from OIS Event data """ mouse = renderer.mouse keyboard = renderer.keyboard evt = cls() evt.source = EventSource.ois event = event.get_state() lastButtons = 0 if cls._lastEvent.has_key(EventSource.ois): lastButtons = cls._lastEvent[EventSource.ois]._buttons evt.pos = (event.X.abs / float(event.width), event.Y.abs / float(event.height)) evt.rel = (event.X.rel, event.Y.rel) buttons = mouse.getMouseState().buttons evt._buttons = buttons evt.leftIsDown = buttons & 1 evt.middleIsDown = buttons & 4 evt.rightIsDown = buttons & 2 if buttons & 1 and not lastButtons & 1: evt.left = MouseButtonState.down evt.leftDown = True elif not buttons & 1 and lastButtons & 1: evt.left = MouseButtonState.up evt.leftUp = True else: evt.left = MouseButtonState.none if buttons & 4 and not lastButtons & 4: evt.middle = MouseButtonState.down evt.middleDown = True elif not buttons & 4 and lastButtons & 4: evt.middle = MouseButtonState.up evt.middleUp = True else: evt.middle = MouseButtonState.none if buttons & 2 and not lastButtons & 2: evt.right = MouseButtonState.down evt.rightDown = True elif not buttons & 2 and lastButtons & 2: evt.right = MouseButtonState.up evt.rightUp = True else: evt.right = MouseButtonState.none evt.wheelRotation = event.Z.rel evt.wheelDelta = abs(event.Z.rel) evt.buttons = buttons evt._oisGetMods(keyboard, renderer.OIS) cls._lastEvent[evt.source] = evt return evt def _setRelative(self): """ Sets the relative movement """ cls = self.__class__ evt = cls._lastEvent.get(self.source) if evt is not None: assert evt.source == self.source self.rel = (self.pos[0] - evt.pos[0], self.pos[1] - evt.pos[1]) else: self.rel = self.pos cls._lastEvent[self.source] = self def dragging(self): """ Returns True if the mouse is being dragged, otherwise False """ return (self.rel != (0, 0) and (self.leftIsDown or self.middleIsDown or self.rightIsDown)) def relativePosTo(self, pos): """ Returns the relative mouse movement between this event and another, or a list containing two ints in the form (x, y) """ if self.rel is not None: return self.rel if pos == self or pos == None: return Vector2(0, 0) if type(pos) == list or type(pos) == tuple: offset = Vector2(pos[0], pos[1]) elif isinstance(pos, MouseEvent): offset = Vector2(pos.pos[0], pos.pos[1]) else: raise TypeError() return self.pos - offset def buttonStillDown(self, event, button): """ Returns True if a button in this event is down and a button in another event is also down """ if not isinstance(button, MouseButton): raise ValueError("Expected MouseButton enum") if button == MouseButton.left: return (self.left == MouseButtonState.down and event.left == MouseButtonState.down) elif button == MouseButton.right: return (self.right == MouseButtonState.down and event.right == MouseButtonState.down) elif button == MouseButton.middle: return (self.middle == MouseButtonState.down and event.middle == MouseButtonState.down) else: # getting here requires support for extra mousebuttons, # which we don't handle yet return False def toOISEvent(self, renderer): """ Convert this event into an OIS event. """ return OISEvent(pos=self.pos, rel=self.rel, size=(self.width, self.height), wheelRotation=self.wheelRotation, wheelDelta=self.wheelDelta) class OISEvent(object): """ Implement our own version of an OIS Event so that we can masquerade as a real event. """ def __init__(self, key=None, text=None, pos=None, rel=None, size=None, wheelRotation=0, wheelDelta=0): self.key = key self.text = text self.pos = pos self.rel = rel self.wheelRotation = wheelRotation self.wheelDelta = wheelDelta if size is not None: self.width = size[0] self.height = size[1] def get_state(self): """ For an OIS mouse event """ return self @property def X(self): """ For an OIS mouse event """ return CEGUIPosition(self.pos[0], self.rel[0]) @property def Y(self): """ For an OIS mouse event """ return CEGUIPosition(self.pos[1], self.rel[1]) @property def Z(self): """ For an OIS mouse event """ return CEGUIPosition(self.wheelRotation, self.wheelDelta) class KeyEvent(InputEvent): """ MV3D Key Event """ keyDown = False keyUp = False key = None # The key from the Keys enum text = "" # The text this key would generate or "" alpha = "abcdefghijklmnopqrstuvwxyz" allText = (alpha + alpha.upper() + "1234567890" "!@#$%^&*()`~_+[];',./-=\\,./;'[] ") shiftMap = dict(zip("abcdefghijklmnopqrstuvwxyz`1234567890-=[]\\;',./", "ABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+{}|:\"<>?")) reverseShiftMap = dict(zip(shiftMap.values(), shiftMap.keys())) @classmethod def fromWxEvent(cls, wxEvt, wxKeyMap, wx): """ Creates a KeyEvent instance from wx.Event data """ evt = cls() if wxEvt.EventType == wx.EVT_KEY_DOWN.typeId: evt.keyDown = True elif wxEvt.EventType == wx.EVT_KEY_UP.typeId: evt.keyUp = True evt.key = wxKeyMap[wxEvt.GetKeyCode()] evt._wxGetMods(wx) evt._getText() return evt @classmethod def fromPandaEvent(cls, key, mods, suffix, renderer): """ Creates a KeyEvent instance from Panda event data """ evt = cls() evt.key = renderer.pandaKeyMap[key] evt._pandaGetMods(mods) if suffix == "-up": evt.keyUp = True elif suffix != "-repeat": evt.keyDown = True evt._getText() return evt @classmethod def fromOISEvent(cls, event, isDown, renderer): """ Creates a KeyEvent from an OIS event. """ evt = cls() evt.key = renderer.oisKeyMap[event.key] evt.keyDown = isDown evt._oisGetMods(renderer.keyboard, renderer.OIS) evt._getText() return evt def __repr__(self): return "" % ( self.text, self.keyDown, self.mods.shift, self.mods.ctrl, self.mods.alt, hex(self.__hash__())) def _getText(self): """ Figure out the text of the key """ if self.key < 256 and chr(self.key) in self.allText: self.text = chr(self.key) if not self.mods.shift: self.text = self.reverseShiftMap.get(self.text, self.text) else: self.text = self.shiftMap.get(self.text, self.text) def toOISEvent(self, renderer): """ Convert keycode into OIS """ if self.text != "": text = ord(self.text) else: text = 0 key = renderer.oisReverseKeyMap[self.key] return OISEvent(key, text) def getShifted(self, char): """ Convert to the shifted version """ return self.shiftMap.get(char, char) def getUnshifted(self, char): """ Convert to the shifted version """ return self.reverseShiftMap.get(char, char)