restiny 0.2.0__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 -178
- restiny/core/request_area.py +76 -77
- restiny/core/response_area.py +53 -20
- restiny/core/url_area.py +28 -20
- restiny/widgets/dynamic_fields.py +129 -103
- restiny/widgets/path_chooser.py +49 -28
- {restiny-0.2.0.dist-info → restiny-0.2.1.dist-info}/METADATA +1 -1
- restiny-0.2.1.dist-info/RECORD +24 -0
- restiny/screens/__init__.py +0 -0
- restiny/screens/dialog.py +0 -109
- restiny-0.2.0.dist-info/RECORD +0 -26
- {restiny-0.2.0.dist-info → restiny-0.2.1.dist-info}/WHEEL +0 -0
- {restiny-0.2.0.dist-info → restiny-0.2.1.dist-info}/entry_points.txt +0 -0
- {restiny-0.2.0.dist-info → restiny-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.2.0.dist-info → restiny-0.2.1.dist-info}/top_level.txt +0 -0
restiny/core/response_area.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
|
|
1
4
|
from textual import on
|
|
2
5
|
from textual.app import ComposeResult
|
|
3
|
-
from textual.
|
|
6
|
+
from textual.containers import VerticalScroll
|
|
4
7
|
from textual.widgets import (
|
|
5
8
|
ContentSwitcher,
|
|
6
9
|
DataTable,
|
|
@@ -15,6 +18,16 @@ from restiny.enums import BodyRawLanguage
|
|
|
15
18
|
from restiny.widgets import CustomTextArea
|
|
16
19
|
|
|
17
20
|
|
|
21
|
+
@dataclass
|
|
22
|
+
class ResponseAreaData:
|
|
23
|
+
status: HTTPStatus
|
|
24
|
+
size: int
|
|
25
|
+
elapsed_time: float | int
|
|
26
|
+
headers: dict
|
|
27
|
+
body_raw_language: BodyRawLanguage
|
|
28
|
+
body_raw: str
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
# TODO: Implement 'Trace' tab pane
|
|
19
32
|
class ResponseArea(Static):
|
|
20
33
|
ALLOW_MAXIMIZE = True
|
|
@@ -38,8 +51,6 @@ class ResponseArea(Static):
|
|
|
38
51
|
|
|
39
52
|
"""
|
|
40
53
|
|
|
41
|
-
has_response: bool = Reactive(False, layout=True, init=True)
|
|
42
|
-
|
|
43
54
|
def compose(self) -> ComposeResult:
|
|
44
55
|
with ContentSwitcher(id='response-switcher', initial='no-content'):
|
|
45
56
|
yield Label(
|
|
@@ -49,7 +60,8 @@ class ResponseArea(Static):
|
|
|
49
60
|
|
|
50
61
|
with TabbedContent(id='content'):
|
|
51
62
|
with TabPane('Headers'):
|
|
52
|
-
|
|
63
|
+
with VerticalScroll():
|
|
64
|
+
yield DataTable(show_cursor=False, id='headers')
|
|
53
65
|
with TabPane('Body'):
|
|
54
66
|
yield Select(
|
|
55
67
|
(
|
|
@@ -60,11 +72,11 @@ class ResponseArea(Static):
|
|
|
60
72
|
('XML', BodyRawLanguage.XML),
|
|
61
73
|
),
|
|
62
74
|
allow_blank=False,
|
|
63
|
-
tooltip='
|
|
64
|
-
id='body-
|
|
75
|
+
tooltip='Syntax highlighting for the response body',
|
|
76
|
+
id='body-raw-language',
|
|
65
77
|
)
|
|
66
78
|
yield CustomTextArea.code_editor(
|
|
67
|
-
id='body', read_only=True, classes='mt-1'
|
|
79
|
+
id='body-raw', read_only=True, classes='mt-1'
|
|
68
80
|
)
|
|
69
81
|
|
|
70
82
|
def on_mount(self) -> None:
|
|
@@ -73,25 +85,46 @@ class ResponseArea(Static):
|
|
|
73
85
|
)
|
|
74
86
|
|
|
75
87
|
self.headers_data_table = self.query_one('#headers', DataTable)
|
|
76
|
-
self.
|
|
77
|
-
|
|
88
|
+
self.body_raw_language_select = self.query_one(
|
|
89
|
+
'#body-raw-language', Select
|
|
90
|
+
)
|
|
91
|
+
self.body_raw_editor = self.query_one('#body-raw', CustomTextArea)
|
|
78
92
|
|
|
79
93
|
self.headers_data_table.add_columns('Key', 'Value')
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
95
|
+
def set_data(self, data: ResponseAreaData | None) -> None:
|
|
96
|
+
self.border_title = self.BORDER_TITLE
|
|
97
|
+
self.border_subtitle = ''
|
|
98
|
+
self.headers_data_table.clear()
|
|
99
|
+
self.body_raw_language_select.value = BodyRawLanguage.PLAIN
|
|
100
|
+
self.body_raw_editor.clear()
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
if data is None:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self.border_title = f'Response - {data.status} {data.status.phrase}'
|
|
106
|
+
self.border_subtitle = (
|
|
107
|
+
f'{data.size} bytes in {data.elapsed_time} seconds'
|
|
108
|
+
)
|
|
109
|
+
for header_key, header_value in data.headers.items():
|
|
110
|
+
self.headers_data_table.add_row(header_key, header_value)
|
|
111
|
+
self.body_raw_language_select.value = data.body_raw_language
|
|
112
|
+
self.body_raw_editor.text = data.body_raw
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_showing_response(self) -> bool:
|
|
116
|
+
if self._response_switcher.current == 'content':
|
|
117
|
+
return True
|
|
118
|
+
elif self._response_switcher.current == 'no-content':
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
@is_showing_response.setter
|
|
122
|
+
def is_showing_response(self, value: bool) -> None:
|
|
86
123
|
if value is True:
|
|
87
124
|
self._response_switcher.current = 'content'
|
|
88
125
|
elif value is False:
|
|
89
126
|
self._response_switcher.current = 'no-content'
|
|
90
|
-
self.reset_response()
|
|
91
127
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.
|
|
95
|
-
self.headers_data_table.clear()
|
|
96
|
-
self.body_type_select.value = BodyRawLanguage.PLAIN
|
|
97
|
-
self.body_text_area.clear()
|
|
128
|
+
@on(Select.Changed, '#body-raw-language')
|
|
129
|
+
def _on_body_raw_language_changed(self, message: Select.Changed) -> None:
|
|
130
|
+
self.body_raw_editor.language = self.body_raw_language_select.value
|
restiny/core/url_area.py
CHANGED
|
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
|
|
3
3
|
from textual import on
|
|
4
4
|
from textual.app import ComposeResult
|
|
5
5
|
from textual.message import Message
|
|
6
|
-
from textual.reactive import Reactive
|
|
7
6
|
from textual.widgets import Button, ContentSwitcher, Input, Select, Static
|
|
8
7
|
|
|
9
8
|
from restiny.enums import HTTPMethod
|
|
@@ -29,8 +28,6 @@ class URLArea(Static):
|
|
|
29
28
|
}
|
|
30
29
|
"""
|
|
31
30
|
|
|
32
|
-
request_pending = Reactive(False)
|
|
33
|
-
|
|
34
31
|
class SendRequest(Message):
|
|
35
32
|
"""
|
|
36
33
|
Sent when the user send a request.
|
|
@@ -47,11 +44,15 @@ class URLArea(Static):
|
|
|
47
44
|
def __init__(self) -> None:
|
|
48
45
|
super().__init__()
|
|
49
46
|
|
|
47
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
self._request_pending = False
|
|
50
|
+
|
|
50
51
|
def compose(self) -> ComposeResult:
|
|
51
52
|
yield Select.from_values(
|
|
52
53
|
values=HTTPMethod.values(), allow_blank=False, id='method'
|
|
53
54
|
)
|
|
54
|
-
yield Input(placeholder='Enter URL', id='url')
|
|
55
|
+
yield Input(placeholder='Enter URL', select_on_focus=False, id='url')
|
|
55
56
|
with ContentSwitcher(
|
|
56
57
|
id='request-button-switcher', initial='send-request'
|
|
57
58
|
):
|
|
@@ -78,9 +79,28 @@ class URLArea(Static):
|
|
|
78
79
|
self.send_request_button = self.query_one('#send-request', Button)
|
|
79
80
|
self.cancel_request_button = self.query_one('#cancel-request', Button)
|
|
80
81
|
|
|
82
|
+
def get_data(self) -> URLAreaData:
|
|
83
|
+
return URLAreaData(
|
|
84
|
+
method=self.method_select.value,
|
|
85
|
+
url=self.url_input.value,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def request_pending(self) -> bool:
|
|
90
|
+
return self._request_pending
|
|
91
|
+
|
|
92
|
+
@request_pending.setter
|
|
93
|
+
def request_pending(self, value: bool) -> None:
|
|
94
|
+
if value is True:
|
|
95
|
+
self._request_button_switcher.current = 'cancel-request'
|
|
96
|
+
elif value is False:
|
|
97
|
+
self._request_button_switcher.current = 'send-request'
|
|
98
|
+
|
|
99
|
+
self._request_pending = value
|
|
100
|
+
|
|
81
101
|
@on(Button.Pressed, '#send-request')
|
|
82
|
-
@on(Input.Submitted)
|
|
83
|
-
def
|
|
102
|
+
@on(Input.Submitted, '#url')
|
|
103
|
+
def _on_send_request(
|
|
84
104
|
self, message: Button.Pressed | Input.Submitted
|
|
85
105
|
) -> None:
|
|
86
106
|
if self.request_pending:
|
|
@@ -88,22 +108,10 @@ class URLArea(Static):
|
|
|
88
108
|
|
|
89
109
|
self.post_message(message=self.SendRequest())
|
|
90
110
|
|
|
91
|
-
@on(Input.Submitted)
|
|
92
111
|
@on(Button.Pressed, '#cancel-request')
|
|
93
|
-
|
|
112
|
+
@on(Input.Submitted, '#url')
|
|
113
|
+
def _on_cancel_request(self, message: Button.Pressed) -> None:
|
|
94
114
|
if not self.request_pending:
|
|
95
115
|
return
|
|
96
116
|
|
|
97
117
|
self.post_message(message=self.CancelRequest())
|
|
98
|
-
|
|
99
|
-
def watch_request_pending(self, value: bool) -> None:
|
|
100
|
-
if value is True:
|
|
101
|
-
self._request_button_switcher.current = 'cancel-request'
|
|
102
|
-
elif value is False:
|
|
103
|
-
self._request_button_switcher.current = 'send-request'
|
|
104
|
-
|
|
105
|
-
def get_data(self) -> URLAreaData:
|
|
106
|
-
return URLAreaData(
|
|
107
|
-
method=self.method_select.value,
|
|
108
|
-
url=self.url_input.value,
|
|
109
|
-
)
|
|
@@ -6,20 +6,20 @@ from textual import on
|
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import VerticalScroll
|
|
8
8
|
from textual.message import Message
|
|
9
|
+
from textual.widget import Widget
|
|
9
10
|
from textual.widgets import (
|
|
10
11
|
Button,
|
|
11
12
|
ContentSwitcher,
|
|
12
13
|
Input,
|
|
13
14
|
RadioButton,
|
|
14
15
|
RadioSet,
|
|
15
|
-
Static,
|
|
16
16
|
Switch,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from restiny.widgets.path_chooser import PathChooser
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class DynamicField(
|
|
22
|
+
class DynamicField(Widget):
|
|
23
23
|
@abstractmethod
|
|
24
24
|
def compose(self) -> ComposeResult: ...
|
|
25
25
|
|
|
@@ -130,6 +130,8 @@ class TextDynamicField(DynamicField):
|
|
|
130
130
|
|
|
131
131
|
DEFAULT_CSS = """
|
|
132
132
|
TextDynamicField {
|
|
133
|
+
width: 100%;
|
|
134
|
+
height: auto;
|
|
133
135
|
layout: grid;
|
|
134
136
|
grid-size: 4 1;
|
|
135
137
|
grid-columns: auto 1fr 2fr auto; /* Set 1:2 ratio between Inputs */
|
|
@@ -140,23 +142,35 @@ class TextDynamicField(DynamicField):
|
|
|
140
142
|
self, enabled: bool, key: str, value: str, *args, **kwargs
|
|
141
143
|
) -> None:
|
|
142
144
|
super().__init__(*args, **kwargs)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
self.
|
|
146
|
-
self._key = key
|
|
147
|
-
self._value = value
|
|
145
|
+
self._initial_enabled = enabled
|
|
146
|
+
self._initial_key = key
|
|
147
|
+
self._initial_value = value
|
|
148
148
|
|
|
149
149
|
def compose(self) -> ComposeResult:
|
|
150
|
-
yield Switch(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
yield Switch(
|
|
151
|
+
value=self._initial_enabled,
|
|
152
|
+
tooltip='Send this field?',
|
|
153
|
+
id='enabled',
|
|
154
|
+
)
|
|
155
|
+
yield Input(
|
|
156
|
+
value=self._initial_key,
|
|
157
|
+
placeholder='Key',
|
|
158
|
+
select_on_focus=False,
|
|
159
|
+
id='key',
|
|
160
|
+
)
|
|
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')
|
|
154
168
|
|
|
155
|
-
def on_mount(self) -> None:
|
|
156
|
-
self.enabled_switch
|
|
157
|
-
self.key_input
|
|
158
|
-
self.value_input
|
|
159
|
-
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)
|
|
160
174
|
|
|
161
175
|
@property
|
|
162
176
|
def enabled(self) -> bool:
|
|
@@ -190,14 +204,15 @@ class TextDynamicField(DynamicField):
|
|
|
190
204
|
def is_empty(self) -> bool:
|
|
191
205
|
return not self.is_filled
|
|
192
206
|
|
|
193
|
-
@on(Switch.Changed)
|
|
207
|
+
@on(Switch.Changed, '#enabled')
|
|
194
208
|
def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
|
|
195
209
|
if message.value is True:
|
|
196
210
|
self.post_message(self.Enabled(field=self))
|
|
197
211
|
elif message.value is False:
|
|
198
212
|
self.post_message(message=self.Disabled(field=self))
|
|
199
213
|
|
|
200
|
-
@on(Input.Changed)
|
|
214
|
+
@on(Input.Changed, '#key')
|
|
215
|
+
@on(Input.Changed, '#value')
|
|
201
216
|
def on_input_changed(self, message: Input.Changed) -> None:
|
|
202
217
|
self.enabled_switch.value = True
|
|
203
218
|
|
|
@@ -206,12 +221,12 @@ class TextDynamicField(DynamicField):
|
|
|
206
221
|
elif self.is_filled:
|
|
207
222
|
self.post_message(message=self.Filled(field=self))
|
|
208
223
|
|
|
209
|
-
@on(Button.Pressed)
|
|
224
|
+
@on(Button.Pressed, '#remove')
|
|
210
225
|
def on_remove_requested(self, message: Button.Pressed) -> None:
|
|
211
226
|
self.post_message(self.RemoveRequested(field=self))
|
|
212
227
|
|
|
213
228
|
|
|
214
|
-
class
|
|
229
|
+
class _ValueKind(StrEnum):
|
|
215
230
|
TEXT = 'text'
|
|
216
231
|
FILE = 'file'
|
|
217
232
|
|
|
@@ -240,66 +255,72 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
240
255
|
enabled: bool = False,
|
|
241
256
|
key: str = '',
|
|
242
257
|
value: str | Path | None = '',
|
|
243
|
-
|
|
258
|
+
value_kind: _ValueKind = _ValueKind.TEXT,
|
|
244
259
|
*args,
|
|
245
260
|
**kwargs,
|
|
246
261
|
) -> None:
|
|
247
262
|
super().__init__(*args, **kwargs)
|
|
248
|
-
self.
|
|
249
|
-
self.
|
|
250
|
-
self.
|
|
251
|
-
self.
|
|
263
|
+
self._initial_enabled = enabled
|
|
264
|
+
self._initial_key = key
|
|
265
|
+
self._initial_value = value
|
|
266
|
+
self._initial_value_kind = value_kind
|
|
252
267
|
|
|
253
268
|
def compose(self) -> ComposeResult:
|
|
254
|
-
with RadioSet(id='value-
|
|
269
|
+
with RadioSet(id='value-kind', compact=True):
|
|
255
270
|
yield RadioButton(
|
|
256
|
-
label=
|
|
257
|
-
value=bool(self.
|
|
258
|
-
id='value-
|
|
271
|
+
label=_ValueKind.TEXT,
|
|
272
|
+
value=bool(self._initial_value_kind == _ValueKind.TEXT),
|
|
273
|
+
id='value-kind-text',
|
|
259
274
|
)
|
|
260
275
|
yield RadioButton(
|
|
261
|
-
label=
|
|
262
|
-
value=bool(self.
|
|
263
|
-
id='value-
|
|
276
|
+
label=_ValueKind.FILE,
|
|
277
|
+
value=bool(self._initial_value_kind == _ValueKind.FILE),
|
|
278
|
+
id='value-kind-file',
|
|
264
279
|
)
|
|
265
280
|
yield Switch(
|
|
266
|
-
value=self.
|
|
281
|
+
value=self._initial_enabled,
|
|
267
282
|
tooltip='Send this field?',
|
|
268
283
|
id='enabled',
|
|
269
284
|
)
|
|
270
|
-
yield Input(
|
|
285
|
+
yield Input(
|
|
286
|
+
value=self._initial_key,
|
|
287
|
+
placeholder='Key',
|
|
288
|
+
select_on_focus=False,
|
|
289
|
+
id='key',
|
|
290
|
+
)
|
|
271
291
|
with ContentSwitcher(
|
|
272
292
|
initial='value-text'
|
|
273
|
-
if self.
|
|
293
|
+
if self._initial_value_kind == _ValueKind.TEXT
|
|
274
294
|
else 'value-file',
|
|
275
|
-
id='value-
|
|
295
|
+
id='value-kind-switcher',
|
|
276
296
|
):
|
|
277
297
|
yield Input(
|
|
278
|
-
value=self.
|
|
279
|
-
if self.
|
|
298
|
+
value=self._initial_value
|
|
299
|
+
if self._initial_value_kind == _ValueKind.TEXT
|
|
280
300
|
else '',
|
|
281
301
|
placeholder='Value',
|
|
302
|
+
select_on_focus=False,
|
|
282
303
|
id='value-text',
|
|
283
304
|
)
|
|
284
305
|
yield PathChooser.file(
|
|
285
|
-
path=self.
|
|
286
|
-
if self.
|
|
306
|
+
path=self._initial_value
|
|
307
|
+
if self._initial_value_kind == _ValueKind.FILE
|
|
287
308
|
else None,
|
|
288
309
|
id='value-file',
|
|
289
310
|
)
|
|
290
311
|
yield Button(label='➖', tooltip='Remove field', id='remove')
|
|
291
312
|
|
|
292
313
|
def on_mount(self) -> None:
|
|
293
|
-
self.
|
|
294
|
-
'#value-
|
|
314
|
+
self.value_kind_switcher = self.query_one(
|
|
315
|
+
'#value-kind-switcher', ContentSwitcher
|
|
295
316
|
)
|
|
296
317
|
|
|
297
|
-
self.
|
|
298
|
-
self.
|
|
299
|
-
'#value-
|
|
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
|
|
300
321
|
)
|
|
301
|
-
self.
|
|
302
|
-
'#value-
|
|
322
|
+
self.value_kind_file_radio_button = self.query_one(
|
|
323
|
+
'#value-kind-file', RadioButton
|
|
303
324
|
)
|
|
304
325
|
self.enabled_switch = self.query_one('#enabled', Switch)
|
|
305
326
|
self.key_input = self.query_one('#key', Input)
|
|
@@ -325,42 +346,42 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
325
346
|
|
|
326
347
|
@property
|
|
327
348
|
def value(self) -> str | Path | None:
|
|
328
|
-
if self.
|
|
349
|
+
if self.value_kind == _ValueKind.TEXT:
|
|
329
350
|
return self.value_text_input.value
|
|
330
|
-
elif self.
|
|
351
|
+
elif self.value_kind == _ValueKind.FILE:
|
|
331
352
|
return self.value_file_input.path
|
|
332
353
|
|
|
333
354
|
@value.setter
|
|
334
|
-
def value(self, value: str | Path) -> None:
|
|
355
|
+
def value(self, value: str | Path | None) -> None:
|
|
335
356
|
if isinstance(value, str):
|
|
336
357
|
self.value_text_input.value = value
|
|
337
|
-
elif isinstance(value, Path):
|
|
358
|
+
elif isinstance(value, Path) or value is None:
|
|
338
359
|
self.value_file_input.path = value
|
|
339
360
|
|
|
340
361
|
@property
|
|
341
|
-
def
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
@
|
|
345
|
-
def
|
|
346
|
-
if value ==
|
|
347
|
-
self.
|
|
348
|
-
self.
|
|
349
|
-
elif value ==
|
|
350
|
-
self.
|
|
351
|
-
self.
|
|
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
|
|
352
373
|
|
|
353
374
|
@property
|
|
354
375
|
def is_filled(self) -> bool:
|
|
355
376
|
if len(self.key_input.value) > 0:
|
|
356
377
|
return True
|
|
357
378
|
elif (
|
|
358
|
-
self.
|
|
379
|
+
self.value_kind == _ValueKind.TEXT
|
|
359
380
|
and len(self.value_text_input.value) > 0
|
|
360
381
|
):
|
|
361
382
|
return True
|
|
362
383
|
elif (
|
|
363
|
-
self.
|
|
384
|
+
self.value_kind == _ValueKind.FILE
|
|
364
385
|
and self.value_file_input.path is not None
|
|
365
386
|
):
|
|
366
387
|
return True
|
|
@@ -371,9 +392,9 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
371
392
|
def is_empty(self) -> bool:
|
|
372
393
|
return not self.is_filled
|
|
373
394
|
|
|
374
|
-
@on(RadioSet.Changed, '#value-
|
|
375
|
-
def
|
|
376
|
-
self.
|
|
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)
|
|
377
398
|
|
|
378
399
|
@on(Switch.Changed, '#enabled')
|
|
379
400
|
def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
|
|
@@ -400,11 +421,18 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
400
421
|
self.post_message(self.RemoveRequested(field=self))
|
|
401
422
|
|
|
402
423
|
|
|
403
|
-
class DynamicFields(
|
|
424
|
+
class DynamicFields(Widget):
|
|
404
425
|
"""
|
|
405
426
|
Enableable and removable fields
|
|
406
427
|
"""
|
|
407
428
|
|
|
429
|
+
DEFAULT_CSS = """
|
|
430
|
+
DynamicFields {
|
|
431
|
+
width: auto;
|
|
432
|
+
height: 1fr;
|
|
433
|
+
}
|
|
434
|
+
"""
|
|
435
|
+
|
|
408
436
|
class FieldEmpty(Message):
|
|
409
437
|
"""
|
|
410
438
|
Sent when one of the fields becomes empty.
|
|
@@ -447,15 +475,12 @@ class DynamicFields(Static):
|
|
|
447
475
|
self._fields = fields
|
|
448
476
|
|
|
449
477
|
def compose(self) -> ComposeResult:
|
|
450
|
-
|
|
478
|
+
with VerticalScroll(can_focus=False):
|
|
479
|
+
yield from self._fields
|
|
451
480
|
|
|
452
|
-
|
|
481
|
+
def on_mount(self) -> None:
|
|
453
482
|
self.fields_container = self.query_one(VerticalScroll)
|
|
454
483
|
|
|
455
|
-
# Set initial_fields
|
|
456
|
-
for field in self._fields:
|
|
457
|
-
await self.add_field(field=field)
|
|
458
|
-
|
|
459
484
|
@property
|
|
460
485
|
def fields(self) -> list[DynamicField]:
|
|
461
486
|
return list(self.query(DynamicField))
|
|
@@ -469,7 +494,7 @@ class DynamicFields(Static):
|
|
|
469
494
|
return [field for field in self.fields if field.is_filled]
|
|
470
495
|
|
|
471
496
|
@property
|
|
472
|
-
def values(self) -> list[dict[str, str | bool]]:
|
|
497
|
+
def values(self) -> list[dict[str, str | bool | Path | None]]:
|
|
473
498
|
return [
|
|
474
499
|
{
|
|
475
500
|
'enabled': field.enabled,
|
|
@@ -479,62 +504,63 @@ class DynamicFields(Static):
|
|
|
479
504
|
for field in self.fields
|
|
480
505
|
]
|
|
481
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
|
+
|
|
482
514
|
@on(DynamicField.Empty)
|
|
483
|
-
|
|
484
|
-
|
|
515
|
+
def _on_field_is_empty(self, message: DynamicField.Empty) -> None:
|
|
516
|
+
self._focus_neighbor_field_then_remove(field=message.field)
|
|
485
517
|
self.post_message(
|
|
486
|
-
message=self.FieldEmpty(fields=self, field=message.
|
|
518
|
+
message=self.FieldEmpty(fields=self, field=message.field)
|
|
487
519
|
)
|
|
488
520
|
|
|
489
521
|
@on(DynamicField.Filled)
|
|
490
|
-
async def
|
|
522
|
+
async def _on_field_is_filled(self, message: DynamicField.Filled) -> None:
|
|
491
523
|
if len(self.empty_fields) == 0:
|
|
492
|
-
|
|
493
|
-
if isinstance(
|
|
524
|
+
field = message.field
|
|
525
|
+
if isinstance(field, TextDynamicField):
|
|
494
526
|
await self.add_field(
|
|
495
527
|
TextDynamicField(enabled=False, key='', value='')
|
|
496
528
|
)
|
|
497
|
-
elif isinstance(
|
|
529
|
+
elif isinstance(field, TextOrFileDynamicField):
|
|
498
530
|
await self.add_field(
|
|
499
531
|
TextOrFileDynamicField(
|
|
500
532
|
enabled=False,
|
|
501
533
|
key='',
|
|
502
534
|
value='',
|
|
503
|
-
|
|
535
|
+
value_kind=_ValueKind.TEXT,
|
|
504
536
|
)
|
|
505
537
|
)
|
|
506
538
|
|
|
507
539
|
self.post_message(
|
|
508
|
-
message=self.FieldFilled(fields=self, field=message.
|
|
540
|
+
message=self.FieldFilled(fields=self, field=message.field)
|
|
509
541
|
)
|
|
510
542
|
|
|
511
543
|
@on(DynamicField.RemoveRequested)
|
|
512
|
-
|
|
544
|
+
def _on_field_remove_requested(
|
|
513
545
|
self, message: DynamicField.RemoveRequested
|
|
514
546
|
) -> None:
|
|
515
|
-
|
|
547
|
+
self._focus_neighbor_field_then_remove(field=message.field)
|
|
516
548
|
|
|
517
|
-
|
|
518
|
-
await self.fields_container.mount(field)
|
|
519
|
-
|
|
520
|
-
async def remove_field(self, field: DynamicField) -> None:
|
|
549
|
+
def _focus_neighbor_field_then_remove(self, field: DynamicField) -> None:
|
|
521
550
|
if len(self.fields) == 1:
|
|
522
551
|
self.app.bell()
|
|
523
552
|
return
|
|
524
|
-
elif self.fields[-1]
|
|
553
|
+
elif field is self.fields[-1]:
|
|
525
554
|
self.app.bell()
|
|
526
555
|
return
|
|
527
556
|
|
|
528
|
-
|
|
529
|
-
self.app.screen.focus_next()
|
|
530
|
-
self.app.screen.focus_next()
|
|
531
|
-
self.app.screen.focus_next()
|
|
532
|
-
self.app.screen.focus_next()
|
|
533
|
-
elif self.fields[-2] is field: # Penultimate field
|
|
534
|
-
self.app.screen.focus_previous()
|
|
535
|
-
self.app.screen.focus_previous()
|
|
536
|
-
self.app.screen.focus_previous()
|
|
537
|
-
self.app.screen.focus_previous()
|
|
557
|
+
field_index = self.fields.index(field)
|
|
538
558
|
|
|
539
|
-
|
|
540
|
-
|
|
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)
|