Skip to content

Commit 89792de

Browse files
committed
implement the keybinds menu
1 parent f3e9a69 commit 89792de

File tree

3 files changed

+368
-31
lines changed

3 files changed

+368
-31
lines changed

src/willow1_mod_menu/options.py

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from .util import WillowGFxMenu
1919

2020
CUSTOM_OPTIONS_MENU_TAG = "willow1-mod-menu:custom-option"
21+
CUSTOM_KEYBINDS_MENU_TAG = "willow1-mod-menu:custom-keybinds"
22+
2123
RE_SELECTED_IDX = re.compile(r"^_level\d+\.(menu\.selections|content)\.mMenu\.mList\.item(\d+)$")
2224

2325
populator_stack: list[Populator] = []
@@ -58,9 +60,20 @@ def create_nested_options_menu(menu: WillowGFxMenu, option: NestedOption) -> Non
5860
open_new_generic_menu(menu)
5961

6062

63+
def create_keybinds_menu(menu: WillowGFxMenu) -> None:
64+
"""
65+
Creates a new menu holding a mod's keybinds.
66+
67+
Args:
68+
menu: The current menu to create the new one under.
69+
"""
70+
create_keybinds_menu_impl(menu)
71+
72+
6173
# ==================================================================================================
6274

6375
# Avoid circular imports
76+
from .populators import LOCKED_KEY_PREFIX # noqa: E402
6477
from .populators.mod_list import ModListPopulator # noqa: E402
6578
from .populators.mod_options import ModOptionPopulator # noqa: E402
6679
from .populators.options import OptionPopulator # noqa: E402
@@ -240,16 +253,22 @@ def reactivate_upper_screen(
240253

241254

242255
# ==================================================================================================
243-
# experimental
244256

245257

246-
def create_keybinds_menu(obj: WillowGFxMenu) -> None:
258+
def create_keybinds_menu_impl(obj: WillowGFxMenu) -> None:
247259
keybinds_frame = unrealsdk.construct_object("WillowGFxMenuScreenFrameKeyBinds", outer=obj)
260+
keybinds_frame.MenuTag = CUSTOM_KEYBINDS_MENU_TAG
248261
keybinds_frame.FrameTag = "PCBindings"
249-
keybinds_frame.MenuTag = "PCBindings"
250-
keybinds_frame.CaptionMarkup = "My Mod"
262+
keybinds_frame.CaptionMarkup = "Keybinds"
251263
keybinds_frame.Tip = "<Strings:WillowMenu.TitleMenu.SelBackBar>"
252264

265+
init_keybinds_frame.enable()
266+
init_bind_list.enable()
267+
bind_keybind_start.enable()
268+
bind_keybind_finish.enable()
269+
reset_keybinds.enable()
270+
localize_key_name.enable()
271+
253272
keybinds_frame.Init(obj, 0)
254273

255274
obj.ScreenStack.append(keybinds_frame)
@@ -264,28 +283,111 @@ def init_keybinds_frame(
264283
_ret: Any,
265284
func: BoundFunction,
266285
) -> type[Block]:
286+
init_keybinds_frame.disable()
287+
267288
super_class = obj.Class.SuperField
268289
assert super_class is not None
269290
super_func = super_class._find(func.func.Name)
270291
assert isinstance(super_func, UFunction)
271292
BoundFunction(super_func, obj)(args.Frame)
272293

273-
obj.ActiveItems.emplace_struct(
274-
Tag="action_mod_kb_0",
275-
Caption="kb caption",
276-
CaptionMarkup="kb markup",
277-
)
278-
obj.Keybinds.emplace_struct(Bind="kb bind", Keys=["P"])
279-
obj.Keybinds[-1].Keys.append("T")
294+
active_items = obj.ActiveItems
295+
keybinds = obj.Keybinds
280296

281-
obj.ActiveItems.emplace_struct(
282-
Tag="action_mod_option_0",
283-
Caption="opt caption",
284-
CaptionMarkup="opt markup",
285-
)
286-
obj.Keybinds.emplace_struct()
297+
active_items.clear()
298+
keybinds.clear()
299+
300+
populator_stack[-1].populate_keybinds(obj)
301+
302+
return Block
303+
304+
305+
# This function deletes the existing keys from the list, so we just block it to keep them
306+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:InitBind")
307+
def init_bind_list(*_: Any) -> type[Block]:
308+
return Block
309+
310+
311+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:DoBind")
312+
def bind_keybind_start(
313+
obj: UObject,
314+
_args: WrappedStruct,
315+
_ret: Any,
316+
_func: BoundFunction,
317+
) -> type[Block] | None:
318+
if (idx := obj.Selection.Current) == 0:
319+
# Reset keybinds, always allowed, continue
320+
return None
321+
322+
if populator_stack[-1].may_bind_key(idx):
323+
# Allowed to bind, continue
324+
return None
325+
326+
# Not allowed, block it
327+
return Block
287328

329+
330+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:Bind")
331+
def bind_keybind_finish(
332+
obj: UObject,
333+
args: WrappedStruct,
334+
_ret: Any,
335+
_func: BoundFunction,
336+
) -> type[Block]:
337+
populator_stack[-1].on_bind_key(obj, obj.Selection.Current, args.Key)
338+
return Block
339+
340+
341+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:ResetBindings_Clicked")
342+
def reset_keybinds(
343+
obj: UObject,
344+
args: WrappedStruct,
345+
_ret: Any,
346+
_func: BoundFunction,
347+
) -> type[Block]:
348+
if args.Dlg.DialogResult != "Yes":
349+
return Block
350+
351+
populator_stack[-1].on_reset_keybinds(obj)
288352
return Block
289353

290354

291-
init_keybinds_frame.disable()
355+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:LocalizeKeyName")
356+
def localize_key_name(
357+
_obj: UObject,
358+
args: WrappedStruct,
359+
_ret: Any,
360+
func: BoundFunction,
361+
) -> tuple[type[Block], str] | None:
362+
key: str = args.Key
363+
364+
if not key.startswith(LOCKED_KEY_PREFIX):
365+
# Normal, non locked key, use the standard function
366+
return None
367+
368+
without_prefix = key.removeprefix(LOCKED_KEY_PREFIX)
369+
if not without_prefix:
370+
# Locked key bound to nothing
371+
return Block, "[ -- ]"
372+
373+
# Locked key bound to something
374+
with unrealsdk.hooks.prevent_hooking_direct_calls():
375+
localized = func(without_prefix)
376+
return Block, f"[ {localized} ]"
377+
378+
379+
@hook("WillowGame.WillowGFxMenuScreenFrameKeyBinds:Screen_Deactivate", immediately_enable=True)
380+
def keybind_screen_deactivate(
381+
obj: UObject,
382+
_args: WrappedStruct,
383+
_ret: Any,
384+
_func: BoundFunction,
385+
) -> None:
386+
if obj.MenuTag == CUSTOM_KEYBINDS_MENU_TAG and populator_stack:
387+
init_bind_list.disable()
388+
bind_keybind_start.disable()
389+
bind_keybind_finish.disable()
390+
reset_keybinds.disable()
391+
localize_key_name.disable()
392+
393+
reactivate_upper_screen.enable()

src/willow1_mod_menu/populators/__init__.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
from unrealsdk import logging
77
from unrealsdk.unreal import UObject
88

9-
from mods_base import BaseOption, BoolOption, DropdownOption, SliderOption, SpinnerOption
9+
from mods_base import (
10+
BaseOption,
11+
BoolOption,
12+
DropdownOption,
13+
KeybindOption,
14+
SliderOption,
15+
SpinnerOption,
16+
)
1017
from willow1_mod_menu.util import WillowGFxMenu, find_focused_item
1118

1219
type WillowGFxLobbyTools = UObject
20+
type WillowGFxMenuScreenFrameKeyBinds = UObject
1321

22+
LOCKED_KEY_PREFIX = "!LOCKED!"
1423

1524
RE_INVALID_SPINNER_CHARS = re.compile("[:,]")
1625

@@ -23,11 +32,16 @@ class Populator(ABC):
2332
repr=False,
2433
default_factory=list[BaseOption],
2534
)
35+
drawn_keybinds: list[KeybindOption | None] = field(
36+
init=False,
37+
repr=False,
38+
default_factory=list[KeybindOption | None],
39+
)
2640

