-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathoptions.py
413 lines (337 loc) · 15.9 KB
/
options.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
from abc import ABC, abstractmethod
from collections.abc import Callable, Mapping, Sequence
from dataclasses import KW_ONLY, dataclass, field
from typing import TYPE_CHECKING, Any, Literal, Self
from unrealsdk import logging
from .keybinds import KeybindType
if TYPE_CHECKING:
from .mod import Mod
# Little ugly to repeat this from settings, but we can't import it from there cause it creates a
# strong circular dependency - we need to import it to get JSON before we can define most options,
# but it needs to import those options from us
type JSON = Mapping[str, JSON] | Sequence[JSON] | str | int | float | bool | None
@dataclass
class BaseOption(ABC):
"""
Abstract base class for all options.
Args:
identifier: The option's identifier.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
Extra attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
"""
identifier: str
_: KW_ONLY
display_name: str = None # type: ignore
description: str = ""
description_title: str = None # type: ignore
is_hidden: bool = False
mod: "Mod | None" = field(default=None, init=False, repr=False)
@abstractmethod
def __init__(self) -> None:
raise NotImplementedError
def __post_init__(self) -> None:
if self.display_name is None: # type: ignore
self.display_name = self.identifier
if self.description_title is None: # type: ignore
self.description_title = self.display_name
@dataclass
class ValueOption[J: JSON](BaseOption):
"""
Abstract base class for all options storing a value.
Args:
identifier: The option's identifier.
value: The option's value.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
value: J
default_value: J = field(init=False)
_: KW_ONLY
on_change: Callable[[Self, J], None] | None = None
@abstractmethod
def __init__(self) -> None:
raise NotImplementedError
def __post_init__(self) -> None:
super().__post_init__()
self.default_value = self.value
def __setattr__(self, name: str, value: Any) -> None:
# Simpler to use `__setattr__` than a property to detect value changes
if (
name == "value"
and self.on_change is not None
and not hasattr(self, "_on_change_recursion_guard")
):
self._on_change_recursion_guard = True
self.on_change(self, value)
del self._on_change_recursion_guard
super().__setattr__(name, value)
def __call__(self, on_change: Callable[[Self, J], None]) -> Self:
"""
Sets the on change callback.
This allows this class to be constructed using decorator syntax, though note it is *not* a
decorator, it returns itself so must be the outermost level.
Args:
on_change: The callback to set.
Returns:
This option instance.
"""
if self.on_change is not None:
logging.dev_warning(
f"{self.__class__.__qualname__}.__call__ was called on an option which already has"
f" a on change callback.",
)
self.on_change = on_change
return self
@dataclass
class HiddenOption[J: JSON](ValueOption[J]):
"""
A generic option which is always hidden. Use this to persist arbitrary (JSON-encodeable) data.
This class is explicitly intended to be modified programmatically, unlike the other options
which are generally only modified by the mod menu.
Args:
identifier: The option's identifier.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
Extra Attributes:
is_hidden: Always true.
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
"""
# Need to redefine on change so that it binds to J@HiddenOption, not J@ValueOption
on_change: Callable[[Self, J], None] | None = None
is_hidden: Literal[True] = field( # pyright: ignore[reportIncompatibleVariableOverride]
default=True,
init=False,
)
def save(self) -> None:
"""Saves the settings of the mod this option is associated with."""
if self.mod is None:
raise RuntimeError(
"Tried to save a hidden option which does not have an associated mod.",
)
self.mod.save_settings()
@dataclass
class SliderOption(ValueOption[float]):
"""
An option selecting a number within a range. Typically implemented as a slider.
Args:
identifier: The option's identifier.
value: The option's value.
min_value: The minimum value.
max_value: The maximum value.
step: How much the value should move each step of the slider.
is_integer: If True, the value is treated as an integer.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
min_value: float
max_value: float
step: float = 1
is_integer: bool = True
@dataclass
class SpinnerOption(ValueOption[str]):
"""
An option selecting one of a set of strings. Typically implemented as a spinner.
Also see DropDownOption, which may be more suitable for larger numbers of choices.
Args:
identifier: The option's identifier.
value: The option's value.
choices: A list of choices for the value.
wrap_enabled: If True, allows moving from the last choice back to the first, or vice versa.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
choices: list[str]
wrap_enabled: bool = False
@dataclass
class BoolOption(ValueOption[bool]):
"""
An option toggling a boolean value. Typically implemented as an "on/off" spinner.
Args:
identifier: The option's identifier.
value: The option's value.
true_text: If not None, overwrites the default text used for the True option.
false_text: If not None, overwrites the default text used for the False option.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
true_text: str | None = None
false_text: str | None = None
@dataclass
class DropdownOption(ValueOption[str]):
"""
An option selecting one of a set of strings. Typically implemented as a dropdown menu.
Also see SpinnerOption, which may be more suitable for smaller numbers of choices.
Args:
identifier: The option's identifier.
value: The option's value.
choices: A list of choices for the value.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
choices: list[str]
@dataclass
class ButtonOption(BaseOption):
"""
An entry in the options list which may be pressed to trigger a callback.
May also be used without a callback, as a way to just inject plain entries, e.g. for extra
descriptions.
Args:
identifier: The option's identifier.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_press: If not None, the callback to run when the button is pressed. Passed a reference to
the option object.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
"""
_: KW_ONLY
on_press: Callable[[Self], None] | None = None
def __call__(self, on_press: Callable[[Self], None]) -> Self:
"""
Sets the on press callback.
This allows this class to be constructed using decorator syntax, though note it is *not* a
decorator, it returns itself so must be the outermost level.
Args:
on_press: The callback to set.
Returns:
This option instance.
"""
if self.on_press is not None:
logging.dev_warning(
f"{self.__class__.__qualname__}.__call__ was called on an option which already has"
f" a on press callback.",
)
self.on_press = on_press
return self
@dataclass
class KeybindOption(ValueOption[str | None]):
"""
An option selecting a keybinding.
Note this class only deals with displaying a key and letting the user rebind it, use `Keybind`
to handle press callbacks.
Args:
identifier: The option's identifier.
value: The option's value.
is_rebindable: True if the key may be rebound.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
on_change: If not None, a callback to run before updating the value. Passed a reference to
the option object and the new value. May be set using decorator syntax.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
default_value: What the value was originally when registered. Does not update on change.
"""
is_rebindable: bool = True
@classmethod
def from_keybind(cls, bind: KeybindType) -> Self:
"""
Constructs an option bound from a keybind.
Changes to the option will be applied back onto the keybind (though not in reverse).
Args:
bind: The keybind to construct from.
Returns:
A new binding option.
"""
option = cls(
identifier=bind.identifier,
value=bind.key,
is_rebindable=bind.is_rebindable,
display_name=bind.display_name,
description=bind.description,
description_title=bind.description_title,
is_hidden=bind.is_hidden,
on_change=lambda _, new_key: setattr(bind, "key", new_key),
)
option.default_value = bind.default_key
return option
@dataclass
class GroupedOption(BaseOption):
"""
A titled group of options, which appear inline.
Note that this class must be explicitly specified in the options list of a mod, it is *not*
picked up by the automatic gathering. This is to avoid issues where storing the child options in
separate variables might cause them to be gathered twice.
Args:
name: The option's name, used as the group title.
children: The group of child options.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
"""
children: Sequence[BaseOption]
@dataclass
class NestedOption(BaseOption):
"""
A nested group of options, which appear in a new menu.
Note that this class must be explicitly specified in the options list of a mod, it is *not*
picked up by the automatic gathering. This is to avoid issues where storing the child options in
separate variables might cause them to be gathered twice.
Args:
identifier: The option's identifier.
children: The group of child options.
Keyword Args:
display_name: The option name to use for display. Defaults to copying the identifier.
description: A short description about the option.
description_title: The title of the description. Defaults to copying the display name.
is_hidden: If true, the option will not be shown in the options menu.
Extra Attributes:
mod: The mod this option stores it's settings in, or None if not (yet) associated with one.
"""
children: Sequence[BaseOption]