421 lines
20 KiB
Python
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()
|