''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import re import time import inspect from copy import deepcopy import bpy from .debug import dprint from .decorators import blender_version_wrapper from .human_readable import convert_actions_to_human_readable, convert_human_readable_to_actions from .maths import Point2D, Vec2D from .timerhandler import TimerHandler from . import blender_preferences as bprefs ### ### ### The classes here will _eventually_ replace those in useractions.py ### ### ''' copied from: - https://docs.blender.org/api/current/bpy.types.Event.html - https://docs.blender.org/api/current/bpy.types.KeyMapItem.html direction: { 'ANY', 'NORTH', 'NORTH_EAST', 'EAST', 'SOUTH_EAST', 'SOUTH', 'SOUTH_WEST', 'WEST', 'NORTH_WEST' } type: { 'NONE', # System 'WINDOW_DEACTIVATE', # window lost focus (minimized, switch away from, etc.) 'ACTIONZONE_AREA', 'ACTIONZONE_REGION', 'ACTIONZONE_FULLSCREEN', # Mouse 'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE', 'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE', 'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE', 'MOUSEROTATE', 'MOUSESMARTZOOM', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE', 'PEN', 'ERASER', 'TRACKPADPAN', 'TRACKPADZOOM', # Keyboard 'LEFT_CTRL', 'LEFT_ALT', 'LEFT_SHIFT', 'RIGHT_ALT', 'RIGHT_CTRL', 'RIGHT_SHIFT', 'OSKEY', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'SEMI_COLON', 'PERIOD', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'MINUS', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET', 'GRLESS', 'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9', 'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_0', 'NUMPAD_MINUS', 'NUMPAD_ENTER', 'NUMPAD_PLUS', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24', 'PAUSE', 'INSERT', 'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END', 'MEDIA_PLAY', 'MEDIA_STOP', 'MEDIA_FIRST', 'MEDIA_LAST', 'ESC', 'TAB', 'RET', 'SPACE', 'LINE_FEED', 'BACK_SPACE', 'DEL', 'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW', # ??? 'APP', # Text Input 'TEXTINPUT', # Timer 'TIMER', 'TIMER0', 'TIMER1', 'TIMER2', 'TIMER_JOBS', 'TIMER_AUTOSAVE', 'TIMER_REPORT', 'TIMERREGION', # NDOF 'NDOF_MOTION', 'NDOF_BUTTON_MENU', 'NDOF_BUTTON_FIT', 'NDOF_BUTTON_TOP', 'NDOF_BUTTON_BOTTOM', 'NDOF_BUTTON_LEFT', 'NDOF_BUTTON_RIGHT', 'NDOF_BUTTON_FRONT', 'NDOF_BUTTON_BACK', 'NDOF_BUTTON_ISO1', 'NDOF_BUTTON_ISO2', 'NDOF_BUTTON_ROLL_CW', 'NDOF_BUTTON_ROLL_CCW', 'NDOF_BUTTON_SPIN_CW', 'NDOF_BUTTON_SPIN_CCW', 'NDOF_BUTTON_TILT_CW', 'NDOF_BUTTON_TILT_CCW', 'NDOF_BUTTON_ROTATE', 'NDOF_BUTTON_PANZOOM', 'NDOF_BUTTON_DOMINANT', 'NDOF_BUTTON_PLUS', 'NDOF_BUTTON_MINUS', 'NDOF_BUTTON_1', 'NDOF_BUTTON_2', 'NDOF_BUTTON_3', 'NDOF_BUTTON_4', 'NDOF_BUTTON_5', 'NDOF_BUTTON_6', 'NDOF_BUTTON_7', 'NDOF_BUTTON_8', 'NDOF_BUTTON_9', 'NDOF_BUTTON_10', 'NDOF_BUTTON_A', 'NDOF_BUTTON_B', 'NDOF_BUTTON_C', # ??? 'XR_ACTION' } value: { 'ANY', 'PRESS', 'RELEASE', 'CLICK', 'DOUBLE_CLICK', 'CLICK_DRAG', 'NOTHING' } class bpy.types.Event: alt True when the Alt/Option key is held (unless both alt keys pressed and one is released) ascii single ASCII character for this event ctrl True when Ctrl key is held (unless both ctrl keys pressed and one is released) direction drag direction (never used?) is_mouse_absolute last motion event was an absolute input is_repeat event is generated by holding a key down is_tablet event has tablet data mouse_prev_press_x window relative location of the last press event (most recent press) mouse_prev_press_y mouse_prev_x window relative location of mouse (in last event?) mouse_prev_y mouse_region_x region relative location of mouse mouse_region_y mouse_x window relative location of mouse mouse_y oskey True when Cmd key is held pressure pressure of tablet or 1.0 if no tablet present shift True when Shift key is held (unless both shift keys pressed and one is released) tilt pressure (tilt?) of tablet or zeros if no tablet present ([float, float]) type (Type of event?) type_prev: (type of last event?) unicode: single unicode character for this event value: type of event, only applies to some value_prev: type of (last?) event, only applies to some xr: XR event data class bpy.types.KeyMapItem: active True when KMI is active alt Alt key pressed (int), -1 for any state alt_ui (bool) any any modifier keys pressed ctrl Control key pressed (int), -1 for any state ctrl_ui (bool) direction drag direction id ID of item (int [-32768, 32767], default 0) idname identifier of operator to call on input event is_user_defined True if KMI is user defined (doesn't just replace a builtin item) is_user_modified True if KMI is modified by user key_modifier Regular key pressed as a modifier (see type above) map_type type of event mapping, { 'KEYBOARD', 'MOUSE', 'NDOF', 'TEXTINPUT', 'TIMER' } name name of operator (translated) to call on input event oskey Operating System Key pressed (int), -1 for any state oskey_ui (bool) properties Properties to set when the operator is called propvalue the value this event translates to in a modal keymap repeat active on key-repeat events (when key is held) shift Shift key pressed (int), -1 for any state shift_ui (bool) show_expanded Show key map event and property details in the user interface type type of event value (value of event?) bprefs.mouse_doubleclick() bprefs.mouse_drag() bprefs.mouse_move() bprefs.mouse_select() notes: * if lshift is pressed, then shift is True. if rshift is pressed, then shift will still be True. if lshift or rshift are released, shift will be False! but, this isn't an issue, as blender handles it in the same way. * if modal operator invokes another operator on action, modal operator will only see the release of the action in (type_prev, value_prev) * mouse_prev_press_* will hold location of mouse at most recent press (keyboard, mouse, anything!) ''' class EventHandler: keyboard_modifier_types = { 'LEFT_CTRL', 'LEFT_ALT', 'LEFT_SHIFT', 'RIGHT_ALT', 'RIGHT_CTRL', 'RIGHT_SHIFT', 'OSKEY', } keyboard_alpha_types = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', } keyboard_number_types = { 'ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9', 'NUMPAD_0', } keyboard_numpad_types = { 'NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_5', 'NUMPAD_7', 'NUMPAD_9', 'NUMPAD_0', 'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_MINUS', 'NUMPAD_PLUS', 'NUMPAD_ENTER', } keyboard_function_types = { 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24', } keyboard_symbols_types = { 'SEMI_COLON', 'PERIOD', 'COMMA', 'QUOTE', 'ACCENT_GRAVE', 'MINUS', 'PLUS', 'SLASH', 'BACK_SLASH', 'EQUAL', 'LEFT_BRACKET', 'RIGHT_BRACKET', 'NUMPAD_PERIOD', 'NUMPAD_SLASH', 'NUMPAD_ASTERIX', 'NUMPAD_MINUS', 'NUMPAD_PLUS', 'GRLESS', } keyboard_media_types = { 'MEDIA_PLAY', 'MEDIA_STOP', 'MEDIA_FIRST', 'MEDIA_LAST', 'PAUSE', # ??? } keyboard_movement_types = { 'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END', 'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW', } keyboard_escape_types = { 'ESC', # 'TAB', ??? } keyboard_edit_types = { 'INSERT', 'TAB', 'RET', 'SPACE', 'LINE_FEED', 'BACK_SPACE', 'DEL', } keyboard_drag_types = { *keyboard_alpha_types, *keyboard_number_types, *keyboard_numpad_types, *keyboard_symbols_types, } keyboard_types = { *keyboard_modifier_types, *keyboard_alpha_types, *keyboard_number_types, *keyboard_numpad_types, *keyboard_function_types, *keyboard_symbols_types, *keyboard_media_types, *keyboard_movement_types, *keyboard_edit_types, } mouse_button_types = { 'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE', 'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE', 'MOUSEROTATE', 'MOUSESMARTZOOM', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE', 'PEN', 'ERASER', 'TRACKPADPAN', 'TRACKPADZOOM', } mouse_move_types = { 'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE', } mouse_types = { *mouse_button_types, *mouse_move_types, } ndof_types = { 'NDOF_MOTION', 'NDOF_BUTTON_MENU', 'NDOF_BUTTON_FIT', 'NDOF_BUTTON_TOP', 'NDOF_BUTTON_BOTTOM', 'NDOF_BUTTON_LEFT', 'NDOF_BUTTON_RIGHT', 'NDOF_BUTTON_FRONT', 'NDOF_BUTTON_BACK', 'NDOF_BUTTON_ISO1', 'NDOF_BUTTON_ISO2', 'NDOF_BUTTON_ROLL_CW', 'NDOF_BUTTON_ROLL_CCW', 'NDOF_BUTTON_SPIN_CW', 'NDOF_BUTTON_SPIN_CCW', 'NDOF_BUTTON_TILT_CW', 'NDOF_BUTTON_TILT_CCW', 'NDOF_BUTTON_ROTATE', 'NDOF_BUTTON_PANZOOM', 'NDOF_BUTTON_DOMINANT', 'NDOF_BUTTON_PLUS', 'NDOF_BUTTON_MINUS', 'NDOF_BUTTON_1', 'NDOF_BUTTON_2', 'NDOF_BUTTON_3', 'NDOF_BUTTON_4', 'NDOF_BUTTON_5', 'NDOF_BUTTON_6', 'NDOF_BUTTON_7', 'NDOF_BUTTON_8', 'NDOF_BUTTON_9', 'NDOF_BUTTON_10', 'NDOF_BUTTON_A', 'NDOF_BUTTON_B', 'NDOF_BUTTON_C', } timer_types = { 'TIMER', 'TIMER0', 'TIMER1', 'TIMER2', 'TIMER_JOBS', 'TIMER_AUTOSAVE', 'TIMER_REPORT', 'TIMERREGION', } scrollable_types = { 'HOME', 'PAGE_UP', 'PAGE_DOWN', 'END', 'LEFT_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'UP_ARROW', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE', 'TRACKPADPAN', } pressable_types = { # pressable also means releasable, clickable, double-clickable *keyboard_types, *mouse_button_types, *ndof_types } special_types = { 'mousemove': { 'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE' }, 'timer': { 'TIMER', 'TIMER_REPORT', 'TIMERREGION' }, 'deactivate': { 'WINDOW_DEACTIVATE' }, } modifier_keys = { 'alt', 'ctrl', 'shift', 'oskey', } def __init__(self, context, *, allow_keyboard_dragging=False): self._allow_keyboard_dragging = allow_keyboard_dragging self.reset() def reset(self): # current states self.mods = { mod: False for mod in self.modifier_keys } self.mouse = None self.mouse_prev = None self._held = {} # types that are currently held. {event.type: time of first held} self._is_dragging = False self.is_navigating = False # <- need this??? # memory self._first_held = None # contains details of when first held action happened (held type, mouse loc, time) self._last_event_type = None self._just_released = None # keep track of last pressed for double click # these properties are for very temporal state changes @property def is_mousemove(self): return self._last_event_type in self.special_types['mousemove'] @property def is_timer(self): return self._last_event_type in self.special_types['timer'] @property def is_deactivate(self): return self._last_event_type in self.special_types['deactivate'] def is_draggable(self, event): if self._allow_keyboard_dragging and event.type in self.keyboard_drag_types: return True if event.type in self.mouse_button_types: return True return False def is_double_click(self, *, event=None): if event and event.type != self.get_just_held('type'): return False delta = self.get_just_held('time') - time.time() return delta < prefs.mouse_doubleclick() def is_dragging(self, *, event=None): return get_held(event.type, prop='dragging') if event else self.get_first_held(prop='dragging') def holding_non_modifiers(self): return bool(t for t in self._held if t not in self.keyboard_modifier_types) def get_held(self, etype, *, prop=None, default=None): if etype not in self._held: return default d = self._held[etype] return d[prop] if prop else d def get_first_held(self, *, ignore_mods=True, prop=None, default=None): held = self._held if ignore_mods: held = {htype:held[htype] for htype in held if htype not in self.keyboard_modifier_types} if not held: return default d = min(held, key=lambda htype: held[htype]['time']) return d[prop] if prop else d def get_just_held(self, *, prop=None, default=None): return self._first_held[prop] if self._first_held else default def _update_press(self, event): # ignore non-pressable events if event.type not in self.pressable_types: return # FIRST, if nothing is held (ignoring modifiers), record first held details if not self.holding_non_modifiers(): self._first_held = { 'type': event.type, 'time': time.time(), 'mouse': self.mouse, 'dragging': False, 'can drag': self.is_type_draggable(event), 'double': self.is_double_click(event), } self._held[event.type] = { 'type': event.type, 'time': time.time(), 'mouse': self.mouse, 'dragging': False, 'can drag': self.is_type_draggable(event), 'double': self.is_double_click(event), } def _update_release(self, event, *, prev=False): etype = event.type if not prev else event.prev_type if etype == self.get_first_held(prop='type'): self._just_released = self._first_held self._first_held = None if etype in self.held: del self._held[etype] def _update_drag(self, event): first_held = self.get_first_held() if first_held['dragging'] or not first_held['can drag']: return # has mouse moved far enough? mouse_travel = (first_held['mouse'] - self.mouse).length if mouse_travel > bprefs.mouse_drag(): self._first_held['dragging'] = True fhtype = self._first_held['type'] if self._allow_keyboard_dragging and fhtype in self.keyboard_drag_types: self._first_held['dragging'] = True elif fhtype in self.mouse_button_types: self._first_held['dragging'] = True def update(self, context, event): self._last_event_type = event.type if self.is_deactivate: # any time these actions are received, all action states will be flushed self.reset() self.mods['alt'] = event.alt self.mods['ctrl'] = event.ctrl self.mods['oskey'] = event.oskey self.mods['shift'] = event.shift self.mouse = Point2D((event.mouse_x, event.mouse_y)) self.mouse_prev = Point2D((event.mouse_prev_x, event.mouse_prev_y)) if event.value_prev == 'RELEASE': self._update_release(event, prev=True) if event.value == 'PRESS': self._update_press(event) elif event.value == 'RELEASE': self._update_release(event) elif event.value == 'NOTHING': if event.type == 'MOUSEMOVE': pass if event.type not in self.mouse_move_types: self._update_drag(event)