# Copyright 2018 The dm_control Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ """Utilities for handling keyboard events.""" import collections # Mapped input values, so that we don't have to reference glfw everywhere. RELEASE = 0 PRESS = 1 REPEAT = 2 KEY_UNKNOWN = -1 KEY_SPACE = 32 KEY_APOSTROPHE = 39 KEY_COMMA = 44 KEY_MINUS = 45 KEY_PERIOD = 46 KEY_SLASH = 47 KEY_0 = 48 KEY_1 = 49 KEY_2 = 50 KEY_3 = 51 KEY_4 = 52 KEY_5 = 53 KEY_6 = 54 KEY_7 = 55 KEY_8 = 56 KEY_9 = 57 KEY_SEMICOLON = 59 KEY_EQUAL = 61 KEY_A = 65 KEY_B = 66 KEY_C = 67 KEY_D = 68 KEY_E = 69 KEY_F = 70 KEY_G = 71 KEY_H = 72 KEY_I = 73 KEY_J = 74 KEY_K = 75 KEY_L = 76 KEY_M = 77 KEY_N = 78 KEY_O = 79 KEY_P = 80 KEY_Q = 81 KEY_R = 82 KEY_S = 83 KEY_T = 84 KEY_U = 85 KEY_V = 86 KEY_W = 87 KEY_X = 88 KEY_Y = 89 KEY_Z = 90 KEY_LEFT_BRACKET = 91 KEY_BACKSLASH = 92 KEY_RIGHT_BRACKET = 93 KEY_GRAVE_ACCENT = 96 KEY_ESCAPE = 256 KEY_ENTER = 257 KEY_TAB = 258 KEY_BACKSPACE = 259 KEY_INSERT = 260 KEY_DELETE = 261 KEY_RIGHT = 262 KEY_LEFT = 263 KEY_DOWN = 264 KEY_UP = 265 KEY_PAGE_UP = 266 KEY_PAGE_DOWN = 267 KEY_HOME = 268 KEY_END = 269 KEY_CAPS_LOCK = 280 KEY_SCROLL_LOCK = 281 KEY_NUM_LOCK = 282 KEY_PRINT_SCREEN = 283 KEY_PAUSE = 284 KEY_F1 = 290 KEY_F2 = 291 KEY_F3 = 292 KEY_F4 = 293 KEY_F5 = 294 KEY_F6 = 295 KEY_F7 = 296 KEY_F8 = 297 KEY_F9 = 298 KEY_F10 = 299 KEY_F11 = 300 KEY_F12 = 301 KEY_KP_0 = 320 KEY_KP_1 = 321 KEY_KP_2 = 322 KEY_KP_3 = 323 KEY_KP_4 = 324 KEY_KP_5 = 325 KEY_KP_6 = 326 KEY_KP_7 = 327 KEY_KP_8 = 328 KEY_KP_9 = 329 KEY_KP_DECIMAL = 330 KEY_KP_DIVIDE = 331 KEY_KP_MULTIPLY = 332 KEY_KP_SUBTRACT = 333 KEY_KP_ADD = 334 KEY_KP_ENTER = 335 KEY_KP_EQUAL = 336 KEY_LEFT_SHIFT = 340 KEY_LEFT_CONTROL = 341 KEY_LEFT_ALT = 342 KEY_LEFT_SUPER = 343 KEY_RIGHT_SHIFT = 344 KEY_RIGHT_CONTROL = 345 KEY_RIGHT_ALT = 346 KEY_RIGHT_SUPER = 347 MOD_NONE = 0 MOD_SHIFT = 0x0001 MOD_CONTROL = 0x0002 MOD_ALT = 0x0004 MOD_SUPER = 0x0008 MOD_SHIFT_CONTROL = MOD_SHIFT | MOD_CONTROL MOUSE_BUTTON_LEFT = 0 MOUSE_BUTTON_RIGHT = 1 MOUSE_BUTTON_MIDDLE = 2 _NO_EXCLUSIVE_KEY = (None, lambda _: None) _NO_CALLBACK = (None, None) class Exclusive(collections.namedtuple('Exclusive', 'combination')): """Defines an exclusive action. Exclusive actions can be invoked in response to single key clicks only. The callback will be called twice. The first time when the key combination is pressed, passing True as the argument to the callback. The second time when the key is released (the modifiers don't have to be present then), passing False as the callback argument. Attributes: combination: A list of integers interpreted as key codes, or tuples in format (keycode, modifier). """ pass class DoubleClick(collections.namedtuple('DoubleClick', 'combination')): """Defines a mouse double click action. It will define a requirement to double click the mouse button specified in the combination in order to be triggered. Attributes: combination: A list of integers interpreted as key codes, or tuples in format (keycode, modifier). The keycodes are limited only to mouse button codes. """ pass class Range(collections.namedtuple('Range', 'collection')): """Binds a number of key combinations to a callback. When triggered, the index of the triggering key combination will be passed as an argument to the callback. Attributes: callback: A callable accepting a single argument - an integer index of the triggered callback. collection: A collection of combinations. Combinations may either be raw key codes, tuples in format (keycode, modifier), or one of the Exclusive or DoubleClick instances. """ pass class InputMap: """Provides ability to alias key combinations and map them to actions.""" def __init__(self, mouse, keyboard): """Instance initializer. Args: mouse: GlfwMouse instance. keyboard: GlfwKeyboard instance. """ self._keyboard = keyboard self._mouse = mouse self._keyboard.on_key += self._handle_key self._mouse.on_click += self._handle_key self._mouse.on_double_click += self._handle_double_click self._mouse.on_move += self._handle_mouse_move self._mouse.on_scroll += self._handle_mouse_scroll self.clear_bindings() def __del__(self): """Instance deleter.""" self._keyboard.on_key -= self._handle_key self._mouse.on_click -= self._handle_key self._mouse.on_double_click -= self._handle_double_click self._mouse.on_move -= self._handle_mouse_move self._mouse.on_scroll -= self._handle_mouse_scroll def clear_bindings(self): """Clears registered action bindings, while keeping key aliases.""" self._action_callbacks = {} self._double_click_callbacks = {} self._plane_callback = [] self._z_axis_callback = [] self._active_exclusive = _NO_EXCLUSIVE_KEY def bind(self, callback, key_binding): """Binds a key combination to a callback. Args: callback: An argument-less callable. key_binding: A integer with a key code, a tuple (keycode, modifier) or one of the actions Exclusive|DoubleClick|Range carrying the key combination. """ def build_callback(index, callback): def indexed_callback(): callback(index) return indexed_callback if isinstance(key_binding, Range): for index, binding in enumerate(key_binding.collection): self._add_binding(build_callback(index, callback), binding) else: self._add_binding(callback, key_binding) def _add_binding(self, callback, key_binding): key_combination = self._extract_key_combination(key_binding) if isinstance(key_binding, Exclusive): self._action_callbacks[key_combination] = (True, callback) elif isinstance(key_binding, DoubleClick): self._double_click_callbacks[key_combination] = callback else: self._action_callbacks[key_combination] = (False, callback) def _extract_key_combination(self, key_binding): if isinstance(key_binding, Exclusive): key_binding = key_binding.combination elif isinstance(key_binding, DoubleClick): key_binding = key_binding.combination if not isinstance(key_binding, tuple): key_binding = (key_binding, MOD_NONE) return key_binding def bind_plane(self, callback): """Binds a callback to a planar motion action (mouse movement).""" self._plane_callback.append(callback) def bind_z_axis(self, callback): """Binds a callback to a z-axis motion action (mouse scroll).""" self._z_axis_callback.append(callback) def _handle_key(self, key, action, modifiers): """Handles a single key press (mouse and keyboard).""" alias_key = (key, modifiers) exclusive_key, exclusive_callback = self._active_exclusive if exclusive_key is not None: if action == RELEASE and key == exclusive_key: exclusive_callback(False) self._active_exclusive = _NO_EXCLUSIVE_KEY else: is_exclusive, callback = self._action_callbacks.get( alias_key, _NO_CALLBACK) if callback: if action == PRESS: if is_exclusive: callback(True) self._active_exclusive = (key, callback) else: callback() def _handle_double_click(self, key, modifiers): """Handles a double mouse click.""" alias_key = (key, modifiers) callback = self._double_click_callbacks.get(alias_key, None) if callback is not None: callback() def _handle_mouse_move(self, position, translation): """Handles mouse move.""" for callback in self._plane_callback: callback(position, translation) def _handle_mouse_scroll(self, value): """Handles mouse wheel scroll.""" for callback in self._z_axis_callback: callback(value)