2741
@abstractmethod
2842
def populate(self, tools: WillowGFxLobbyTools) -> None:
2943
"""
30-
Populates the menu with the appropriate contents.
44+
Populates the menu with the appropriate options.
3145
3246
Args:
3347
tools: The lobby tools which may be used to add to the menu.
@@ -43,6 +57,20 @@ def handle_activate(self, menu: WillowGFxMenu, option: BaseOption) -> None:
4357
menu: The currently open menu.
4458
option: The option which was activate.
4559
"""
60+
raise NotImplementedError
61+
62+
def populate_keybinds(self, kb_frame: WillowGFxMenuScreenFrameKeyBinds) -> None:
63+
"""
64+
Populates the menu with the appropriate keybinds.
65+
66+
Args:
67+
kb_frame: The keybinds frame object which may be used to add binds.
68+
"""
69+
raise NotImplementedError
70+
71+
def handle_reset_keybinds(self) -> None:
72+
"""Handles the reset keybind menu being activated."""
73+
raise NotImplementedError
4674

4775
# ==============================================================================================
4876

@@ -190,3 +218,92 @@ def on_slider_spinner_change(self, menu: WillowGFxMenu, idx: int, value: float)
190218
f"Option '{option.identifier}' of unknown type {type(option)} unexpectedly got"
191219
f" a slider/spinner change event",
192220
)
221+
222+
def draw_keybind(
223+
self,
224+
kb_frame: WillowGFxMenuScreenFrameKeyBinds,
225+
name: str,
226+
key: str | None = None,
227+
is_rebindable: bool = True,
228+
option: KeybindOption | None = None,
229+
) -> None:
230+
"""
231+
Adds an individual keybind to the menu.
232+
233+
Args:
234+
kb_frame: The keybinds frame object to add to.
235+
name: The name of the bind.
236+
key: The key the bind is bound to.
237+
is_rebindable: True if the key is rebindable.
238+
option: The option to use during the callback, or None.
239+
"""
240+
kb_frame.ActiveItems.emplace_struct(Caption=name)
241+
242+
key_list: list[str]
243+
if key is None:
244+
key_list = [] if is_rebindable else [LOCKED_KEY_PREFIX]
245+
else:
246+
key_list = [key] if is_rebindable else [LOCKED_KEY_PREFIX + key]
247+
248+
kb_frame.Keybinds.emplace_struct(Keys=key_list)
249+
250+
self.drawn_keybinds.append(option)
251+
252+
def may_bind_key(self, idx: int) -> bool:
253+
"""
254+
Checks if we're allowed to bind the given key.
255+
256+
Args:
257+
idx: The index of the key to check.
258+
Returns:
259+
True if we're allowed to bind this key index.
260+
"""
261+
try:
262+
option = self.drawn_keybinds[idx]
263+
except IndexError:
264+
return False
265+
266+
if option is None:
267+
return False
268+
269+
return option.is_rebindable
270+
271+
def on_bind_key(self, kb_frame: WillowGFxMenuScreenFrameKeyBinds, idx: int, key: str) -> None:
272+
"""
273+
Handles a raw key bind event.
274+
275+
Args:
276+
kb_frame: The keybinds frame object to update with the new key.
277+
idx: The index of the key which was bound.
278+
key: The new value the key was requested to be set to.
279+
"""
280+
try:
281+
option = self.drawn_keybinds[idx]
282+
except IndexError:
283+
return
284+
if option is None or not option.is_rebindable:
285+
return
286+
287+
if key == option.value:
288+
option.value = None
289+
else:
290+
option.value = key
291+
292+
kb_frame.Keybinds[idx].Keys = [] if option.value is None else [option.value]
293+
294+
def on_reset_keybinds(self, kb_frame: WillowGFxMenuScreenFrameKeyBinds) -> None:
295+
"""
296+
Handles a raw reset keybinds event.
297+
298+
Args:
299+
kb_frame: The keybinds frame object to update with the new key.
300+
"""
301+
self.handle_reset_keybinds()
302+
303+
keybinds = kb_frame.Keybinds
304+
for idx, option in enumerate(self.drawn_keybinds):
305+
if option is None:
306+
continue
307+
keybinds[idx].Keys = [] if option.value is None else [option.value]
308+
309+
kb_frame.ApplyPageContent()

0 commit comments

Comments
 (0)