Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rf/rf_keymapsystem.py
T
2026-03-17 14:30:01 -06:00

421 lines
20 KiB
Python

'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
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 os
import re
import bpy
from ..updater import updater
from ...addon_common.common.blender import get_path_from_addon_root
from ...addon_common.common.globals import Globals
from ...addon_common.common.utils import delay_exec, abspath
from ...addon_common.common.ui_styling import load_defaultstylings
from ...addon_common.common.ui_core import UI_Element
from ...addon_common.common.human_readable import convert_actions_to_human_readable
from ...config.options import options
from ...config.keymaps import get_keymaps, reset_all_keymaps, save_custom_keymaps, reset_keymap, default_rf_keymaps
class RetopoFlow_KeymapSystem:
@staticmethod
def reload_stylings():
load_defaultstylings()
path = get_path_from_addon_root('config', 'ui.css')
try:
Globals.ui_draw.load_stylesheet(path)
except AssertionError as e:
# TODO: show proper dialog to user here!!
print('could not load stylesheet "%s"' % path)
print(e)
Globals.ui_document.body.dirty(cause='Reloaded stylings', children=True)
Globals.ui_document.body.dirty_styling()
Globals.ui_document.body.dirty_flow()
def substitute_keymaps(self, mdown, wrap='`', pre='', post='', separator=', ', onlyfirst=None):
if type(wrap) is str: wrap_pre, wrap_post = wrap, wrap
else: wrap_pre, wrap_post = wrap
while True:
m = re.search(r'{{(?P<action>[^}]+)}}', mdown)
if not m: break
action = { s.strip() for s in m.group('action').split(',') }
sub = f'{pre}{wrap_pre}' + self.actions.to_human_readable(action, sep=f'{wrap_post}{separator}{wrap_pre}', onlyfirst=onlyfirst) + f'{wrap_post}{post}'
mdown = mdown[:m.start()] + sub + mdown[m.end():]
return mdown
def substitute_options(self, mdown, wrap='', pre='', post='', separator=', ', onlyfirst=None):
if type(wrap) is str: wrap_pre, wrap_post = wrap, wrap
else: wrap_pre, wrap_post = wrap
while True:
m = re.search(r'{\[(?P<option>[^\]]+)\]}', mdown)
if not m: break
opts = { s.strip() for s in m.group('option').split(',') }
sub = f'{pre}{wrap_pre}' + separator.join(str(options[opt]) for opt in opts) + f'{wrap_post}{post}'
mdown = mdown[:m.start()] + sub + mdown[m.end():]
return mdown
def substitute_python(self, mdown, wrap='', pre='', post=''):
if type(wrap) is str: wrap_pre, wrap_post = wrap, wrap
else: wrap_pre, wrap_post = wrap
while True:
m = re.search(r'{`(?P<python>[^`]+)`}', mdown)
if not m: break
pyret = eval(m.group('python'), globals(), locals())
sub = f'{pre}{wrap_pre}{pyret}{wrap_post}{post}'
mdown = mdown[:m.start()] + sub + mdown[m.end():]
return mdown
def keymap_config_open(self): #, mdown_path, done_on_esc=False, closeable=True, *args, **kwargs):
newversion = ''
keymaps = get_keymaps(force_reload=True)
humanread = self.actions.to_human_readable
tokmi = self.actions.from_human_readable
def ok():
save_custom_keymaps()
self.done()
def cancel():
get_keymaps(force_reload=True)
self.done()
def reset_all():
reset_all_keymaps()
rebuild()
self.alert_user(
message='Keymaps reset, but changes will not be saved until OK is clicked',
title='Keymaps Config System',
level='warning',
)
def key(e):
nonlocal keymaps, self
pass
# if e.key == 'ESC': close()
def action_to_label(action):
for category,actions in keymap_details:
for a,al in actions:
if a == action: return al
return action
def action_to_id(a):
aid = a.replace(' ', '_')
return aid
def set_edit_key_span(hk, is_key=True):
if not is_key:
clear_edit_key_span()
return
span = self.document.body.getElementById('edit-key-span')
span.innerText = f'Key: {hk}'
# span.style = ''
def capture_edit_key_span():
span = self.document.body.getElementById('edit-key-span')
span.innerText = '(capturing... press key to capture)'
# span.style = 'color: rgba(255, 255, 255, 0.5)'
def clear_edit_key_span():
span = self.document.body.getElementById('edit-key-span')
span.innerText = '(click to start capture)'
# span.style = 'color: rgba(255, 255, 255, 0.5)'
edit_data = {}
def edit_capture():
ui_button = self.document.body.getElementById('edit-key-span')
ui_button.can_focus = True
self.document.focus(ui_button, full=True)
capture_edit_key_span()
def edit_capture_mouse(action):
clear_edit_key_span()
edit_data['key'] = action
def edit_capture_key(event):
if event.key is None or event.key == 'NONE': return
ui_button = self.document.body.getElementById('edit-key-span')
if self.document.activeElement != ui_button: return
has_ctrl = 'CTRL+' in event.key
has_shift = 'SHIFT+' in event.key
has_alt = 'ALT+' in event.key
has_oskey = 'OSKEY+' in event.key
key = event.key.replace('CTRL+','').replace('SHIFT+','').replace('ALT+','').replace('OSKEY+','')
set_edit_key_span(humanread([key], visible=True))
edit_data['key'] = key
editor = self.document.body.getElementById('keymapconfig')
editor.getElementById('edit-ctrl').checked = has_ctrl
editor.getElementById('edit-shift').checked = has_shift
editor.getElementById('edit-alt').checked = has_alt
editor.getElementById('edit-oskey').checked = has_oskey
self.document.blur()
def edit_ok():
nonlocal edit_data, keymaps, tokmi
if edit_data['key'] == '':
self.alert_user(
message='Must select a key or mouse interaction first',
title='Keymaps Config System',
level='warning',
)
return
editor = self.document.body.getElementById('keymapconfig')
editor.style = "display: none"
self.document.body.getElementById('keymapsystem-cover').style = "display: none"
nk = ''
nk += 'CTRL+' if editor.getElementById('edit-ctrl').checked else ''
nk += 'SHIFT+' if editor.getElementById('edit-shift').checked else ''
nk += 'ALT+' if editor.getElementById('edit-alt').checked else ''
nk += 'OSKEY+' if editor.getElementById('edit-oskey').checked else ''
nk += tokmi(edit_data['key'])[0]
nk += '+CLICK' if editor.getElementById('edit-click').checked else ''
nk += '+DOUBLE' if editor.getElementById('edit-double').checked else ''
nk += '+DRAG' if editor.getElementById('edit-drag').checked else ''
a = edit_data['action']
al = edit_data['action label']
# do not change ordering of keymaps, just update
idx = keymaps[a].index(edit_data['keymap'])
keymaps[a][idx] = nk
rebuild_action(a, al)
def edit_cancel():
self.document.body.getElementById('keymapconfig').style = "display: none"
self.document.body.getElementById('keymapsystem-cover').style = "display: none"
if edit_data['keymap'] == '':
keymaps[edit_data['action']].remove('')
def edit_delete():
self.document.body.getElementById('keymapconfig').style = "display: none"
self.document.body.getElementById('keymapsystem-cover').style = "display: none"
delete_keymap(edit_data['action'], edit_data['keymap'])
def edit_start(a, al, k):
nonlocal edit_data, keymaps
aid = action_to_id(a)
ok = str(k)
hkctrl, k = 'CTRL+' in k, k.replace('CTRL+', '')
hkshift, k = 'SHIFT+' in k, k.replace('SHIFT+', '')
hkalt, k = 'ALT+' in k, k.replace('ALT+', '')
hkoskey, k = 'OSKEY+' in k, k.replace('OSKEY+', '')
hkclick, k = '+CLICK' in k, k.replace('+CLICK', '')
hkdouble, k = '+DOUBLE' in k, k.replace('+DOUBLE', '')
hkdrag, k = '+DRAG' in k, k.replace('+DRAG', '')
hk = humanread(k, visible=True)
is_key = hk not in {'LMB', 'MMB', 'RMB', 'WheelUp', 'WheelDown', ''}
if not is_key: hm, hk = hk, ''
else: hm = ''
edit_data['action'] = a
edit_data['action label'] = al
edit_data['keymap'] = ok
edit_data['key'] = k
self.document.body.getElementById('keymapsystem-cover').style = ""
editor = self.document.body.getElementById('keymapconfig')
editor.style = ''
editor.getElementById('edit-action').innerText = action_to_label(a)
set_edit_key_span(hk, is_key)
editor.getElementById('edit-key').checked = is_key
editor.getElementById('edit-lmb').checked = (hm == 'LMB')
editor.getElementById('edit-mmb').checked = (hm == 'MMB')
editor.getElementById('edit-rmb').checked = (hm == 'RMB')
editor.getElementById('edit-wu').checked = (hm == 'WheelUp')
editor.getElementById('edit-wd').checked = (hm == 'WheelDown')
editor.getElementById('edit-ctrl').checked = hkctrl
editor.getElementById('edit-shift').checked = hkshift
editor.getElementById('edit-alt').checked = hkalt
editor.getElementById('edit-oskey').checked = hkoskey
editor.getElementById('edit-press').checked = not (hkclick or hkdouble or hkdrag)
editor.getElementById('edit-click').checked = hkclick
editor.getElementById('edit-double').checked = hkdouble
editor.getElementById('edit-drag').checked = hkdrag
def add_keymap(a, al):
nonlocal keymaps
keymaps[a].append('')
edit_start(a, al, '')
def delete_keymap(a, al, k):
keymaps[a].remove(k)
rebuild_action(a, al)
def keymap_html(a, al):
nonlocal edit_start, delete_keymap, rebuild_action, add_keymap
aid = action_to_id(a)
html = ''
for k in keymaps[a]:
html += f'''<button id="keymap-{aid}-key" class="key" on_mouseclick="edit_start('{a}', '{al}', '{k}')" title="Click to edit this keymap for {al}">{humanread(k, visible=True)}</button>'''
html += f'''<button id="keymap-{aid}-del" class="delkey" on_mouseclick="delete_keymap('{a}', '{al}', '{k}')" title="Click to delete this keymap for {al}">✕</button>'''
html += f'''<button class="half-size" on_mouseclick="add_keymap('{a}', '{al}')" title="Click to add a new keymap for {al}">+ Add New Keymap</button>'''
html += f'''<button class="half-size" on_mouseclick="reset_keymap('{a}'); rebuild_action('{a}', '{al}')" title="Click to reset the keymaps for {al}">Reset Keymap</button>'''
return html
def rebuild_action(a, al):
# vvv this must be here so fromHTML() can see these fns!
nonlocal edit_start, delete_keymap, rebuild_action, add_keymap
# ^^^ this must be here so fromHTML() can see these fns!
aid = action_to_id(a)
ui_td = self.document.body.getElementById(f'keymap-{aid}')
ui_td.clear_children()
ui_td.append_children(UI_Element.fromHTML(keymap_html(a, al)))
def rebuild():
# vvv this must be here so fromHTML() can see these fns!
nonlocal edit_start, delete_keymap, rebuild_action, add_keymap
# ^^^ this must be here so fromHTML() can see these fns!
ui_keymaps = self.document.body.getElementById('keymaps')
html = ''
for category,actions in keymap_details:
html += f'<details>'
html += f'<summary>{category}</summary>'
html += f'<table>'
for a,al in actions:
aid = action_to_id(a)
html += f'<tr>'
html += f'<td class="action">{al}:</td>'
html += f'<td id="keymap-{aid}" class="keymap">{keymap_html(a, al)}</td>'
html += f'</tr>'
html += f'</table>'
html += f'</details>'
ui_keymaps.clear_children()
ui_keymaps.append_children(UI_Element.fromHTML(html))
ui_keymaps = UI_Element.fromHTMLFile(abspath('../html/keymaps_dialog.html'))
self.document.body.append_children(ui_keymaps)
self.document.body.getElementById('keymapconfig').style = 'display: none'
self.document.body.getElementById('keymapsystem-cover').style = "display: none"
self.document.body.getElementById('edit-ctrl').innerText = convert_actions_to_human_readable('CTRL')
self.document.body.getElementById('edit-shift').innerText = convert_actions_to_human_readable('SHIFT')
self.document.body.getElementById('edit-oskey').innerText = convert_actions_to_human_readable('OSKEY')
self.document.body.getElementById('edit-alt').innerText = convert_actions_to_human_readable('ALT')
rebuild()
self.document.body.dirty()
keymap_details = [
('General', [
('confirm', 'Confirm'),
('confirm drag', 'Confirm with Drag (sometimes this is needed for certain actions)'),
('confirm quick', 'Confirm with quick switch tool'),
('cancel', 'Cancel'),
('done', 'Quit RetopoFlow'),
('done alt0', 'Quit RetopoFlow (alternative)'),
('toggle ui', 'Toggle UI visibility'),
('blender passthrough', 'Blender passthrough'),
]),
('Insert, Move, Rotate, Scale', [
('insert', 'Insert new geometry'),
('quick insert', 'Quick insert (Knife, Loops)'),
('increase count', 'Increase Count'),
('decrease count', 'Decrease Count'),
('action', 'Action'),
('action alt0', 'Action (alt0)'),
('action alt1', 'Action (alt1)'),
('grab', 'Grab / move'),
('rotate', 'Rotate'),
('scale', 'Scale'),
('delete', 'Show delete menu'),
('delete pie menu', 'Show delete/dissolve/merge pie menu'),
# ('merge', 'Show merge menu'),
('smooth edge flow', 'Smooth edge flow of selected geometry'),
('rotate plane', 'Contours: rotate plane'),
('rotate screen', 'Contours: rotate screen'),
('slide', 'Loops: slide loop'),
('fill', 'Patches: fill'),
('knife reset', 'Knife: reset'),
('rip', 'PolyPen: rip'),
('rip fill', 'PolyPen: rip fill'),
]),
('Selection', [
('select all', 'Select all'),
('select invert', 'Select invert'),
('select linked', 'Select linked'),
('select linked mouse', 'Select linked under mouse'),
('deselect linked mouse', 'Deselect linked under mouse'),
('deselect all', 'Deselect all'),
('select single', 'Select single item (default depends on Blender selection setting)'),
('select single add', 'Add single item to selection (default depends on Blender selection setting)'),
('select smart', 'Smart selection (default depends on Blender selection setting)'),
('select smart add', 'Smart add to selection (default depends on Blender selection setting)'),
('select paint', 'Selection painting (default depends on Blender selection setting)'),
('select paint add', 'Paint to add to selection (default depends on Blender selection setting)'),
('select path add', 'Select along shortest path (default depends on Blender selection setting)'),
]),
('Geometry Attributes', [
('hide selected', 'Hide selected geometry'),
('hide unselected', 'Hide unselected geometry'),
('reveal hidden', 'Reveal hidden geometry'),
('pin', 'Pin selected geometry'),
('unpin', 'Unpin selected geometry'),
('unpin all', 'Unpin all geometry'),
('mark seam', 'Mark selected edges as seam'),
('clear seam', 'Unmark selected edges as seam'),
]),
('Switching Between Tools', [
('contours tool', 'Switch to Contours'),
('polystrips tool', 'Switch to PolyStrips'),
('strokes tool', 'Switch to Strokes'),
('patches tool', 'Switch to Patches'),
('polypen tool', 'Switch to PolyPen'),
('knife tool', 'Switch to Knife'),
('knife quick', 'Quick switch to Knife'),
('loops tool', 'Switch to Loops'),
('loops quick', 'Quick switch to Loops'),
('tweak tool', 'Switch to Tweak'),
('tweak quick', 'Quick switch to Tweak'),
('relax tool', 'Switch to Relax'),
('relax quick', 'Quick switch to Relax'),
('select tool', 'Switch to Select'),
('select quick', 'Quick switch to Select'),
]),
('Brush Actions', [
('brush', 'Brush'),
('brush alt', 'Brush (alt)'),
('brush radius', 'Change brush radius'),
('brush radius increase', 'Increase brush radius'),
('brush radius decrease', 'Decrease brush radius'),
('brush falloff', 'Change brush falloff'),
('brush strength', 'Change brush strength'),
]),
('Pie Menus', [
('pie menu', 'Show pie menu'),
('pie menu alt0', 'Show tool/alt pie menu'),
('pie menu confirm', 'Confirm pie menu selection'),
]),
('Help', [
('all help', 'Show all help'),
('general help', 'Show general help'),
('tool help', 'Show help for selected tool'),
]),
]
ignored_keys = {
'autosave',
'grease clear', 'grease pencil tool',
'stretch tool',
'toggle full area',
'reload css',
'select box', 'select box del', 'select box add',
'merge',
}
# check that all keymaps are able to be edited
def check_keymap_editor():
flattened_details = { key for (_, keyset) in keymap_details for (key, _) in keyset }
default_keys = set(default_rf_keymaps.keys()) - ignored_keys
missing_keys = default_keys - flattened_details
extra_keys = flattened_details - default_keys
if not missing_keys and not extra_keys: return
print(f'Error detected in keymap editor')
if missing_keys: print(f'Missing Keys: {sorted(missing_keys)}\nEither add to keymap_details or ignored_keys in rf_keymapsystem.py')
if extra_keys: print(f'Extra Keys: {sorted(extra_keys)}\nRemove these from keymap_details')
assert False
check_keymap_editor()