Files
blender-portable-repo/scripts/addons/RetopoFlow/addon_common/common/useractions.py
T
2026-03-17 14:30:01 -06:00

741 lines
28 KiB
Python

'''
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 <http://www.gnu.org/licenses/>.
'''
import re
import time
import inspect
from copy import deepcopy
import bpy
from .blender import get_view3d_area, get_view3d_space, get_view3d_region
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 .utils import Dict
from . import blender_preferences as bprefs
action_to_char = {
'ZERO': '0', 'NUMPAD_0': '0',
'ONE': '1', 'NUMPAD_1': '1',
'TWO': '2', 'NUMPAD_2': '2',
'THREE': '3', 'NUMPAD_3': '3',
'FOUR': '4', 'NUMPAD_4': '4',
'FIVE': '5', 'NUMPAD_5': '5',
'SIX': '6', 'NUMPAD_6': '6',
'SEVEN': '7', 'NUMPAD_7': '7',
'EIGHT': '8', 'NUMPAD_8': '8',
'NINE': '9', 'NUMPAD_9': '9',
'PERIOD': '.', 'NUMPAD_PERIOD': '.',
'PLUS': '+', 'NUMPAD_PLUS': '+',
'MINUS': '-', 'NUMPAD_MINUS': '-',
'SLASH': '/', 'NUMPAD_SLASH': '/',
'NUMPAD_ASTERIX': '*',
'BACK_SLASH': '\\',
'SPACE': ' ',
'EQUAL': '=',
'SEMI_COLON': ';', 'COMMA': ',',
'LEFT_BRACKET': '[', 'RIGHT_BRACKET': ']',
'QUOTE': "'", 'ACCENT_GRAVE': '`',
'GRLESS': '>',
'A':'a', 'B':'b', 'C':'c', 'D':'d',
'E':'e', 'F':'f', 'G':'g', 'H':'h',
'I':'i', 'J':'j', 'K':'k', 'L':'l',
'M':'m', 'N':'n', 'O':'o', 'P':'p',
'Q':'q', 'R':'r', 'S':'s', 'T':'t',
'U':'u', 'V':'v', 'W':'w', 'X':'x',
'Y':'y', 'Z':'z',
'SHIFT+A':'A', 'SHIFT+B':'B', 'SHIFT+C':'C', 'SHIFT+D':'D',
'SHIFT+E':'E', 'SHIFT+F':'F', 'SHIFT+G':'G', 'SHIFT+H':'H',
'SHIFT+I':'I', 'SHIFT+J':'J', 'SHIFT+K':'K', 'SHIFT+L':'L',
'SHIFT+M':'M', 'SHIFT+N':'N', 'SHIFT+O':'O', 'SHIFT+P':'P',
'SHIFT+Q':'Q', 'SHIFT+R':'R', 'SHIFT+S':'S', 'SHIFT+T':'T',
'SHIFT+U':'U', 'SHIFT+V':'V', 'SHIFT+W':'W', 'SHIFT+X':'X',
'SHIFT+Y':'Y', 'SHIFT+Z':'Z',
'SHIFT+ZERO': ')',
'SHIFT+ONE': '!',
'SHIFT+TWO': '@',
'SHIFT+THREE': '#',
'SHIFT+FOUR': '$',
'SHIFT+FIVE': '%',
'SHIFT+SIX': '^',
'SHIFT+SEVEN': '&',
'SHIFT+EIGHT': '*',
'SHIFT+NINE': '(',
'SHIFT+PERIOD': '>',
'SHIFT+PLUS': '+',
'SHIFT+MINUS': '_',
'SHIFT+SLASH': '?',
'SHIFT+BACK_SLASH': '|',
'SHIFT+EQUAL': '+',
'SHIFT+SEMI_COLON': ':', 'SHIFT+COMMA': '<',
'SHIFT+LEFT_BRACKET': '{', 'SHIFT+RIGHT_BRACKET': '}',
'SHIFT+QUOTE': '"', 'SHIFT+ACCENT_GRAVE': '~',
'SHIFT+GRLESS': '<',
'ESC': 'Escape',
'BACK_SPACE': 'Backspace',
'RET': 'Enter', 'NUMPAD_ENTER': 'Enter',
'HOME': 'Home', 'END': 'End',
'LEFT_ARROW': 'ArrowLeft', 'RIGHT_ARROW': 'ArrowRight',
'UP_ARROW': 'ArrowUp', 'DOWN_ARROW': 'ArrowDown',
'PAGE_UP': 'PageUp', 'PAGE_DOWN': 'PageDown',
'DEL': 'Delete',
'TAB': 'Tab',
}
translate_action = {
'WHEELINMOUSE': 'WHEELUPMOUSE',
'WHEELOUTMOUSE': 'WHEELDOWNMOUSE',
}
re_blenderop = re.compile(r'(?P<keymap>.+?) *\| *(?P<operator>.+)')
# https://docs.blender.org/api/current/bpy.types.KeyMapItems.html
# https://docs.blender.org/api/current/bpy_types_enum_items/event_type_items.html
# https://docs.blender.org/api/current/bpy_types_enum_items/event_value_items.html
ndof_actions = {
'NDOF_MOTION',
'NDOF_BUTTON', '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_ESC',
'NDOF_BUTTON_ALT', 'NDOF_BUTTON_SHIFT', 'NDOF_BUTTON_CTRL',
'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',
}
mousebutton_actions = {
'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE',
'BUTTON4MOUSE', 'BUTTON5MOUSE', 'BUTTON6MOUSE', 'BUTTON7MOUSE',
}
ignore_actions = {}
nonprintable_actions = {
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
'TIMER', 'TIMER_REPORT', 'TIMERREGION',
}
reset_actions = {
# any time these actions are received, all action states will be flushed
'WINDOW_DEACTIVATE',
}
timer_actions = {
'TIMER'
}
mousemove_actions = {
'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE',
}
trackpad_actions = {
'TRACKPADPAN', 'TRACKPADZOOM',
'MOUSEROTATE', 'MOUSESMARTZOOM',
}
modifier_actions = {
'OSKEY',
'LEFT_CTRL', 'LEFT_SHIFT', 'LEFT_ALT',
'RIGHT_CTRL', 'RIGHT_SHIFT', 'RIGHT_ALT',
}
blender_operator_keymaps = [
{
'name': 'navigate',
'operators': [
'3D View | view3d.rotate', # Rotate View
'3D View | view3d.move', # Move View
'3D View | view3d.zoom', # Zoom View
'3D View | view3d.dolly', # Dolly View
'3D View | view3d.view_pan', # View Pan
'3D View | view3d.view_orbit', # View Orbit
'3D View | view3d.view_persportho', # View Persp/Ortho
'3D View | view3d.viewnumpad', # View Numpad
'3D View | view3d.view_axis', # View Axis
'3D View | view2d.ndof', # NDOF Pan Zoom
'3D View | view3d.ndof_orbit_zoom', # NDOF Orbit View with Zoom
'3D View | view3d.ndof_orbit', # NDOF Orbit View
'3D View | view3d.ndof_pan', # NDOF Pan View
'3D View | view3d.ndof_all', # NDOF Move View
'3D View | view3d.view_roll', # NDOF View Roll
'3D View | view3d.view_selected', # View Selected
'3D View | view3d.view_center_cursor', # Center View to Cursor
'3D View | view3d.view_center_pick', # Center View to Mouse
# '3D View | view3d.navigate', # View Navigation
],
}, {
'name': 'blender window action',
'operators': [
# COMMENTED OUT, BECAUSE THERE IS A BUG WITH CONTEXT CHANGING!!
# 'Screen | screen.screen_full_area',
# 'Window | wm.window_fullscreen_toggle',
],
}, {
'name': 'blender save',
'operators': [
'Window | wm.save_mainfile',
],
}, {
'name': 'blender undo',
'operators': [
'Screen | ed.undo',
],
}, {
'name': 'blender redo',
'operators': [
'Screen | ed.redo',
],
}, {
'name': 'clipboard paste',
'operators': [
'Text | text.paste',
'3D View | view3d.pastebuffer',
'Console | console.paste',
],
},
]
def blenderop_to_kmis(blenderop):
keymaps = bpy.context.window_manager.keyconfigs.user.keymaps
i18n_translate = bpy.app.translations.pgettext # bpy.app.translations.pgettext tries to translate the given parameter
m = re_blenderop.match(blenderop)
if not m:
print(f'blenderop_to_kmis: {blenderop}')
return set()
okeymap, ooperator = m['keymap'], m['operator']
tkeymap, toperator = i18n_translate(okeymap), i18n_translate(ooperator)
keymap = keymaps.get(okeymap, None) or keymaps.get(tkeymap, None)
if not keymap: return set()
return {
kmi
for kmi in keymap.keymap_items
if all([
kmi.active,
kmi.idname in {ooperator, toperator},
getattr(kmi, 'direction', 'ANY') == 'ANY'
])
}
def kmi_to_op_properties(kmi):
path = kmi.idname.split('.')
op = getattr(getattr(bpy.ops, path[0]), path[1])
props = { k: kmi.path_resolve(f'properties.{k}') for k in kmi.properties.keys() }
return (op, props)
def blenderop_to_actions(blenderop):
return { kmi_to_action(kmi) for kmi in blenderop_to_kmis(blenderop) }
def action_strip_mods(action, *, ctrl=True, shift=True, alt=True, oskey=True, click=True, double_click=True, drag_click=True, mouse=False):
if action is None: return None
if mouse and 'MOUSE' in action: return ''
if ctrl: action = action.replace('CTRL+', '')
if shift: action = action.replace('SHIFT+', '')
if alt: action = action.replace('ALT+', '')
if oskey: action = action.replace('OSKEY+', '')
if click: action = action.replace('+CLICK', '')
if double_click: action = action.replace('+DOUBLE', '')
if drag_click: action = action.replace('+DRAG', '')
return action
def action_add_mods(action, *, ctrl=False, shift=False, alt=False, oskey=False, click=False, double_click=False, drag_click=False):
if not action: return action
action = translate_action.get(action, action)
return ''.join([
('CTRL+' if ctrl else ''),
('SHIFT+' if shift else ''),
('ALT+' if alt else ''),
('OSKEY+' if oskey else ''),
action,
('+CLICK' if click and not (double_click or drag_click) else ''),
('+DOUBLE' if double_click and not drag_click else ''),
('+DRAG' if drag_click else ''),
])
def kmi_to_action(kmi, *, event_type=None, click=False, double_click=False, drag_click=False):
return action_add_mods(
event_type or kmi.type,
ctrl=kmi.ctrl, shift=kmi.shift, alt=kmi.alt, oskey=kmi.oskey,
click=(kmi.value=='CLICK' or click),
double_click=(kmi.value=='DOUBLE_CLICK' or double_click),
drag_click=(kmi.value=='CLICK_DRAG' or drag_click),
)
class Actions:
@staticmethod
def get_instance(context):
if not hasattr(Actions, '_instance'):
Actions._create = True
Actions._instance = Actions(context)
del Actions._create
return Actions._instance
@staticmethod
def done():
if not hasattr(Actions, '_instance'): return
del Actions._instance
def __init__(self, context):
assert hasattr(Actions, '_create'), 'Do not create new instance of Actions. Instead, use Actions.get_instance()'
assert not hasattr(Actions, '_instance'), 'Only create one instance of Actions! Then use Actions.get_instance()'
self.update_context(context)
# keymaps
self.keymaps_universal = Dict(get_default_fn=set)
self.keymaps_contextual = Dict(get_default_fn=set)
self.keymaps_blender_operators = Dict(get_default_fn=list)
# fill in universal and action keymaps
self.keymaps_universal['navigate'] = trackpad_actions | ndof_actions
for group in blender_operator_keymaps:
group_name, blenderops = group['name'], group['operators']
# self.keymaps_universal.setdefault(group_name, set())
self.keymaps_universal[group_name] |= {
action
for blenderop in blenderops
for action in blenderop_to_actions(blenderop)
}
for blenderop in blenderops:
for kmi in blenderop_to_kmis(blenderop):
action, op_props = kmi_to_action(kmi), kmi_to_op_properties(kmi)
self.keymaps_blender_operators[action] += [op_props]
self.timer = False # is action from timer?
self.time_delta = 0 # elapsed time since last "step" (units=seconds)
self.time_last = time.time()
# IMPORTANT: the following properties are updated external to Actions
self.hit_pos = None # position of raytraced mouse to scene (updated externally!)
self.hit_norm = None # normal of raytraced mouse to scene (updated externally!)
self.reset_state(all_state=True)
def update_context(self, context):
self.context = context
self.screen = context.screen
self.window = context.window
# try to find area, region, space, region_3d
try:
self.area = get_view3d_area(context)
self.region = get_view3d_region(context)
self.space = get_view3d_space(context)
self.size = Vec2D((self.region.width, self.region.height))
self.r3d = self.space.region_3d
except Exception as e:
print(f'******************************************')
print(f'Addon Common: Could not find VIEW_3D area!')
print(f'Exception: {e}')
self.area = None
self.region = None
self.size = None
self.r3d = None
def reset_state(self, all_state=False):
self.actions_using = set()
self.actions_pressed = set()
self.actions_prevtime = dict() # previous time when action was pressed
self.now_pressed = dict() # currently pressed keys. key=stripped event type, value=full event type (includes modifiers)
self.just_pressed = None
self.last_pressed = None
self.event_type = None
# indicates if modifier keys are currently pressed
self.ctrl = False # note: will be true if either ctrl_left or ctrl_right are true
self.ctrl_left = False
self.ctrl_right = False
self.shift = False
self.shift_left = False
self.shift_right = False
self.alt = False
self.alt_left = False
self.alt_right = False
# non-keyboard and non-mouse properties
self.trackpad = False # is current action from trackpad?
self.ndof = False # is current action from NDOF?
self.scroll = (0, 0)
# mouse-related properties
if all_state:
self.mouse_select = bprefs.mouse_select()
self.mouse = None # current mouse position
self.mouse_prev = None # previous mouse position
self.mouse_lastb = None # last button pressed on mouse
self.mousemove = False # is the current action a mouse move?
self.mousemove_prev = False # was the previous action a mouse move?
self.mousemove_stop = False # did the mouse just stop moving?
self.mousedown = None # mouse position when a mouse button was pressed
self.mousedown_left = None # mouse position when LMB was pressed
self.mousedown_middle = None # mouse position when MMB was pressed
self.mousedown_right = None # mouse position when RMB was pressed
self.mousedown_drag = False # is user dragging?
# indicates if currently navigating
self.is_navigating = False
def call_action_operator(self, action, *args, **kwargs):
ops_props = self.keymaps_blender_operators[action]
if not ops_props: return
try:
op, props = ops_props[0]
# print(f'Invoking {action} {op} {props}')
ret = op('INVOKE_DEFAULT', *args, **kwargs, **props)
except Exception as e:
print(f'Actions.call_action_operator: Caught Exception while calling Blender operator')
print(f' {action=}')
print(f' {op=}')
print(f' {props=}')
print(e)
ret = None
return ret
actions_prevtime_default = (0, 0, float('inf'))
def get_last_press_time(self, event_type):
return self.actions_prevtime.get(event_type, self.actions_prevtime_default)
def update(self, context, event, fn_debug=None):
if event.type in reset_actions:
# print(f'Actions.update: resetting state')
self.reset_state()
return
self.unpress()
self.update_context(context)
event_type, pressed = event.type, (event.value == 'PRESS')
if pressed:
_,prevtime,_ = self.get_last_press_time(event_type)
curtime = time.time()
self.actions_prevtime[event_type] = (prevtime, curtime, curtime - prevtime)
self.event_type = event_type
self.mousemove_prev = self.mousemove
self.timer = (event_type in timer_actions)
self.mousemove = (event_type in mousemove_actions)
self.trackpad = (event_type in trackpad_actions)
self.ndof = (event_type in ndof_actions)
self.mousemove_stop = not self.mousemove and self.mousemove_prev
self.scroll = (0, 0) # to be set below
# record held modifiers
self.ctrl = event.ctrl
self.alt = event.alt
self.shift = event.shift
self.oskey = event.oskey
# handle completely ignorable actions (if any)
if event_type in ignore_actions: return
if fn_debug and event_type not in nonprintable_actions:
fn_debug('update start', event_type=event_type, event_value=event.value)
# ignore modifier key presses, as they do not "fire" pressed events
if event_type in modifier_actions:
return
# handle timer event
if self.timer:
time_cur = time.time()
self.time_delta = self.time_last - time_cur
self.time_last = time_cur
return
self.is_navigating = False
# handle mouse move event
if self.mousemove:
self.mouse_prev = self.mouse
self.mouse = Point2D((float(event.mouse_region_x), float(event.mouse_region_y)))
if not self.mousedown:
self.mousedown_drag = False
return
if self.mousedown_drag: return
if (self.mouse - self.mousedown).length <= bprefs.mouse_drag(): return
self.mousedown_drag = True
# can user drag non-mouse keys??
if self.mousedown_left: event_type = 'LEFTMOUSE'
elif self.mousedown_middle: event_type = 'MIDDLEMOUSE'
elif self.mousedown_right: event_type = 'RIGHTMOUSE'
self.event_type = event_type
pressed = True
elif event_type in {'LEFTMOUSE', 'MIDDLEMOUSE', 'RIGHTMOUSE'} and not pressed:
# release drag when mouse button is released
# can user drag non-mouse keys??
self.mousedown_drag = False
# handle trackpad event
if self.trackpad:
pressed = True
self.scroll = (event.mouse_x - event.mouse_prev_x, event.mouse_y - event.mouse_prev_y)
# handle navigation event
full_event_type = action_add_mods(
event_type,
ctrl=self.ctrl, alt=self.alt,
shift=self.shift, oskey=self.oskey,
drag_click=self.mousedown_drag,
)
self.is_navigating = (full_event_type in self.keymaps_universal['navigate'])
if self.is_navigating:
self.unuse(full_event_type)
mouse_event = event_type in mousebutton_actions and not self.is_navigating
if mouse_event:
if pressed:
if self.mouse_lastb != event_type: self.mousedown_drag = False
self.mousedown = Point2D((float(event.mouse_region_x), float(event.mouse_region_y)))
if event_type == 'LEFTMOUSE': self.mousedown_left = self.mousedown
elif event_type == 'MIDDLEMOUSE': self.mousedown_middle = self.mousedown
elif event_type == 'RIGHTMOUSE': self.mousedown_right = self.mousedown
self.mouse_lastb = event_type
else:
self.mousedown = None
self.mousedown_left = None
self.mousedown_middle = None
self.mousedown_right = None
self.mousedown_drag = False
ftype = kmi_to_action(event, event_type=event_type, drag_click=self.mousedown_drag and mouse_event)
if pressed:
# if event_type not in self.now_pressed:
# self.just_pressed = ftype
self.just_pressed = ftype
if 'WHEELUPMOUSE' in ftype or 'WHEELDOWNMOUSE' in ftype:
# mouse wheel actions have no release, so handle specially
self.just_pressed = ftype
else:
self.now_pressed[event_type] = ftype
self.last_pressed = ftype
else:
if event_type in self.now_pressed:
if event_type in mousebutton_actions and not self.mousedown_drag:
_,_,deltatime = self.get_last_press_time(event_type)
single = (deltatime > bprefs.mouse_doubleclick()) or (self.mouse_lastb != event_type)
self.just_pressed = kmi_to_action(event, event_type=event_type, click=single, double_click=not single)
else:
del self.now_pressed[event_type]
if fn_debug and event_type not in nonprintable_actions:
fn_debug(
'update end',
ftype=ftype,
pressed=pressed,
just_pressed=self.just_pressed,
now_pressed=self.now_pressed,
last_pressed=self.last_pressed,
)
def _convert(self, action):
return (self.keymaps_universal[action] | self.keymaps_contextual[action]) or { action }
def convert(self, actions):
match actions:
case set(): pass # already a set; no need to do anything
case str(): actions = { actions } # passed only a string
case list(): actions = set(actions) # prevent duplicate actions by converting to set
case _: actions = { actions } # catch all (should not happen)
return { a for action in actions for a in self._convert(action) }
def to_human_readable(self, actions, *, sep=',', onlyfirst=None, visible=False):
if type(actions) is str: actions = { actions }
actions = [ act for action in actions for act in self.convert(action) ]
return convert_actions_to_human_readable(actions, sep=sep, onlyfirst=onlyfirst, visible=visible)
def from_human_readable(self, actions):
return convert_human_readable_to_actions(actions)
def unuse(self, actions, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False):
if not actions: return
strip_mods = lambda p: action_strip_mods(
p,
ctrl = ignorectrl or ignoremods,
shift = ignoreshift or ignoremods,
alt = ignorealt or ignoremods,
oskey = ignoreoskey or ignoremods,
click = ignoreclick or ignoremulti,
double_click = ignoredouble or ignoremulti,
drag_click = ignoredrag or ignoremulti,
)
actions = [ strip_mods(p) for p in self.convert(actions) ]
keys = [k for k,v in self.now_pressed.items() if strip_mods(v) in actions]
for k in keys: del self.now_pressed[k]
self.mousedown = None
self.mousedown_left = None
self.mousedown_middle = None
self.mousedown_right = None
self.mousedown_drag = False
self.unpress()
def unpress(self):
if not self.just_pressed: return
just_pressed_no_mods = action_strip_mods(self.just_pressed)
if just_pressed_no_mods in self.now_pressed:
if '+CLICK' in self.just_pressed:
del self.now_pressed[just_pressed_no_mods]
elif '+DOUBLE' in self.just_pressed:
del self.now_pressed[just_pressed_no_mods]
self.just_pressed = None
def using(self, actions, using_all=False, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False):
if actions is None: return False
strip_mods = lambda p: action_strip_mods(
p,
ctrl = ignorectrl or ignoremods,
shift = ignoreshift or ignoremods,
alt = ignorealt or ignoremods,
oskey = ignoreoskey or ignoremods,
click = ignoreclick or ignoremulti,
double_click = ignoredouble or ignoremulti,
drag_click = ignoredrag or ignoremulti,
)
actions = [ strip_mods(p) for p in self.convert(actions) ]
results = [ strip_mods(p) in actions for p in self.now_pressed.values() ]
return all(results) if using_all else any(results)
def using_onlymods(self, actions, exact=True):
if actions is None: return False
def action_good(action):
nonlocal exact
act_c = 'CTRL+' in action
act_s = 'SHIFT+' in action
act_a = 'ALT+' in action
ret = True
if exact:
ret &= act_c == self.ctrl
ret &= act_s == self.shift
ret &= act_a == self.alt
else:
ret &= not (act_c and self.ctrl)
ret &= not (act_s and self.shift)
ret &= not (act_a and self.alt)
return ret
return any(action_good(action) for action in self.convert(actions))
def pressed(self, actions, unpress=True, ignoremods=False, ignorectrl=False, ignoreshift=False, ignorealt=False, ignoreoskey=False, ignoremulti=False, ignoreclick=False, ignoredouble=False, ignoredrag=False, ignoremouse=False):
if actions is None: return False
if not self.just_pressed: return False
actions = self.convert(actions)
just_pressed = action_strip_mods(
self.just_pressed,
ctrl = ignorectrl or ignoremods,
shift = ignoreshift or ignoremods,
alt = ignorealt or ignoremods,
oskey = ignoreoskey or ignoremods,
click = ignoreclick or ignoremulti,
double_click = ignoredouble or ignoremulti,
drag_click = ignoredrag or ignoremulti,
mouse = ignoremouse,
)
if not just_pressed: return False
ret = just_pressed in actions
if ret and unpress: self.unpress()
return ret
def released(self, actions, released_all=False, ignoredrag=True, **kwargs):
if actions is None: return False
return not self.using(actions, using_all=released_all, ignoredrag=ignoredrag, **kwargs)
def warp_mouse(self, xy:Point2D):
rx,ry = self.region.x,self.region.y
mx,my = xy
self.context.window.cursor_warp(rx + mx, ry + my)
def valid_mouse(self):
if self.mouse is None: return False
mx,my = self.mouse
sx,sy = self.size
return 0 <= mx < sx and 0 <= my < sy
def as_char(self, ftype):
return action_to_char.get(ftype, '')
def start_timer(self, hz, enabled=True):
return TimerHandler(hz, context=self.context, enabled=enabled)
class ActionHandler:
_actions = None
def __init__(self, context, keymap={}):
if not ActionHandler._actions:
ActionHandler._actions = Actions.get_instance(context)
self.__dict__['_keymap'] = Dict({
k: ({actions} if type(actions) is str else set(actions))
for (k, actions) in keymap.items()
}, get_default_fn=set)
def __getattr__(self, key):
if not ActionHandler._actions: return None
ActionHandler._actions.keymaps_contextual = self._keymap
return getattr(ActionHandler._actions, key)
def __setattr__(self, key, value):
if not ActionHandler._actions: return
ActionHandler._actions.keymaps_contextual = self._keymap
return setattr(ActionHandler._actions, key, value)
def done(self):
if not ActionHandler._actions: return
ActionHandler._actions.done()
ActionHandler._actions = None