runtimepy 5.14.2__py3-none-any.whl → 5.15.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.
- runtimepy/__init__.py +2 -2
- runtimepy/channel/__init__.py +1 -4
- runtimepy/channel/environment/__init__.py +93 -2
- runtimepy/channel/environment/create.py +16 -1
- runtimepy/channel/environment/sample.py +10 -2
- runtimepy/channel/registry.py +2 -3
- runtimepy/codec/protocol/base.py +34 -14
- runtimepy/codec/protocol/json.py +5 -3
- runtimepy/codec/system/__init__.py +6 -2
- runtimepy/control/source.py +1 -1
- runtimepy/data/404.md +16 -0
- runtimepy/data/base.yaml +3 -0
- runtimepy/data/css/bootstrap_extra.css +59 -44
- runtimepy/data/css/main.css +23 -4
- runtimepy/data/dummy_load.yaml +5 -2
- runtimepy/data/factories.yaml +1 -0
- runtimepy/data/js/classes/App.js +54 -2
- runtimepy/data/js/classes/ChannelTable.js +6 -8
- runtimepy/data/js/classes/Plot.js +9 -4
- runtimepy/data/js/classes/TabFilter.js +47 -9
- runtimepy/data/js/classes/TabInterface.js +106 -11
- runtimepy/data/js/classes/WindowHashManager.js +30 -15
- runtimepy/data/js/init.js +18 -1
- runtimepy/data/js/markdown_page.js +10 -0
- runtimepy/data/js/sample.js +1 -0
- runtimepy/data/schemas/BitFields.yaml +9 -0
- runtimepy/data/schemas/RuntimeEnum.yaml +6 -0
- runtimepy/data/schemas/StructConfig.yaml +9 -1
- runtimepy/data/static/css/bootstrap-icons.min.css +4 -3
- runtimepy/data/static/css/bootstrap.min.css +3 -4
- runtimepy/data/static/css/fonts/bootstrap-icons.woff +0 -0
- runtimepy/data/static/css/fonts/bootstrap-icons.woff2 +0 -0
- runtimepy/data/static/js/bootstrap.bundle.min.js +5 -4
- runtimepy/data/static/js/webglplot.umd.min.js +2 -1
- runtimepy/data/static/svg/outline-dark.svg +22 -0
- runtimepy/data/static/svg/outline-light.svg +22 -0
- runtimepy/enum/__init__.py +13 -1
- runtimepy/enum/registry.py +13 -1
- runtimepy/message/__init__.py +3 -3
- runtimepy/mixins/logging.py +6 -1
- runtimepy/net/__init__.py +0 -2
- runtimepy/net/arbiter/info.py +36 -4
- runtimepy/net/arbiter/struct/__init__.py +3 -2
- runtimepy/net/connection.py +6 -7
- runtimepy/net/html/__init__.py +29 -11
- runtimepy/net/html/bootstrap/__init__.py +2 -2
- runtimepy/net/html/bootstrap/elements.py +44 -24
- runtimepy/net/html/bootstrap/tabs.py +18 -11
- runtimepy/net/http/__init__.py +3 -3
- runtimepy/net/http/request_target.py +3 -3
- runtimepy/net/mixin.py +4 -2
- runtimepy/net/server/__init__.py +16 -9
- runtimepy/net/server/app/__init__.py +1 -0
- runtimepy/net/server/app/create.py +3 -3
- runtimepy/net/server/app/env/__init__.py +30 -4
- runtimepy/net/server/app/env/settings.py +4 -7
- runtimepy/net/server/app/env/tab/base.py +2 -1
- runtimepy/net/server/app/env/tab/controls.py +141 -27
- runtimepy/net/server/app/env/tab/html.py +68 -26
- runtimepy/net/server/app/env/widgets.py +115 -61
- runtimepy/net/server/app/landing_page.py +1 -1
- runtimepy/net/server/app/tab.py +12 -3
- runtimepy/net/server/html.py +2 -2
- runtimepy/net/server/json.py +1 -1
- runtimepy/net/server/markdown.py +29 -12
- runtimepy/net/server/mux.py +29 -0
- runtimepy/net/stream/__init__.py +6 -5
- runtimepy/net/stream/base.py +4 -2
- runtimepy/net/tcp/connection.py +5 -3
- runtimepy/net/tcp/http/__init__.py +10 -9
- runtimepy/net/tcp/protocol.py +2 -2
- runtimepy/net/tcp/scpi/__init__.py +5 -2
- runtimepy/net/tcp/telnet/__init__.py +2 -1
- runtimepy/net/udp/connection.py +10 -6
- runtimepy/net/udp/protocol.py +5 -6
- runtimepy/net/udp/queue.py +5 -2
- runtimepy/net/udp/tftp/base.py +2 -1
- runtimepy/net/websocket/connection.py +58 -9
- runtimepy/primitives/array/__init__.py +7 -5
- runtimepy/primitives/base.py +3 -2
- runtimepy/primitives/field/__init__.py +35 -2
- runtimepy/primitives/field/fields.py +11 -2
- runtimepy/primitives/field/manager/base.py +19 -2
- runtimepy/primitives/serializable/base.py +5 -2
- runtimepy/primitives/serializable/fixed.py +5 -2
- runtimepy/primitives/serializable/prefixed.py +4 -1
- runtimepy/primitives/types/base.py +4 -1
- runtimepy/primitives/types/bounds.py +10 -4
- runtimepy/registry/__init__.py +20 -0
- runtimepy/registry/name.py +6 -0
- runtimepy/requirements.txt +2 -2
- runtimepy/ui/controls.py +20 -1
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/METADATA +6 -6
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/RECORD +98 -94
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/WHEEL +1 -1
- runtimepy/data/404.html +0 -7
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/entry_points.txt +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/licenses/LICENSE +0 -0
- {runtimepy-5.14.2.dist-info → runtimepy-5.15.1.dist-info}/top_level.txt +0 -0
|
@@ -4,9 +4,9 @@ A module implementing a simple request-target (3.2) interface.
|
|
|
4
4
|
|
|
5
5
|
# built-in
|
|
6
6
|
import http
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Optional
|
|
8
8
|
|
|
9
|
-
PathMaybeQuery =
|
|
9
|
+
PathMaybeQuery = tuple[str, Optional[str]]
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class RequestTarget:
|
|
@@ -20,7 +20,7 @@ class RequestTarget:
|
|
|
20
20
|
self.raw = request_target_raw
|
|
21
21
|
|
|
22
22
|
# Host and port.
|
|
23
|
-
self.authority_form: Optional[
|
|
23
|
+
self.authority_form: Optional[tuple[str, int]] = None
|
|
24
24
|
|
|
25
25
|
# Path and optional query.
|
|
26
26
|
self.origin_form: Optional[PathMaybeQuery] = None
|
runtimepy/net/mixin.py
CHANGED
|
@@ -11,10 +11,12 @@ from typing import Callable
|
|
|
11
11
|
from typing import Optional as _Optional
|
|
12
12
|
from typing import cast as _cast
|
|
13
13
|
|
|
14
|
+
# third-party
|
|
15
|
+
from vcorelib.io import BinaryMessage
|
|
16
|
+
|
|
14
17
|
# internal
|
|
15
18
|
from runtimepy.net import IpHost as _IpHost
|
|
16
19
|
from runtimepy.net import normalize_host as _normalize_host
|
|
17
|
-
from runtimepy.net.connection import BinaryMessage as _BinaryMessage
|
|
18
20
|
from runtimepy.net.mtu import ETHERNET_MTU, UDP_DEFAULT_MTU, host_discover_mtu
|
|
19
21
|
|
|
20
22
|
|
|
@@ -23,7 +25,7 @@ class BinaryMessageQueueMixin:
|
|
|
23
25
|
|
|
24
26
|
def __init__(self) -> None:
|
|
25
27
|
"""Initialize this protocol."""
|
|
26
|
-
self.queue: _asyncio.Queue[
|
|
28
|
+
self.queue: _asyncio.Queue[BinaryMessage] = _asyncio.Queue()
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class TransportMixin:
|
runtimepy/net/server/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import mimetypes
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Optional, TextIO, Union
|
|
12
|
+
from urllib.parse import urlencode
|
|
12
13
|
|
|
13
14
|
# third-party
|
|
14
15
|
from vcorelib import DEFAULT_ENCODING
|
|
@@ -24,7 +25,8 @@ from runtimepy.net.http.request_target import PathMaybeQuery
|
|
|
24
25
|
from runtimepy.net.http.response import AsyncResponse, ResponseHeader
|
|
25
26
|
from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
|
|
26
27
|
from runtimepy.net.server.json import encode_json, json_handler
|
|
27
|
-
from runtimepy.net.server.markdown import markdown_for_dir
|
|
28
|
+
from runtimepy.net.server.markdown import DIR_FILE, markdown_for_dir
|
|
29
|
+
from runtimepy.net.server.mux import mux_app
|
|
28
30
|
from runtimepy.net.tcp.http import HttpConnection, HttpResult
|
|
29
31
|
from runtimepy.util import normalize_root, path_has_part, read_binary
|
|
30
32
|
|
|
@@ -43,7 +45,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
43
45
|
"""A class implementing a server-connection interface for this package."""
|
|
44
46
|
|
|
45
47
|
# Can register application methods to URL paths.
|
|
46
|
-
apps: HtmlApps = {}
|
|
48
|
+
apps: HtmlApps = {"/mux.html": mux_app}
|
|
47
49
|
default_app: Optional[HtmlApp] = None
|
|
48
50
|
|
|
49
51
|
# Can load additional data into this dictionary for easy HTTP access.
|
|
@@ -58,7 +60,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
58
60
|
# Set these to control meta attributes.
|
|
59
61
|
metadata: dict[str, Optional[str]] = {
|
|
60
62
|
"title": HttpConnection.identity,
|
|
61
|
-
"description":
|
|
63
|
+
"description": f"({HttpConnection.identity})",
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
def add_path(self, path: Pathlike, front: bool = False) -> None:
|
|
@@ -153,7 +155,6 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
153
155
|
|
|
154
156
|
meta: dict[str, str] = type(self).metadata.copy() # type: ignore
|
|
155
157
|
|
|
156
|
-
meta.setdefault("description", "")
|
|
157
158
|
meta["description"] += (
|
|
158
159
|
" This page was rendered from "
|
|
159
160
|
f"Markdown by {HttpConnection.identity}."
|
|
@@ -202,6 +203,10 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
202
203
|
candidates: list[Path] = []
|
|
203
204
|
for search in self.paths:
|
|
204
205
|
candidate = search.joinpath(path[0][1:])
|
|
206
|
+
|
|
207
|
+
if candidate.name == DIR_FILE:
|
|
208
|
+
candidate = candidate.parent
|
|
209
|
+
|
|
205
210
|
if candidate.is_dir():
|
|
206
211
|
directories.append((candidate, search))
|
|
207
212
|
candidates.append(candidate.joinpath("index.html"))
|
|
@@ -245,10 +250,9 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
245
250
|
|
|
246
251
|
# Handle a directory as a last resort.
|
|
247
252
|
if not result and directories:
|
|
248
|
-
candidate, search = directories[0]
|
|
249
253
|
result = self.render_markdown(
|
|
250
254
|
markdown_for_dir(
|
|
251
|
-
|
|
255
|
+
directories, {"applications": self.apps.keys()}
|
|
252
256
|
),
|
|
253
257
|
response,
|
|
254
258
|
path[1],
|
|
@@ -285,7 +289,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
285
289
|
self,
|
|
286
290
|
response: ResponseHeader,
|
|
287
291
|
request: RequestHeader,
|
|
288
|
-
request_data: Optional[
|
|
292
|
+
request_data: Optional[bytearray],
|
|
289
293
|
) -> HttpResult:
|
|
290
294
|
"""Handle POST requests."""
|
|
291
295
|
|
|
@@ -307,7 +311,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
307
311
|
self,
|
|
308
312
|
response: ResponseHeader,
|
|
309
313
|
request: RequestHeader,
|
|
310
|
-
request_data: Optional[
|
|
314
|
+
request_data: Optional[bytearray],
|
|
311
315
|
) -> HttpResult:
|
|
312
316
|
"""Handle GET requests."""
|
|
313
317
|
|
|
@@ -360,4 +364,7 @@ class RuntimepyServerConnection(HttpConnection):
|
|
|
360
364
|
if populated:
|
|
361
365
|
result = stream.getvalue().encode()
|
|
362
366
|
|
|
363
|
-
return result or self.redirect_to(
|
|
367
|
+
return result or self.redirect_to(
|
|
368
|
+
f"/404.html?{urlencode({'target': request.target.raw})}",
|
|
369
|
+
response,
|
|
370
|
+
)
|
|
@@ -74,6 +74,7 @@ async def setup(app: AppInfo) -> int:
|
|
|
74
74
|
html_app = create_app(app, getattr(_import_module(module), method))
|
|
75
75
|
target: str
|
|
76
76
|
for target in app.config_param("http_app_paths", []):
|
|
77
|
+
assert target not in RuntimepyServerConnection.apps, target
|
|
77
78
|
RuntimepyServerConnection.apps[target] = html_app
|
|
78
79
|
|
|
79
80
|
# Register redirects.
|
|
@@ -29,7 +29,7 @@ def config_param(
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
HtmlAppComposer = Callable[
|
|
32
|
-
[AppInfo, Html, RequestHeader, ResponseHeader, Optional[
|
|
32
|
+
[AppInfo, Html, RequestHeader, ResponseHeader, Optional[bytearray]], Html
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|
|
@@ -43,7 +43,7 @@ def create_cacheable_app(app: AppInfo, compose: HtmlAppComposer) -> HtmlApp:
|
|
|
43
43
|
document: Html,
|
|
44
44
|
request: RequestHeader,
|
|
45
45
|
response: ResponseHeader,
|
|
46
|
-
request_data: Optional[
|
|
46
|
+
request_data: Optional[bytearray],
|
|
47
47
|
) -> Html:
|
|
48
48
|
"""A simple 'Hello, world!' application."""
|
|
49
49
|
|
|
@@ -82,7 +82,7 @@ def create_app(
|
|
|
82
82
|
document: Html,
|
|
83
83
|
request: RequestHeader,
|
|
84
84
|
response: ResponseHeader,
|
|
85
|
-
request_data: Optional[
|
|
85
|
+
request_data: Optional[bytearray],
|
|
86
86
|
) -> Html:
|
|
87
87
|
"""Main package web application."""
|
|
88
88
|
|
|
@@ -40,6 +40,7 @@ def populate_tabs(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
40
40
|
tabs,
|
|
41
41
|
icon="arrow-repeat",
|
|
42
42
|
markdown=task.markdown,
|
|
43
|
+
js_uris=task.config.get("js_uris", []),
|
|
43
44
|
).entry()
|
|
44
45
|
|
|
45
46
|
# Struct tabs.
|
|
@@ -51,6 +52,7 @@ def populate_tabs(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
51
52
|
tabs,
|
|
52
53
|
icon="bucket",
|
|
53
54
|
markdown=struct.markdown,
|
|
55
|
+
js_uris=struct.config.get("js_uris", []),
|
|
54
56
|
).entry()
|
|
55
57
|
|
|
56
58
|
# Subprocess tabs.
|
|
@@ -109,16 +111,27 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
109
111
|
# Remove tab-content scrolling.
|
|
110
112
|
tabs.set_scroll(False)
|
|
111
113
|
|
|
112
|
-
# Tab name filter.
|
|
113
|
-
input_box(tabs.tabs, label="tab", description="Tab name filter.")
|
|
114
|
-
|
|
115
114
|
centered_markdown(
|
|
116
115
|
tabs.tabs,
|
|
117
116
|
app.config_param("top_markdown", "configure `top_markdown`"),
|
|
118
117
|
"border-start",
|
|
119
118
|
"border-bottom",
|
|
120
119
|
"border-end",
|
|
120
|
+
"bg-gradient-tertiary-to-top",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Tab name filter.
|
|
124
|
+
_, label, box = input_box(
|
|
125
|
+
div(tag="form", autocomplete="off", parent=tabs.tabs),
|
|
126
|
+
label="tab",
|
|
127
|
+
description="Tab name filter.",
|
|
128
|
+
placement="bottom",
|
|
129
|
+
icon="funnel",
|
|
130
|
+
spellcheck="false",
|
|
131
|
+
pattern=".* $ @",
|
|
121
132
|
)
|
|
133
|
+
label.add_class("border-top-0")
|
|
134
|
+
box.add_class("border-top-0")
|
|
122
135
|
|
|
123
136
|
populate_tabs(app, tabs)
|
|
124
137
|
|
|
@@ -129,6 +142,18 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
129
142
|
icon="table",
|
|
130
143
|
id="channels-button",
|
|
131
144
|
)
|
|
145
|
+
tabs.add_button(
|
|
146
|
+
"Open channel table",
|
|
147
|
+
"",
|
|
148
|
+
icon="arrow-bar-right",
|
|
149
|
+
id="open-channels-button",
|
|
150
|
+
)
|
|
151
|
+
tabs.add_button(
|
|
152
|
+
"Dedent channel table",
|
|
153
|
+
"",
|
|
154
|
+
icon="arrow-bar-left",
|
|
155
|
+
id="dedent-channels-button",
|
|
156
|
+
)
|
|
132
157
|
|
|
133
158
|
# Plot settings modal.
|
|
134
159
|
plot_settings(tabs)
|
|
@@ -149,11 +174,12 @@ def channel_environments(app: AppInfo, tabs: TabbedContent) -> None:
|
|
|
149
174
|
app.config_param("bottom_markdown", "configure `bottom_markdown`"),
|
|
150
175
|
"border-start",
|
|
151
176
|
"border-end",
|
|
177
|
+
"bg-gradient-tertiary-to-bottom",
|
|
152
178
|
)
|
|
153
179
|
|
|
154
180
|
# Add splash screen element.
|
|
155
181
|
div(
|
|
156
182
|
id=f"{PKG_NAME}-splash",
|
|
157
183
|
parent=tabs.container,
|
|
158
|
-
class_str="bg-
|
|
184
|
+
class_str="bg-secondary-subtle bg-gradient",
|
|
159
185
|
)
|
|
@@ -15,17 +15,14 @@ from runtimepy.net.server.app.placeholder import under_construction
|
|
|
15
15
|
def plot_settings(tabs: TabbedContent) -> None:
|
|
16
16
|
"""Create the plot settings modal."""
|
|
17
17
|
|
|
18
|
-
modal = Modal(tabs, name="
|
|
18
|
+
modal = Modal(tabs, name="settings", icon="sliders")
|
|
19
19
|
under_construction(modal.footer)
|
|
20
20
|
|
|
21
|
-
div(tag="h1", text="
|
|
22
|
-
div(tag="hr", parent=modal.body)
|
|
23
|
-
|
|
24
|
-
div(tag="h2", text="plot status", parent=modal.body)
|
|
21
|
+
div(tag="h1", text="plot status", parent=modal.body)
|
|
25
22
|
div(id="plot-status-inner", parent=modal.body)
|
|
26
|
-
div(tag="hr", parent=modal.body)
|
|
27
23
|
|
|
28
|
-
div(tag="
|
|
24
|
+
div(tag="hr", parent=modal.body)
|
|
25
|
+
div(tag="h1", text="minimum transmit period (ms)", parent=modal.body)
|
|
29
26
|
|
|
30
27
|
div(
|
|
31
28
|
tag="p",
|
|
@@ -28,12 +28,13 @@ class ChannelEnvironmentTabBase(Tab, LoggerMixin, MarkdownMixin):
|
|
|
28
28
|
tabs: TabbedContent,
|
|
29
29
|
icon: str = "alarm",
|
|
30
30
|
markdown: str = None,
|
|
31
|
+
**kwargs,
|
|
31
32
|
) -> None:
|
|
32
33
|
"""Initialize this instance."""
|
|
33
34
|
|
|
34
35
|
self.command = command
|
|
35
36
|
self.set_markdown(markdown=markdown, package=PKG_NAME)
|
|
36
|
-
super().__init__(name, app, tabs, source="env", icon=icon)
|
|
37
|
+
super().__init__(name, app, tabs, source="env", icon=icon, **kwargs)
|
|
37
38
|
|
|
38
39
|
# Logging.
|
|
39
40
|
LoggerMixin.__init__(self, logger=self.command.logger)
|
|
@@ -20,6 +20,7 @@ from runtimepy.net.server.app.env.widgets import (
|
|
|
20
20
|
enum_dropdown,
|
|
21
21
|
value_input_box,
|
|
22
22
|
)
|
|
23
|
+
from runtimepy.ui.controls import Controls, Default
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def get_channel_kind_str(
|
|
@@ -39,7 +40,7 @@ def get_channel_kind_str(
|
|
|
39
40
|
def default_button(
|
|
40
41
|
parent: Element,
|
|
41
42
|
name: str,
|
|
42
|
-
|
|
43
|
+
default: Default,
|
|
43
44
|
*classes: str,
|
|
44
45
|
front: bool = True,
|
|
45
46
|
) -> Element:
|
|
@@ -49,14 +50,31 @@ def default_button(
|
|
|
49
50
|
parent,
|
|
50
51
|
id=name,
|
|
51
52
|
icon="arrow-counterclockwise",
|
|
52
|
-
title=f"Reset '{name}' to default value '{
|
|
53
|
-
value=
|
|
53
|
+
title=f"Reset '{name}' to default value '{default}'.",
|
|
54
|
+
value=default,
|
|
54
55
|
front=front,
|
|
55
56
|
)
|
|
56
57
|
button.add_class("default-button", *classes)
|
|
57
58
|
return button
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def handle_controls(parent: Element, name: str, controls: Controls) -> None:
|
|
62
|
+
"""Add control elements."""
|
|
63
|
+
|
|
64
|
+
# Determine if a slider should be created.
|
|
65
|
+
if "slider" in controls:
|
|
66
|
+
elem = controls["slider"]
|
|
67
|
+
|
|
68
|
+
slider(
|
|
69
|
+
elem["min"], # type: ignore
|
|
70
|
+
elem["max"], # type: ignore
|
|
71
|
+
int(elem["step"]), # type: ignore
|
|
72
|
+
parent=parent,
|
|
73
|
+
id=name,
|
|
74
|
+
title=f"Value control for '{name}'.",
|
|
75
|
+
).add_class("bg-body", "rounded-pill", "me-2")
|
|
76
|
+
|
|
77
|
+
|
|
60
78
|
class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
61
79
|
"""A channel-environment tab interface."""
|
|
62
80
|
|
|
@@ -74,12 +92,18 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
74
92
|
# Add boolean/bit toggle button.
|
|
75
93
|
control = div(tag="td", parent=parent, class_str="p-0")
|
|
76
94
|
|
|
95
|
+
if chan.commandable:
|
|
96
|
+
control.add_class("border-start-info-subtle")
|
|
97
|
+
parent.add_class("channel-commandable")
|
|
98
|
+
else:
|
|
99
|
+
parent.add_class("channel-regular")
|
|
100
|
+
|
|
77
101
|
chan_type = div(
|
|
78
102
|
tag="td",
|
|
79
103
|
text=get_channel_kind_str(env, chan, enum),
|
|
80
104
|
parent=parent,
|
|
81
105
|
title=f"Underlying primitive type for '{name}'.",
|
|
82
|
-
class_str="p-0 ps-
|
|
106
|
+
class_str="p-0 ps-2 pe-1",
|
|
83
107
|
)
|
|
84
108
|
|
|
85
109
|
control_added = False
|
|
@@ -88,50 +112,82 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
88
112
|
chan_type.add_class("fw-bold")
|
|
89
113
|
|
|
90
114
|
if chan.commandable and not chan.type.is_boolean:
|
|
91
|
-
enum_dropdown(
|
|
115
|
+
enum_dropdown(
|
|
116
|
+
control, name, enum, cast(int, chan.raw.value)
|
|
117
|
+
).add_class(
|
|
118
|
+
"border-0",
|
|
119
|
+
"text-secondary-emphasis",
|
|
120
|
+
"pt-0",
|
|
121
|
+
"pb-0",
|
|
122
|
+
"d-inline",
|
|
123
|
+
)
|
|
124
|
+
control.add_class("border-end-info-subtle")
|
|
92
125
|
control_added = True
|
|
93
126
|
|
|
127
|
+
if chan.default is not None:
|
|
128
|
+
default_button(
|
|
129
|
+
control,
|
|
130
|
+
name,
|
|
131
|
+
chan.default,
|
|
132
|
+
"p-0",
|
|
133
|
+
"d-inline",
|
|
134
|
+
*TABLE_BUTTON_CLASSES,
|
|
135
|
+
front=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
94
138
|
if chan.type.is_boolean:
|
|
95
|
-
chan_type.add_class("text-primary
|
|
139
|
+
chan_type.add_class("text-primary")
|
|
96
140
|
if chan.commandable:
|
|
97
141
|
button = toggle_button(
|
|
98
142
|
control, id=name, title=f"Toggle '{name}'."
|
|
99
143
|
)
|
|
100
|
-
button.add_class(
|
|
144
|
+
button.add_class(
|
|
145
|
+
"toggle-value",
|
|
146
|
+
"pt-0",
|
|
147
|
+
"pb-0",
|
|
148
|
+
"fs-5",
|
|
149
|
+
"border-end-info-subtle",
|
|
150
|
+
*TABLE_BUTTON_CLASSES,
|
|
151
|
+
)
|
|
101
152
|
control_added = True
|
|
102
153
|
|
|
103
154
|
if chan.default is not None:
|
|
104
155
|
default_button(
|
|
105
|
-
control,
|
|
156
|
+
control,
|
|
157
|
+
name,
|
|
158
|
+
chan.default,
|
|
159
|
+
"p-0",
|
|
160
|
+
*TABLE_BUTTON_CLASSES,
|
|
161
|
+
front=False,
|
|
106
162
|
)
|
|
107
163
|
|
|
108
164
|
elif chan.type.is_float:
|
|
109
165
|
chan_type.add_class("text-secondary-emphasis")
|
|
110
166
|
else:
|
|
111
|
-
chan_type.add_class("text-primary")
|
|
167
|
+
chan_type.add_class("text-primary-emphasis")
|
|
112
168
|
|
|
113
169
|
# Input box with send button.
|
|
114
170
|
if not control_added and chan.commandable:
|
|
115
|
-
|
|
171
|
+
control.add_class("border-end-info-subtle")
|
|
172
|
+
|
|
173
|
+
container = value_input_box(name, control).add_class(
|
|
174
|
+
"justify-content-start"
|
|
175
|
+
)
|
|
116
176
|
|
|
117
177
|
# Reset-to-default button if a default value exists.
|
|
118
178
|
if chan.default is not None:
|
|
119
|
-
default_button(
|
|
179
|
+
default_button(
|
|
180
|
+
container,
|
|
181
|
+
name,
|
|
182
|
+
chan.default,
|
|
183
|
+
"pt-0",
|
|
184
|
+
"pb-0",
|
|
185
|
+
*TABLE_BUTTON_CLASSES,
|
|
186
|
+
front=False,
|
|
187
|
+
)
|
|
120
188
|
|
|
121
189
|
if chan.controls:
|
|
122
|
-
|
|
123
|
-
if "slider" in chan.controls:
|
|
124
|
-
elem = chan.controls["slider"]
|
|
125
|
-
|
|
126
|
-
slider(
|
|
127
|
-
elem["min"], # type: ignore
|
|
128
|
-
elem["max"], # type: ignore
|
|
129
|
-
int(elem["step"]), # type: ignore
|
|
130
|
-
parent=container,
|
|
131
|
-
id=name,
|
|
132
|
-
title=f"Value control for '{name}'.",
|
|
133
|
-
front=True,
|
|
134
|
-
)
|
|
190
|
+
handle_controls(container, name, chan.controls)
|
|
135
191
|
|
|
136
192
|
def _bit_field_controls(
|
|
137
193
|
self,
|
|
@@ -146,12 +202,70 @@ class ChannelEnvironmentTabControls(ChannelEnvironmentTabBase):
|
|
|
146
202
|
|
|
147
203
|
field = self.command.env.fields[name]
|
|
148
204
|
if field.commandable:
|
|
205
|
+
control.add_class("border-start-info-subtle")
|
|
206
|
+
parent.add_class("channel-commandable")
|
|
207
|
+
|
|
208
|
+
if not is_bit:
|
|
209
|
+
control.add_class("border-end-info-subtle")
|
|
210
|
+
|
|
149
211
|
if is_bit:
|
|
150
212
|
button = toggle_button(
|
|
151
213
|
control, id=name, title=f"Toggle '{name}'."
|
|
152
214
|
)
|
|
153
|
-
button.add_class(
|
|
215
|
+
button.add_class(
|
|
216
|
+
"toggle-value",
|
|
217
|
+
"pt-0",
|
|
218
|
+
"pb-0",
|
|
219
|
+
"fs-5",
|
|
220
|
+
"border-start-0",
|
|
221
|
+
"border-end-info-subtle",
|
|
222
|
+
*TABLE_BUTTON_CLASSES,
|
|
223
|
+
)
|
|
224
|
+
if field.default is not None:
|
|
225
|
+
default_button(
|
|
226
|
+
control,
|
|
227
|
+
name,
|
|
228
|
+
field.default, # type: ignore
|
|
229
|
+
"p-0",
|
|
230
|
+
*TABLE_BUTTON_CLASSES,
|
|
231
|
+
front=False,
|
|
232
|
+
)
|
|
233
|
+
|
|
154
234
|
elif enum:
|
|
155
|
-
enum_dropdown(control, name, enum, field())
|
|
235
|
+
enum_dropdown(control, name, enum, field()).add_class(
|
|
236
|
+
"border-0",
|
|
237
|
+
"text-secondary-emphasis",
|
|
238
|
+
"pt-0",
|
|
239
|
+
"pb-0",
|
|
240
|
+
"d-inline",
|
|
241
|
+
)
|
|
242
|
+
if field.default is not None:
|
|
243
|
+
default_button(
|
|
244
|
+
control,
|
|
245
|
+
name,
|
|
246
|
+
field.default, # type: ignore
|
|
247
|
+
"p-0",
|
|
248
|
+
"d-inline",
|
|
249
|
+
*TABLE_BUTTON_CLASSES,
|
|
250
|
+
front=False,
|
|
251
|
+
)
|
|
156
252
|
else:
|
|
157
|
-
value_input_box(name, control)
|
|
253
|
+
container = value_input_box(name, control).add_class(
|
|
254
|
+
"justify-content-start"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if field.default is not None:
|
|
258
|
+
default_button(
|
|
259
|
+
container,
|
|
260
|
+
name,
|
|
261
|
+
field.default, # type: ignore
|
|
262
|
+
"pt-0",
|
|
263
|
+
"pb-0",
|
|
264
|
+
*TABLE_BUTTON_CLASSES,
|
|
265
|
+
front=False,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if field.controls:
|
|
269
|
+
handle_controls(container, name, field.controls)
|
|
270
|
+
else:
|
|
271
|
+
parent.add_class("channel-regular")
|