Skip to content

Commit

Permalink
Keybindings dialog (mbrlabs#169)
Browse files Browse the repository at this point in the history
* First work on keybindings settings

- Add keybindings tab to settings dialog
- Add basic list of actions and english translations for keybinding
- Add non-functional UI elements as mock up

* Add basic template parser

* Start integrating the new StringTEmplating engine

* Parser cleanup

* Fixed warnings

* Implement filters

* Add test for template detection

* Add keybindings string generation

* Fix typos

* Add docstrings, fix naming

* Add templating to all translations

* Fix templates againg after merge-conflict

* Revert accidental commit of Main.tscn

* PR change requests

* Sort out GUI-event passing

- Rewrote most event handling to use event-callbacks
- This allows events to be intercepted in the _input method
- I tried to rewrite everything so that input events are passed in
	_unhandled_input functions, bit was not able to do so for
	the ViewportContainer. Had to fallback on _gui_input instead
	which is good enough for my purpose. _unhandled_input really
	would be more desirable but requires a larger restructuring
	and putting all tools IN the viewport (I think).
- There is one last use of _input but it does not handle events as
	the other old _event functions did: ColorPalettePicker
	still uses _input to decide when to close. It does not
	interfer with anything else though, so this should be okay.

* Sort out GUI-event passing

- Rewrote most event handling to use event-callbacks
- This allows events to be intercepted in the _input method
- I tried to rewrite everything so that input events are passed in
	_unhandled_input functions, bit was not able to do so for
	the ViewportContainer. Had to fallback on _gui_input instead
	which is good enough for my purpose. _unhandled_input really
	would be more desirable but requires a larger restructuring
	and putting all tools IN the viewport (I think).
- There is one last use of _input but it does not handle events as
	the other old _event functions did: ColorPalettePicker
	still uses _input to decide when to close. It does not
	interfer with anything else though, so this should be okay.

* Fix base-class function signature

* Fix potential memory leak of StringTemplating

* Implement removal of keybindings

* Fix key-event processing

* Basic working of keybinding editor - needs a lot of cleaning still

* Change: Use _exact_ matching and allow key-repeatitions

* Implement rebinds

* Brigthen the popup borders a bit

* Fix potential memory leak, less spammy print statements

* Clean function signatures

* Add back required function

* Move around scripts to more apropriate locations

* Implement workaround for keystroke detection

* Use workaround in Main.gd as well

* Fix: Use typed assignments

* Collect translation strings and put them in the English translation file

* Type annotations

* Add trashcan to icons, revert red background

* Code convention fixups

* Fix: Remove message update as part of _process and modifier-key detection

* Remove unused mask-var

* Fix spacings, use existing delete.png
  • Loading branch information
MrApplejuice authored Jul 2, 2022
1 parent 9708ec0 commit d38b9aa
Show file tree
Hide file tree
Showing 17 changed files with 441 additions and 17 deletions.
40 changes: 40 additions & 0 deletions lorien/Assets/I18n/en.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ SETTINGS_TITLE Settings
SETTINGS_GENERAL General
SETTINGS_APPEARANCE Appearance
SETTINGS_RENDERING Rendering
SETTINGS_KEYBINDINGS Keybindings
SETTINGS_PRESSURE_SENSITIVITY Pressure Sensitivity
SETTINGS_BRUSH_SIZE Default Brush Size
SETTINGS_CANVAS_COLOR Default Canvas Color
Expand Down Expand Up @@ -137,3 +138,42 @@ SAVE Save
DISCARD Discard
CANCEL Cancel
DELETE Delete

# -----------------------------------------------------------------------------
# Action names
# -----------------------------------------------------------------------------

ACTION_shortcut_save_project Save project
ACTION_shortcut_new_project New Project
ACTION_shortcut_open_project Open Project
ACTION_shortcut_undo Undo
ACTION_shortcut_redo Redo
ACTION_shortcut_brush_tool Brush tool
ACTION_shortcut_line_tool Line tool
ACTION_shortcut_eraser_tool Eraser tool
ACTION_shortcut_select_tool Selection tool
ACTION_shortcut_move_tool Move tool
ACTION_shortcut_rectangle_tool Rectangle tool
ACTION_shortcut_circle_tool Circle tool
ACTION_shortcut_export_project Export project
ACTION_deselect_all_strokes Deselect all strokes
ACTION_center_canvas_to_mouse Space
ACTION_delete_selected_strokes Delete selected strokes
ACTION_copy_strokes Copy
ACTION_paste_strokes Paste
ACTION_duplicate_strokes Duplicate strokes
ACTION_toggle_distraction_free_mode Toggle distraction free mode
ACTION_toggle_player EFF TWELVE
ACTION_toggle_fullscreen Toggle fullscreen

# -----------------------------------------------------------------------------
# Kebindings dialog messages
# -----------------------------------------------------------------------------

# Bind key dialog
KEYBINDING_DIALOG_BIND_WINDOW_NAME Bind key
KEYBINDING_DIALOG_BIND_ACTION Action: {action}

# Rebind already bound key dialog
KEYBINDING_DIALOG_REBIND_WINDOW_NAME Rebind key?
KEYBINDING_DIALOG_REBIND_MESSAGE '{event}' already is bound to {action}.\n\nDo you want to rebind?
8 changes: 5 additions & 3 deletions lorien/Misc/DSLParser.gd
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class ParsedSymbol:

# -------------------------------------------------------------------------------------------------
class GrammarElement:
extends Reference

func parse(s: String):
pass

Expand Down Expand Up @@ -117,7 +119,7 @@ class GrammarSequence:
var elements: Array
var flatten_same_name := false

func _init(_name: String, _elements: Array = [], _flatten_same_name = false):
func _init(_name: String, _elements: Array = [], _flatten_same_name = false).():
name = _name
elements = _elements
flatten_same_name = _flatten_same_name
Expand Down Expand Up @@ -154,7 +156,7 @@ class GrammarLiteral:
var value: String
var ignore_whitespace := true

func _init(_name: String, _value = null):
func _init(_name: String, _value = null).():
if _value == null:
_value = _name
name = _name
Expand All @@ -177,7 +179,7 @@ class GrammarRegexMatch:
var regex: RegEx
var ignore_whitespace := true

func _init(_name: String, pattern: String):
func _init(_name: String, pattern: String).():
name = _name
regex = RegEx.new()
regex.compile(pattern)
Expand Down
15 changes: 14 additions & 1 deletion lorien/Misc/I18nParser.gd
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ class_name I18nParser
const I18N_FOLDER := "res://Assets/I18n/"
const StringTemplating := preload("res://Misc/StringTemplating.gd")

# -------------------------------------------------------------------------------------------------
var _first_load := true

# -------------------------------------------------------------------------------------------------
func reload_locales() -> ParseResult:
TranslationServer.clear()
return load_files()

# -------------------------------------------------------------------------------------------------
class ParseResult:
extends Reference

var locales := PoolStringArray()
var language_names := PoolStringArray()

Expand Down Expand Up @@ -51,12 +61,15 @@ func load_files() -> ParseResult:

value = value.strip_edges()
value = templater.process_string(value)
value = value.replace("\\n", "\n")
translation.add_message(key, value)
else:
printerr("Key not found (make sure to use spaces; not tabs): %s" % line)
TranslationServer.add_translation(translation)
result.append(translation.locale, name)
print("Loaded i18n file: %s" % f)
if _first_load:
print("Loaded i18n file: %s" % f)
_first_load = false
return result

# -------------------------------------------------------------------------------------------------
Expand Down
7 changes: 5 additions & 2 deletions lorien/Misc/Settings.gd
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ const COLOR_PALETTE_UUID_LAST_USED := "color_palette_uuid_last_used"

# -------------------------------------------------------------------------------------------------
var _config_file := ConfigFile.new()
var _i18n := I18nParser.new()
var locales: PoolStringArray
var language_names: PoolStringArray

# -------------------------------------------------------------------------------------------------
func _ready():
_config_file = ConfigFile.new()
_load_settings()
reload_locales()

var i18n := I18nParser.new()
var parse_result := i18n.load_files()
# -------------------------------------------------------------------------------------------------
func reload_locales():
var parse_result := _i18n.reload_locales()
TranslationServer.set_locale(get_value(GENERAL_LANGUAGE, "en"))
locales = parse_result.locales
language_names = parse_result.language_names
Expand Down
15 changes: 14 additions & 1 deletion lorien/Misc/Utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,22 @@ func generate_uuid(length: int) -> String:
s += UUID_ALPHABET[idx]
return s

# -------------------------------------------------------------------------------------------------
func translate_action(action_name: String) -> String:
return TranslationServer.translate("ACTION_" + action_name)

# -------------------------------------------------------------------------------------------------
func bindable_actions() -> Array:
var result := []
for action in InputMap.get_actions():
# Suppress default keybindings for using menus etc and EFF TWELVE
if action.begins_with("ui_") || action.begins_with("player_"):
continue
result.append(action)
return result

# ------------------------------------------------------------------------------------------------
# See: https://github.com/mbrlabs/Lorien/pull/168#discussion_r908251372 for details
# Does an _exact_ match for the given key stroke.
func event_pressed_bug_workaround(action_name: String, event: InputEvent) -> bool:
return InputMap.action_has_event(action_name, event) && event.is_pressed()

38 changes: 38 additions & 0 deletions lorien/UI/Components/KeyBindingsLine.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[gd_scene load_steps=4 format=2]

[ext_resource path="res://UI/Components/KeyBindingsLineName.gd" type="Script" id=1]
[ext_resource path="res://UI/Components/KeyBindingsLineBindings.gd" type="Script" id=2]
[ext_resource path="res://UI/Components/KeyBindingsLineAddButton.gd" type="Script" id=3]

[node name="KeyBindingsLine" type="GridContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
columns = 3
__meta__ = {
"_edit_use_anchors_": false
}

[node name="Name" type="Label" parent="."]
margin_top = 3.0
margin_right = 38.0
margin_bottom = 17.0
text = "Name
"
valign = 1
max_lines_visible = 1
script = ExtResource( 1 )

[node name="Bindings" type="HBoxContainer" parent="."]
margin_left = 42.0
margin_right = 42.0
margin_bottom = 20.0
script = ExtResource( 2 )

[node name="AddButton" type="Button" parent="."]
margin_left = 46.0
margin_right = 66.0
margin_bottom = 20.0
text = "+"
script = ExtResource( 3 )

[connection signal="pressed" from="AddButton" to="AddButton" method="_on_AddButton_pressed"]
12 changes: 12 additions & 0 deletions lorien/UI/Components/KeyBindingsLineAddButton.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
extends Button

# -------------------------------------------------------------------------------------------------
signal bind_new_key

# -------------------------------------------------------------------------------------------------
func set_keybindings_data(_bindings_data: Dictionary) -> void:
pass

# -------------------------------------------------------------------------------------------------
func _on_AddButton_pressed() -> void:
emit_signal("bind_new_key")
31 changes: 31 additions & 0 deletions lorien/UI/Components/KeyBindingsLineBindings.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
extends HBoxContainer

# -------------------------------------------------------------------------------------------------
signal modified_binding(bindings_data)

# -------------------------------------------------------------------------------------------------
var _bindings_data := {}
var _preloaded_image := preload("res://Assets/Icons/delete.png")

# -------------------------------------------------------------------------------------------------
# Keybindings data: {"action": "str", "readable_name": "str", "events": [...]}
func set_keybindings_data(bindings_data: Dictionary) -> void:
for child in get_children():
remove_child(child)

_bindings_data = bindings_data
for event in bindings_data["events"]:
if event is InputEventKey:
var remove_button = Button.new()
remove_button.text = OS.get_scancode_string(event.get_scancode_with_modifiers())
remove_button.icon = _preloaded_image

remove_button.add_constant_override("hseparation", 6)

remove_button.connect("pressed", self, "_remove_pressed", [event])
add_child(remove_button)

# -------------------------------------------------------------------------------------------------
func _remove_pressed(event: InputEvent) -> void:
_bindings_data["events"].erase(event)
emit_signal("modified_binding", _bindings_data)
9 changes: 9 additions & 0 deletions lorien/UI/Components/KeyBindingsLineName.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extends Label

# -------------------------------------------------------------------------------------------------
signal modified_binding(bindings_data)

# -------------------------------------------------------------------------------------------------
# Keybindings data: {"action": "str", "readable_name": "str", "events": [...]}
func set_keybindings_data(_bindings_data: Dictionary) -> void:
text = _bindings_data["readable_name"]
77 changes: 77 additions & 0 deletions lorien/UI/Dialogs/AddKeyDialog.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
extends WindowDialog

# -------------------------------------------------------------------------------------------------
const _MODIFIER_KEYS := [KEY_SUPER_L, KEY_SUPER_R, KEY_CONTROL, KEY_SHIFT, KEY_META, KEY_ALT]

# -------------------------------------------------------------------------------------------------
export var action_name := ""
export var readable_action_name := "" setget _set_readable_action_name

onready var _confirm_rebind_dialog := $ConfirmRebind

var _pending_bind_event = null

# -------------------------------------------------------------------------------------------------
func _ready() -> void:
_update_event_text()
GlobalSignals.connect("language_changed", self, "_update_event_text")

# -------------------------------------------------------------------------------------------------
func _set_readable_action_name(s: String):
readable_action_name = s
_update_event_text()

# -------------------------------------------------------------------------------------------------
func _update_event_text() -> void:
$VBoxContainer/EventText.text = tr(
"KEYBINDING_DIALOG_BIND_ACTION"
).format({"action": readable_action_name})

# -------------------------------------------------------------------------------------------------
func _action_for_event(event: InputEvent):
for action in Utils.bindable_actions():
if InputMap.action_has_event(action, event):
return action
return null

# -------------------------------------------------------------------------------------------------
func _input(event: InputEvent) -> void:
if ! visible || _confirm_rebind_dialog.visible:
return
if event is InputEventKey && event.is_pressed():
get_tree().set_input_as_handled()

if event.scancode in _MODIFIER_KEYS:
return

var event_type := InputEventKey.new()
event_type.scancode = event.scancode
event_type.alt = event.alt
event_type.shift = event.shift
event_type.control = event.control
event_type.meta = event.meta
event_type.command = event.command

var _conflicting_action = _action_for_event(event_type)

_pending_bind_event = event_type
if _conflicting_action is String && _conflicting_action != action_name:
_confirm_rebind_dialog.dialog_text = tr("KEYBINDING_DIALOG_REBIND_MESSAGE").format({
"event": OS.get_scancode_string(event_type.get_scancode_with_modifiers()),
"action": Utils.translate_action(_conflicting_action)
})
_confirm_rebind_dialog.popup_centered()
else:
_finish_rebind()

# -------------------------------------------------------------------------------------------------
func _on_ConfirmRebind_confirmed() -> void:
_finish_rebind()

# -------------------------------------------------------------------------------------------------
func _finish_rebind() -> void:
for action in Utils.bindable_actions():
if InputMap.action_has_event(action, _pending_bind_event):
InputMap.action_erase_event(action, _pending_bind_event)
InputMap.action_add_event(action_name, _pending_bind_event)
visible = false
Loading

0 comments on commit d38b9aa

Please sign in to comment.