restiny 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- restiny/__about__.py +1 -1
- restiny/core/app.py +184 -177
- restiny/core/request_area.py +102 -100
- restiny/core/response_area.py +53 -20
- restiny/core/url_area.py +28 -20
- restiny/enums.py +0 -1
- restiny/widgets/__init__.py +2 -2
- restiny/widgets/dynamic_fields.py +375 -96
- restiny/widgets/path_chooser.py +71 -26
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/METADATA +5 -4
- restiny-0.2.1.dist-info/RECORD +24 -0
- restiny/assets/__pycache__/__init__.cpython-310.pyc +0 -0
- restiny/assets/__pycache__/__init__.cpython-313.pyc +0 -0
- restiny/assets/__pycache__/__init__.cpython-314.pyc +0 -0
- restiny/screens/__init__.py +0 -0
- restiny/screens/dialog.py +0 -109
- restiny-0.1.2.dist-info/RECORD +0 -29
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/WHEEL +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/entry_points.txt +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.1.2.dist-info → restiny-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,73 +1,111 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
1
5
|
from textual import on
|
|
2
6
|
from textual.app import ComposeResult
|
|
3
7
|
from textual.containers import VerticalScroll
|
|
4
8
|
from textual.message import Message
|
|
5
|
-
from textual.
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.widgets import (
|
|
11
|
+
Button,
|
|
12
|
+
ContentSwitcher,
|
|
13
|
+
Input,
|
|
14
|
+
RadioButton,
|
|
15
|
+
RadioSet,
|
|
16
|
+
Switch,
|
|
17
|
+
)
|
|
6
18
|
|
|
19
|
+
from restiny.widgets.path_chooser import PathChooser
|
|
7
20
|
|
|
8
|
-
class DynamicField(Static):
|
|
9
|
-
"""
|
|
10
|
-
Enableable and removable field
|
|
11
|
-
"""
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
class DynamicField(Widget):
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def compose(self) -> ComposeResult: ...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def enabled(self) -> bool: ...
|
|
29
|
+
|
|
30
|
+
@enabled.setter
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def enabled(self, value: bool) -> None: ...
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def key(self) -> str: ...
|
|
37
|
+
|
|
38
|
+
@key.setter
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def key(self, value: str) -> None: ...
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def value(self) -> str | Path | None: ...
|
|
45
|
+
|
|
46
|
+
@value.setter
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def value(self, value: str | Path | None) -> None: ...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def is_empty(self) -> bool: ...
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def is_filled(self) -> bool: ...
|
|
20
57
|
|
|
21
58
|
class Enabled(Message):
|
|
22
59
|
"""
|
|
23
60
|
Sent when the user enables the field.
|
|
24
61
|
"""
|
|
25
62
|
|
|
26
|
-
def __init__(self,
|
|
63
|
+
def __init__(self, field: 'DynamicField') -> None:
|
|
27
64
|
super().__init__()
|
|
28
|
-
self.
|
|
65
|
+
self.field = field
|
|
29
66
|
|
|
67
|
+
@property
|
|
30
68
|
def control(self) -> 'DynamicField':
|
|
31
|
-
return self.
|
|
69
|
+
return self.field
|
|
32
70
|
|
|
33
71
|
class Disabled(Message):
|
|
34
72
|
"""
|
|
35
73
|
Sent when the user disables the field.
|
|
36
74
|
"""
|
|
37
75
|
|
|
38
|
-
def __init__(self,
|
|
76
|
+
def __init__(self, field: 'DynamicField') -> None:
|
|
39
77
|
super().__init__()
|
|
40
|
-
self.
|
|
78
|
+
self.field = field
|
|
41
79
|
|
|
42
80
|
@property
|
|
43
81
|
def control(self) -> 'DynamicField':
|
|
44
|
-
return self.
|
|
82
|
+
return self.field
|
|
45
83
|
|
|
46
84
|
class Empty(Message):
|
|
47
85
|
"""
|
|
48
86
|
Sent when the key input and value input is empty.
|
|
49
87
|
"""
|
|
50
88
|
|
|
51
|
-
def __init__(self,
|
|
89
|
+
def __init__(self, field: 'DynamicField') -> None:
|
|
52
90
|
super().__init__()
|
|
53
|
-
self.
|
|
91
|
+
self.field = field
|
|
54
92
|
|
|
55
93
|
@property
|
|
56
94
|
def control(self) -> 'DynamicField':
|
|
57
|
-
return self.
|
|
95
|
+
return self.field
|
|
58
96
|
|
|
59
97
|
class Filled(Message):
|
|
60
98
|
"""
|
|
61
99
|
Sent when the key input or value input is filled.
|
|
62
100
|
"""
|
|
63
101
|
|
|
64
|
-
def __init__(self,
|
|
102
|
+
def __init__(self, field: 'DynamicField') -> None:
|
|
65
103
|
super().__init__()
|
|
66
|
-
self.
|
|
104
|
+
self.field = field
|
|
67
105
|
|
|
68
106
|
@property
|
|
69
107
|
def control(self) -> 'DynamicField':
|
|
70
|
-
return self.
|
|
108
|
+
return self.field
|
|
71
109
|
|
|
72
110
|
class RemoveRequested(Message):
|
|
73
111
|
"""
|
|
@@ -76,37 +114,63 @@ class DynamicField(Static):
|
|
|
76
114
|
to actually remove the field or not.
|
|
77
115
|
"""
|
|
78
116
|
|
|
79
|
-
def __init__(self,
|
|
117
|
+
def __init__(self, field: 'DynamicField') -> None:
|
|
80
118
|
super().__init__()
|
|
81
|
-
self.
|
|
119
|
+
self.field = field
|
|
82
120
|
|
|
83
121
|
@property
|
|
84
122
|
def control(self) -> 'DynamicField':
|
|
85
|
-
return self.
|
|
123
|
+
return self.field
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TextDynamicField(DynamicField):
|
|
127
|
+
"""
|
|
128
|
+
Enableable and removable field
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
DEFAULT_CSS = """
|
|
132
|
+
TextDynamicField {
|
|
133
|
+
width: 100%;
|
|
134
|
+
height: auto;
|
|
135
|
+
layout: grid;
|
|
136
|
+
grid-size: 4 1;
|
|
137
|
+
grid-columns: auto 1fr 2fr auto; /* Set 1:2 ratio between Inputs */
|
|
138
|
+
}
|
|
139
|
+
"""
|
|
86
140
|
|
|
87
141
|
def __init__(
|
|
88
142
|
self, enabled: bool, key: str, value: str, *args, **kwargs
|
|
89
143
|
) -> None:
|
|
90
144
|
super().__init__(*args, **kwargs)
|
|
91
|
-
|
|
92
|
-
# Store initial values temporarily; applied after mounting.
|
|
93
145
|
self._initial_enabled = enabled
|
|
94
146
|
self._initial_key = key
|
|
95
147
|
self._initial_value = value
|
|
96
148
|
|
|
97
149
|
def compose(self) -> ComposeResult:
|
|
98
|
-
yield Switch(
|
|
99
|
-
|
|
150
|
+
yield Switch(
|
|
151
|
+
value=self._initial_enabled,
|
|
152
|
+
tooltip='Send this field?',
|
|
153
|
+
id='enabled',
|
|
154
|
+
)
|
|
100
155
|
yield Input(
|
|
101
|
-
value=self.
|
|
156
|
+
value=self._initial_key,
|
|
157
|
+
placeholder='Key',
|
|
158
|
+
select_on_focus=False,
|
|
159
|
+
id='key',
|
|
102
160
|
)
|
|
103
|
-
yield
|
|
161
|
+
yield Input(
|
|
162
|
+
value=self._initial_value,
|
|
163
|
+
placeholder='Value',
|
|
164
|
+
select_on_focus=False,
|
|
165
|
+
id='value',
|
|
166
|
+
)
|
|
167
|
+
yield Button(label='➖', tooltip='Remove field', id='remove')
|
|
104
168
|
|
|
105
|
-
def on_mount(self) -> None:
|
|
106
|
-
self.enabled_switch
|
|
107
|
-
self.key_input
|
|
108
|
-
self.value_input
|
|
109
|
-
self.remove_button
|
|
169
|
+
async def on_mount(self) -> None:
|
|
170
|
+
self.enabled_switch = self.query_one('#enabled', Switch)
|
|
171
|
+
self.key_input = self.query_one('#key', Input)
|
|
172
|
+
self.value_input = self.query_one('#value', Input)
|
|
173
|
+
self.remove_button = self.query_one('#remove', Button)
|
|
110
174
|
|
|
111
175
|
@property
|
|
112
176
|
def enabled(self) -> bool:
|
|
@@ -132,57 +196,258 @@ class DynamicField(Static):
|
|
|
132
196
|
def value(self, value: str) -> None:
|
|
133
197
|
self.value_input.value = value
|
|
134
198
|
|
|
199
|
+
@property
|
|
200
|
+
def is_filled(self) -> bool:
|
|
201
|
+
return len(self.key_input.value) > 0 or len(self.value_input.value) > 0
|
|
202
|
+
|
|
135
203
|
@property
|
|
136
204
|
def is_empty(self) -> bool:
|
|
137
|
-
return
|
|
138
|
-
|
|
205
|
+
return not self.is_filled
|
|
206
|
+
|
|
207
|
+
@on(Switch.Changed, '#enabled')
|
|
208
|
+
def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
|
|
209
|
+
if message.value is True:
|
|
210
|
+
self.post_message(self.Enabled(field=self))
|
|
211
|
+
elif message.value is False:
|
|
212
|
+
self.post_message(message=self.Disabled(field=self))
|
|
213
|
+
|
|
214
|
+
@on(Input.Changed, '#key')
|
|
215
|
+
@on(Input.Changed, '#value')
|
|
216
|
+
def on_input_changed(self, message: Input.Changed) -> None:
|
|
217
|
+
self.enabled_switch.value = True
|
|
218
|
+
|
|
219
|
+
if self.is_empty:
|
|
220
|
+
self.post_message(message=self.Empty(field=self))
|
|
221
|
+
elif self.is_filled:
|
|
222
|
+
self.post_message(message=self.Filled(field=self))
|
|
223
|
+
|
|
224
|
+
@on(Button.Pressed, '#remove')
|
|
225
|
+
def on_remove_requested(self, message: Button.Pressed) -> None:
|
|
226
|
+
self.post_message(self.RemoveRequested(field=self))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _ValueKind(StrEnum):
|
|
230
|
+
TEXT = 'text'
|
|
231
|
+
FILE = 'file'
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TextOrFileDynamicField(DynamicField):
|
|
235
|
+
DEFAULT_CSS = """
|
|
236
|
+
TextOrFileDynamicField {
|
|
237
|
+
width: 100%;
|
|
238
|
+
height: auto;
|
|
239
|
+
layout: grid;
|
|
240
|
+
grid-size: 5 1;
|
|
241
|
+
grid-columns: auto auto 1fr 2fr auto; /* Set 1:2 ratio between Inputs */
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
TextOrFileDynamicField > RadioSet > RadioButton.-selected {
|
|
245
|
+
background: $surface;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
TextOrFileDynamicField > ContentSwitcher > PathChooser{
|
|
249
|
+
margin-right: 1;
|
|
250
|
+
}
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
enabled: bool = False,
|
|
256
|
+
key: str = '',
|
|
257
|
+
value: str | Path | None = '',
|
|
258
|
+
value_kind: _ValueKind = _ValueKind.TEXT,
|
|
259
|
+
*args,
|
|
260
|
+
**kwargs,
|
|
261
|
+
) -> None:
|
|
262
|
+
super().__init__(*args, **kwargs)
|
|
263
|
+
self._initial_enabled = enabled
|
|
264
|
+
self._initial_key = key
|
|
265
|
+
self._initial_value = value
|
|
266
|
+
self._initial_value_kind = value_kind
|
|
267
|
+
|
|
268
|
+
def compose(self) -> ComposeResult:
|
|
269
|
+
with RadioSet(id='value-kind', compact=True):
|
|
270
|
+
yield RadioButton(
|
|
271
|
+
label=_ValueKind.TEXT,
|
|
272
|
+
value=bool(self._initial_value_kind == _ValueKind.TEXT),
|
|
273
|
+
id='value-kind-text',
|
|
274
|
+
)
|
|
275
|
+
yield RadioButton(
|
|
276
|
+
label=_ValueKind.FILE,
|
|
277
|
+
value=bool(self._initial_value_kind == _ValueKind.FILE),
|
|
278
|
+
id='value-kind-file',
|
|
279
|
+
)
|
|
280
|
+
yield Switch(
|
|
281
|
+
value=self._initial_enabled,
|
|
282
|
+
tooltip='Send this field?',
|
|
283
|
+
id='enabled',
|
|
139
284
|
)
|
|
285
|
+
yield Input(
|
|
286
|
+
value=self._initial_key,
|
|
287
|
+
placeholder='Key',
|
|
288
|
+
select_on_focus=False,
|
|
289
|
+
id='key',
|
|
290
|
+
)
|
|
291
|
+
with ContentSwitcher(
|
|
292
|
+
initial='value-text'
|
|
293
|
+
if self._initial_value_kind == _ValueKind.TEXT
|
|
294
|
+
else 'value-file',
|
|
295
|
+
id='value-kind-switcher',
|
|
296
|
+
):
|
|
297
|
+
yield Input(
|
|
298
|
+
value=self._initial_value
|
|
299
|
+
if self._initial_value_kind == _ValueKind.TEXT
|
|
300
|
+
else '',
|
|
301
|
+
placeholder='Value',
|
|
302
|
+
select_on_focus=False,
|
|
303
|
+
id='value-text',
|
|
304
|
+
)
|
|
305
|
+
yield PathChooser.file(
|
|
306
|
+
path=self._initial_value
|
|
307
|
+
if self._initial_value_kind == _ValueKind.FILE
|
|
308
|
+
else None,
|
|
309
|
+
id='value-file',
|
|
310
|
+
)
|
|
311
|
+
yield Button(label='➖', tooltip='Remove field', id='remove')
|
|
312
|
+
|
|
313
|
+
def on_mount(self) -> None:
|
|
314
|
+
self.value_kind_switcher = self.query_one(
|
|
315
|
+
'#value-kind-switcher', ContentSwitcher
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
self.value_kind_radioset = self.query_one('#value-kind', RadioSet)
|
|
319
|
+
self.value_kind_text_radio_button = self.query_one(
|
|
320
|
+
'#value-kind-text', RadioButton
|
|
321
|
+
)
|
|
322
|
+
self.value_kind_file_radio_button = self.query_one(
|
|
323
|
+
'#value-kind-file', RadioButton
|
|
324
|
+
)
|
|
325
|
+
self.enabled_switch = self.query_one('#enabled', Switch)
|
|
326
|
+
self.key_input = self.query_one('#key', Input)
|
|
327
|
+
self.value_text_input = self.query_one('#value-text', Input)
|
|
328
|
+
self.value_file_input = self.query_one('#value-file', PathChooser)
|
|
329
|
+
self.remove_button = self.query_one('#remove', Button)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def enabled(self) -> bool:
|
|
333
|
+
return self.enabled_switch.value
|
|
334
|
+
|
|
335
|
+
@enabled.setter
|
|
336
|
+
def enabled(self, value: bool) -> None:
|
|
337
|
+
self.enabled_switch.value = value
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def key(self) -> str:
|
|
341
|
+
return self.key_input.value
|
|
342
|
+
|
|
343
|
+
@key.setter
|
|
344
|
+
def key(self, value: str) -> None:
|
|
345
|
+
self.key_input.value = value
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def value(self) -> str | Path | None:
|
|
349
|
+
if self.value_kind == _ValueKind.TEXT:
|
|
350
|
+
return self.value_text_input.value
|
|
351
|
+
elif self.value_kind == _ValueKind.FILE:
|
|
352
|
+
return self.value_file_input.path
|
|
353
|
+
|
|
354
|
+
@value.setter
|
|
355
|
+
def value(self, value: str | Path | None) -> None:
|
|
356
|
+
if isinstance(value, str):
|
|
357
|
+
self.value_text_input.value = value
|
|
358
|
+
elif isinstance(value, Path) or value is None:
|
|
359
|
+
self.value_file_input.path = value
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def value_kind(self) -> _ValueKind:
|
|
363
|
+
return _ValueKind(self.value_kind_radioset.pressed_button.label)
|
|
364
|
+
|
|
365
|
+
@value_kind.setter
|
|
366
|
+
def value_kind(self, value: _ValueKind) -> None:
|
|
367
|
+
if value == _ValueKind.TEXT:
|
|
368
|
+
self.value_kind_switcher.current = 'value-text'
|
|
369
|
+
self.value_kind_text_radio_button.value = True
|
|
370
|
+
elif value == _ValueKind.FILE:
|
|
371
|
+
self.value_kind_switcher.current = 'value-file'
|
|
372
|
+
self.value_kind_file_radio_button.value = True
|
|
140
373
|
|
|
141
374
|
@property
|
|
142
375
|
def is_filled(self) -> bool:
|
|
143
|
-
|
|
376
|
+
if len(self.key_input.value) > 0:
|
|
377
|
+
return True
|
|
378
|
+
elif (
|
|
379
|
+
self.value_kind == _ValueKind.TEXT
|
|
380
|
+
and len(self.value_text_input.value) > 0
|
|
381
|
+
):
|
|
382
|
+
return True
|
|
383
|
+
elif (
|
|
384
|
+
self.value_kind == _ValueKind.FILE
|
|
385
|
+
and self.value_file_input.path is not None
|
|
386
|
+
):
|
|
387
|
+
return True
|
|
388
|
+
else:
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def is_empty(self) -> bool:
|
|
393
|
+
return not self.is_filled
|
|
394
|
+
|
|
395
|
+
@on(RadioSet.Changed, '#value-kind')
|
|
396
|
+
def on_value_kind_changed(self, message: RadioSet.Changed) -> None:
|
|
397
|
+
self.value_kind = _ValueKind(message.pressed.label)
|
|
144
398
|
|
|
145
|
-
@on(Switch.Changed)
|
|
399
|
+
@on(Switch.Changed, '#enabled')
|
|
146
400
|
def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
|
|
147
401
|
if message.value is True:
|
|
148
|
-
self.post_message(self.Enabled(
|
|
402
|
+
self.post_message(self.Enabled(field=self))
|
|
149
403
|
elif message.value is False:
|
|
150
|
-
self.post_message(message=self.Disabled(
|
|
404
|
+
self.post_message(message=self.Disabled(field=self))
|
|
151
405
|
|
|
152
|
-
@on(Input.Changed)
|
|
153
|
-
|
|
406
|
+
@on(Input.Changed, '#key')
|
|
407
|
+
@on(Input.Changed, '#value-text')
|
|
408
|
+
@on(PathChooser.Changed, '#value-file')
|
|
409
|
+
def on_input_changed(
|
|
410
|
+
self, message: Input.Changed | PathChooser.Changed
|
|
411
|
+
) -> None:
|
|
154
412
|
self.enabled_switch.value = True
|
|
155
413
|
|
|
156
414
|
if self.is_empty:
|
|
157
|
-
self.post_message(message=self.Empty(
|
|
415
|
+
self.post_message(message=self.Empty(field=self))
|
|
158
416
|
elif self.is_filled:
|
|
159
|
-
self.post_message(message=self.Filled(
|
|
417
|
+
self.post_message(message=self.Filled(field=self))
|
|
160
418
|
|
|
161
|
-
@on(Button.Pressed)
|
|
419
|
+
@on(Button.Pressed, '#remove')
|
|
162
420
|
def on_remove_requested(self, message: Button.Pressed) -> None:
|
|
163
|
-
self.post_message(self.RemoveRequested(
|
|
421
|
+
self.post_message(self.RemoveRequested(field=self))
|
|
164
422
|
|
|
165
423
|
|
|
166
|
-
class DynamicFields(
|
|
424
|
+
class DynamicFields(Widget):
|
|
167
425
|
"""
|
|
168
426
|
Enableable and removable fields
|
|
169
427
|
"""
|
|
170
428
|
|
|
429
|
+
DEFAULT_CSS = """
|
|
430
|
+
DynamicFields {
|
|
431
|
+
width: auto;
|
|
432
|
+
height: 1fr;
|
|
433
|
+
}
|
|
434
|
+
"""
|
|
435
|
+
|
|
171
436
|
class FieldEmpty(Message):
|
|
172
437
|
"""
|
|
173
438
|
Sent when one of the fields becomes empty.
|
|
174
439
|
"""
|
|
175
440
|
|
|
176
441
|
def __init__(
|
|
177
|
-
self,
|
|
442
|
+
self, fields: 'DynamicFields', field: DynamicField
|
|
178
443
|
) -> None:
|
|
179
444
|
super().__init__()
|
|
180
|
-
self.
|
|
445
|
+
self.fields = fields
|
|
181
446
|
self.field = field
|
|
182
447
|
|
|
183
448
|
@property
|
|
184
449
|
def control(self) -> 'DynamicFields':
|
|
185
|
-
return self.
|
|
450
|
+
return self.fields
|
|
186
451
|
|
|
187
452
|
class FieldFilled(Message):
|
|
188
453
|
"""
|
|
@@ -190,29 +455,31 @@ class DynamicFields(Static):
|
|
|
190
455
|
"""
|
|
191
456
|
|
|
192
457
|
def __init__(
|
|
193
|
-
self,
|
|
458
|
+
self, fields: 'DynamicFields', field: DynamicField
|
|
194
459
|
) -> None:
|
|
195
460
|
super().__init__()
|
|
196
|
-
self.
|
|
461
|
+
self.fields = fields
|
|
197
462
|
self.field = field
|
|
198
463
|
|
|
199
464
|
@property
|
|
200
465
|
def control(self) -> 'DynamicFields':
|
|
201
|
-
return self.
|
|
466
|
+
return self.fields
|
|
202
467
|
|
|
203
|
-
def __init__(
|
|
468
|
+
def __init__(
|
|
469
|
+
self,
|
|
470
|
+
fields: list[DynamicField],
|
|
471
|
+
*args,
|
|
472
|
+
**kwargs,
|
|
473
|
+
) -> None:
|
|
204
474
|
super().__init__(*args, **kwargs)
|
|
205
|
-
self.
|
|
475
|
+
self._fields = fields
|
|
206
476
|
|
|
207
477
|
def compose(self) -> ComposeResult:
|
|
208
|
-
|
|
478
|
+
with VerticalScroll(can_focus=False):
|
|
479
|
+
yield from self._fields
|
|
209
480
|
|
|
210
|
-
|
|
211
|
-
self.fields_container
|
|
212
|
-
|
|
213
|
-
# Set initial_fields
|
|
214
|
-
for field in self._initial_fields:
|
|
215
|
-
await self.add_field(field=field)
|
|
481
|
+
def on_mount(self) -> None:
|
|
482
|
+
self.fields_container = self.query_one(VerticalScroll)
|
|
216
483
|
|
|
217
484
|
@property
|
|
218
485
|
def fields(self) -> list[DynamicField]:
|
|
@@ -220,14 +487,14 @@ class DynamicFields(Static):
|
|
|
220
487
|
|
|
221
488
|
@property
|
|
222
489
|
def empty_fields(self) -> list[DynamicField]:
|
|
223
|
-
return [field for field in self.
|
|
490
|
+
return [field for field in self.fields if field.is_empty]
|
|
224
491
|
|
|
225
492
|
@property
|
|
226
493
|
def filled_fields(self) -> list[DynamicField]:
|
|
227
|
-
return [field for field in self.
|
|
494
|
+
return [field for field in self.fields if field.is_filled]
|
|
228
495
|
|
|
229
496
|
@property
|
|
230
|
-
def values(self) -> list[dict[str, str | bool]]:
|
|
497
|
+
def values(self) -> list[dict[str, str | bool | Path | None]]:
|
|
231
498
|
return [
|
|
232
499
|
{
|
|
233
500
|
'enabled': field.enabled,
|
|
@@ -237,51 +504,63 @@ class DynamicFields(Static):
|
|
|
237
504
|
for field in self.fields
|
|
238
505
|
]
|
|
239
506
|
|
|
507
|
+
async def add_field(self, field: DynamicField) -> None:
|
|
508
|
+
await self.fields_container.mount(field)
|
|
509
|
+
|
|
510
|
+
def remove_field(self, field: DynamicField) -> None:
|
|
511
|
+
field.add_class('hidden')
|
|
512
|
+
field.remove()
|
|
513
|
+
|
|
240
514
|
@on(DynamicField.Empty)
|
|
241
|
-
|
|
242
|
-
|
|
515
|
+
def _on_field_is_empty(self, message: DynamicField.Empty) -> None:
|
|
516
|
+
self._focus_neighbor_field_then_remove(field=message.field)
|
|
243
517
|
self.post_message(
|
|
244
|
-
message=self.FieldEmpty(
|
|
518
|
+
message=self.FieldEmpty(fields=self, field=message.field)
|
|
245
519
|
)
|
|
246
520
|
|
|
247
521
|
@on(DynamicField.Filled)
|
|
248
|
-
async def
|
|
522
|
+
async def _on_field_is_filled(self, message: DynamicField.Filled) -> None:
|
|
249
523
|
if len(self.empty_fields) == 0:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
524
|
+
field = message.field
|
|
525
|
+
if isinstance(field, TextDynamicField):
|
|
526
|
+
await self.add_field(
|
|
527
|
+
TextDynamicField(enabled=False, key='', value='')
|
|
528
|
+
)
|
|
529
|
+
elif isinstance(field, TextOrFileDynamicField):
|
|
530
|
+
await self.add_field(
|
|
531
|
+
TextOrFileDynamicField(
|
|
532
|
+
enabled=False,
|
|
533
|
+
key='',
|
|
534
|
+
value='',
|
|
535
|
+
value_kind=_ValueKind.TEXT,
|
|
536
|
+
)
|
|
537
|
+
)
|
|
253
538
|
|
|
254
539
|
self.post_message(
|
|
255
|
-
message=self.FieldFilled(
|
|
540
|
+
message=self.FieldFilled(fields=self, field=message.field)
|
|
256
541
|
)
|
|
257
542
|
|
|
258
543
|
@on(DynamicField.RemoveRequested)
|
|
259
|
-
|
|
544
|
+
def _on_field_remove_requested(
|
|
260
545
|
self, message: DynamicField.RemoveRequested
|
|
261
546
|
) -> None:
|
|
262
|
-
|
|
547
|
+
self._focus_neighbor_field_then_remove(field=message.field)
|
|
263
548
|
|
|
264
|
-
|
|
265
|
-
await self.fields_container.mount(field)
|
|
266
|
-
|
|
267
|
-
async def remove_field(self, field: DynamicField) -> None:
|
|
549
|
+
def _focus_neighbor_field_then_remove(self, field: DynamicField) -> None:
|
|
268
550
|
if len(self.fields) == 1:
|
|
269
551
|
self.app.bell()
|
|
270
552
|
return
|
|
271
|
-
elif self.fields[-1]
|
|
553
|
+
elif field is self.fields[-1]:
|
|
272
554
|
self.app.bell()
|
|
273
555
|
return
|
|
274
556
|
|
|
275
|
-
|
|
276
|
-
self.app.screen.focus_next()
|
|
277
|
-
self.app.screen.focus_next()
|
|
278
|
-
self.app.screen.focus_next()
|
|
279
|
-
self.app.screen.focus_next()
|
|
280
|
-
elif self.fields[-2] is field: # Penultimate field
|
|
281
|
-
self.app.screen.focus_previous()
|
|
282
|
-
self.app.screen.focus_previous()
|
|
283
|
-
self.app.screen.focus_previous()
|
|
284
|
-
self.app.screen.focus_previous()
|
|
557
|
+
field_index = self.fields.index(field)
|
|
285
558
|
|
|
286
|
-
|
|
287
|
-
|
|
559
|
+
neighbor_field = None
|
|
560
|
+
if field_index == 0:
|
|
561
|
+
neighbor_field = self.fields[field_index + 1]
|
|
562
|
+
else:
|
|
563
|
+
neighbor_field = self.fields[field_index - 1]
|
|
564
|
+
|
|
565
|
+
self.app.set_focus(neighbor_field.query_one(Input))
|
|
566
|
+
self.remove_field(field=field)
|