ovos-gui-api-client 0.0.2a1__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.
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OVOS GUI API Client
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
This module provides the public interface for OVOS skills and plugins to
|
|
6
|
+
interact with the GUI layer.
|
|
7
|
+
|
|
8
|
+
Design contract
|
|
9
|
+
---------------
|
|
10
|
+
* Skills may ONLY display pre-defined page templates (see `PageTemplates`).
|
|
11
|
+
* All session data set via ``gui[key] = value`` is synced to the GUI service
|
|
12
|
+
in real time and made available to the active display layer.
|
|
13
|
+
* A single `GUIInterface` instance is bound to one skill / namespace.
|
|
14
|
+
* The message-bus must be set (via constructor or `set_bus`) before any
|
|
15
|
+
display call is made.
|
|
16
|
+
|
|
17
|
+
Typical usage inside a skill::
|
|
18
|
+
|
|
19
|
+
gui = GUIInterface("my.skill.id", bus=my_bus)
|
|
20
|
+
gui["temperature"] = 22
|
|
21
|
+
gui.show_text("Hello world", title="Greeting")
|
|
22
|
+
|
|
23
|
+
# later …
|
|
24
|
+
gui.release()
|
|
25
|
+
"""
|
|
26
|
+
import enum
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import asdict, dataclass, field
|
|
29
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
30
|
+
|
|
31
|
+
from ovos_config import Configuration
|
|
32
|
+
from ovos_utils.log import LOG
|
|
33
|
+
|
|
34
|
+
from ovos_bus_client.message import Message
|
|
35
|
+
from ovos_bus_client.util import get_mycroft_bus
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Page template registry
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class PageTemplates(str, enum.Enum):
|
|
43
|
+
"""Enumeration of all pre-defined GUI page templates.
|
|
44
|
+
|
|
45
|
+
Skills and plugins may only display pages from this set. Custom
|
|
46
|
+
display-layer pages are not supported through this interface by design.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
IDLE: Default resting state – reserved for the ovos-gui
|
|
50
|
+
service itself; skills must not use this directly.
|
|
51
|
+
LOADING: Generic loading / spinner animation.
|
|
52
|
+
STATUS: Success or failure result animation.
|
|
53
|
+
ERROR: Error message with optional detail text.
|
|
54
|
+
TEXT: Scrollable plain-text view.
|
|
55
|
+
IMAGE: Static image viewer.
|
|
56
|
+
ANIMATED_IMAGE: Animated GIF/WebP viewer.
|
|
57
|
+
LIST: Scrollable list of labelled items.
|
|
58
|
+
GRID: 2-D tile grid of image-primary items.
|
|
59
|
+
TABLE: Columnar data table with named headers.
|
|
60
|
+
HTML: In-process HTML renderer.
|
|
61
|
+
URL: Full web-page renderer.
|
|
62
|
+
AUDIO_PLAYER: Now-playing card for audio playback.
|
|
63
|
+
VIDEO_PLAYER: Embedded video playback surface.
|
|
64
|
+
CLOCK: Clock / time display (self-updating).
|
|
65
|
+
TIMER: Countdown / count-up display (self-updating).
|
|
66
|
+
WEATHER: Weather summary card.
|
|
67
|
+
MAP: Geographic location view.
|
|
68
|
+
CONFIRM: Visual accompaniment to a yes/no voice dialogue.
|
|
69
|
+
SELECT: Visual accompaniment to a choice voice dialogue.
|
|
70
|
+
FACE: Avatar face (awake / sleeping states).
|
|
71
|
+
"""
|
|
72
|
+
IDLE = "SYSTEM_idle"
|
|
73
|
+
LOADING = "SYSTEM_loading"
|
|
74
|
+
STATUS = "SYSTEM_status"
|
|
75
|
+
ERROR = "SYSTEM_error"
|
|
76
|
+
TEXT = "SYSTEM_text"
|
|
77
|
+
IMAGE = "SYSTEM_image"
|
|
78
|
+
ANIMATED_IMAGE = "SYSTEM_animated_image"
|
|
79
|
+
LIST = "SYSTEM_list"
|
|
80
|
+
GRID = "SYSTEM_grid"
|
|
81
|
+
TABLE = "SYSTEM_table"
|
|
82
|
+
HTML = "SYSTEM_html"
|
|
83
|
+
URL = "SYSTEM_url"
|
|
84
|
+
AUDIO_PLAYER = "SYSTEM_audio_player"
|
|
85
|
+
VIDEO_PLAYER = "SYSTEM_video_player"
|
|
86
|
+
CLOCK = "SYSTEM_clock"
|
|
87
|
+
TIMER = "SYSTEM_timer"
|
|
88
|
+
WEATHER = "SYSTEM_weather"
|
|
89
|
+
MAP = "SYSTEM_map"
|
|
90
|
+
CONFIRM = "SYSTEM_confirm"
|
|
91
|
+
SELECT = "SYSTEM_select"
|
|
92
|
+
FACE = "SYSTEM_face"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class FillMode(str, enum.Enum):
|
|
96
|
+
"""How an image should be scaled to fit its display area.
|
|
97
|
+
|
|
98
|
+
The display layer is responsible for mapping these values to its own
|
|
99
|
+
rendering primitives (e.g. Qt's ``Image.fillMode`` property).
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
FIT: Scale the image to fit entirely within the area, preserving
|
|
103
|
+
the aspect ratio. Letterboxing may appear.
|
|
104
|
+
CROP: Scale the image to fill the area entirely, cropping any
|
|
105
|
+
overflow, preserving the aspect ratio.
|
|
106
|
+
STRETCH: Stretch the image to fill the area exactly, ignoring the
|
|
107
|
+
aspect ratio.
|
|
108
|
+
"""
|
|
109
|
+
FIT = "fit"
|
|
110
|
+
CROP = "crop"
|
|
111
|
+
STRETCH = "stretch"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Data structures for template payloads
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class ListItem:
|
|
120
|
+
"""A single entry in a :attr:`PageTemplates.LIST` page.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
title: Primary label (required).
|
|
124
|
+
subtitle: Secondary label shown below the title.
|
|
125
|
+
image: URL or local path to a thumbnail image.
|
|
126
|
+
"""
|
|
127
|
+
title: str
|
|
128
|
+
subtitle: Optional[str] = None
|
|
129
|
+
image: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
132
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class GridItem:
|
|
137
|
+
"""A single tile in a :attr:`PageTemplates.GRID` page.
|
|
138
|
+
|
|
139
|
+
The grid is image-primary: all tiles are equal in visual weight and the
|
|
140
|
+
display layer determines column count based on screen dimensions.
|
|
141
|
+
|
|
142
|
+
Attributes:
|
|
143
|
+
image: URL or local path to the tile image (required).
|
|
144
|
+
title: Optional label shown below the image.
|
|
145
|
+
"""
|
|
146
|
+
image: str
|
|
147
|
+
title: Optional[str] = None
|
|
148
|
+
|
|
149
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
150
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class SelectItem:
|
|
155
|
+
"""A single option in a :attr:`PageTemplates.SELECT` dialogue page.
|
|
156
|
+
|
|
157
|
+
Attributes:
|
|
158
|
+
label: Human-readable text shown to the user.
|
|
159
|
+
value: Machine-readable value sent back with the touch event.
|
|
160
|
+
"""
|
|
161
|
+
label: str
|
|
162
|
+
value: Any
|
|
163
|
+
|
|
164
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
165
|
+
return asdict(self)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Internal helpers
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
class _GUIDict(dict):
|
|
173
|
+
"""A ``dict`` subclass that propagates every mutation to the GUI service.
|
|
174
|
+
|
|
175
|
+
This is used automatically when a skill assigns a ``dict`` value to a
|
|
176
|
+
session-data key so that nested key changes are also synced::
|
|
177
|
+
|
|
178
|
+
gui["info"] = {"city": "Berlin", "temp": 12}
|
|
179
|
+
gui["info"]["temp"] = 13 # <- triggers sync without extra code
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, gui: "GUIInterface", **kwargs: Any) -> None:
|
|
183
|
+
self._gui = gui
|
|
184
|
+
super().__init__(**kwargs)
|
|
185
|
+
|
|
186
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
187
|
+
if self.get(key) != value:
|
|
188
|
+
super().__setitem__(key, value)
|
|
189
|
+
self._gui._sync_data()
|
|
190
|
+
|
|
191
|
+
def update(self, other: Optional[Dict] = None, **kwargs: Any) -> None: # type: ignore[override]
|
|
192
|
+
"""Bulk-update and sync once after all changes are applied."""
|
|
193
|
+
changed = False
|
|
194
|
+
items = dict(other or {}, **kwargs)
|
|
195
|
+
for key, value in items.items():
|
|
196
|
+
if self.get(key) != value:
|
|
197
|
+
super().__setitem__(key, value)
|
|
198
|
+
changed = True
|
|
199
|
+
if changed:
|
|
200
|
+
self._gui._sync_data()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Public interface
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
class GUIInterface:
|
|
208
|
+
"""Interface between an OVOS skill / plugin and the GUI service.
|
|
209
|
+
|
|
210
|
+
Session data set on this object is forwarded to the GUI service and made
|
|
211
|
+
available to the active display layer under each key's name.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
skill_id: Unique identifier for the skill owning this interface.
|
|
215
|
+
This is also used as the *namespace* in GUI protocol
|
|
216
|
+
messages.
|
|
217
|
+
bus: Optional :class:`MessageBusClient`. When omitted the
|
|
218
|
+
bus must be supplied later via :meth:`set_bus`.
|
|
219
|
+
config: Optional GUI configuration dict. Defaults to the
|
|
220
|
+
``[gui]`` section of the global OVOS configuration.
|
|
221
|
+
|
|
222
|
+
Example::
|
|
223
|
+
|
|
224
|
+
gui = GUIInterface("my.skill.id", bus=bus)
|
|
225
|
+
gui["answer"] = 42
|
|
226
|
+
gui.show_text("The answer is 42")
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
skill_id: str,
|
|
232
|
+
bus=None,
|
|
233
|
+
config: Optional[Dict] = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
self._skill_id: str = skill_id
|
|
236
|
+
self._bus = None
|
|
237
|
+
self.config: Dict = config or Configuration().get("gui", {})
|
|
238
|
+
|
|
239
|
+
self._session_data: Dict[str, Any] = {}
|
|
240
|
+
self._pages: List[PageTemplates] = []
|
|
241
|
+
self.current_page_idx: int = -1
|
|
242
|
+
self._events: List[tuple] = []
|
|
243
|
+
self._on_gui_changed_callback: Optional[Callable] = None
|
|
244
|
+
|
|
245
|
+
if bus:
|
|
246
|
+
self.set_bus(bus)
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# Bus
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def set_bus(self, bus=None) -> None:
|
|
253
|
+
"""Attach a message-bus client and register default event handlers.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
bus: :class:`MessageBusClient` instance. Falls back to the
|
|
257
|
+
shared/global bus when ``None``.
|
|
258
|
+
"""
|
|
259
|
+
self._bus = bus or get_mycroft_bus()
|
|
260
|
+
self._setup_default_handlers()
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def bus(self):
|
|
264
|
+
"""The attached :class:`MessageBusClient`, or ``None``."""
|
|
265
|
+
return self._bus
|
|
266
|
+
|
|
267
|
+
@bus.setter
|
|
268
|
+
def bus(self, val) -> None:
|
|
269
|
+
self.set_bus(val)
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
# Identity
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def skill_id(self) -> str:
|
|
277
|
+
"""Unique identifier / namespace for this interface."""
|
|
278
|
+
return self._skill_id
|
|
279
|
+
|
|
280
|
+
@skill_id.setter
|
|
281
|
+
def skill_id(self, val: str) -> None:
|
|
282
|
+
self._skill_id = val
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# GUI availability
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def gui_disabled(self) -> bool:
|
|
290
|
+
"""``True`` when the GUI service is explicitly disabled in config.
|
|
291
|
+
|
|
292
|
+
When disabled all display calls are silently no-ops so that the
|
|
293
|
+
same skill code works on headless devices.
|
|
294
|
+
"""
|
|
295
|
+
return bool(self.config.get("disable_gui", False))
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# Active pages
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def page(self) -> Optional[PageTemplates]:
|
|
303
|
+
"""The currently active page, or ``None`` when the GUI is idle."""
|
|
304
|
+
if not self._pages or self.current_page_idx >= len(self._pages):
|
|
305
|
+
return None
|
|
306
|
+
return self._pages[self.current_page_idx]
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def pages(self) -> List[PageTemplates]:
|
|
310
|
+
"""All pages currently managed by this interface."""
|
|
311
|
+
return list(self._pages)
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
# Dict-like session data access
|
|
315
|
+
# ------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
318
|
+
"""Set a session-data value and sync it to the GUI if a page is active."""
|
|
319
|
+
if self._session_data.get(key) == value:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
if isinstance(value, dict) and not isinstance(value, _GUIDict):
|
|
323
|
+
value = _GUIDict(self, **value)
|
|
324
|
+
|
|
325
|
+
self._session_data[key] = value
|
|
326
|
+
|
|
327
|
+
if self.page:
|
|
328
|
+
self._sync_data()
|
|
329
|
+
|
|
330
|
+
def __getitem__(self, key: str) -> Any:
|
|
331
|
+
return self._session_data[key]
|
|
332
|
+
|
|
333
|
+
def __contains__(self, key: str) -> bool:
|
|
334
|
+
return key in self._session_data
|
|
335
|
+
|
|
336
|
+
def __len__(self) -> int:
|
|
337
|
+
return len(self._session_data)
|
|
338
|
+
|
|
339
|
+
def __repr__(self) -> str:
|
|
340
|
+
return f"GUIInterface(skill_id={self._skill_id!r}, data={self._session_data!r})"
|
|
341
|
+
|
|
342
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
343
|
+
"""Return ``self[key]`` or *default* when the key is absent."""
|
|
344
|
+
return self._session_data.get(key, default)
|
|
345
|
+
|
|
346
|
+
def keys(self):
|
|
347
|
+
return self._session_data.keys()
|
|
348
|
+
|
|
349
|
+
def values(self):
|
|
350
|
+
return self._session_data.values()
|
|
351
|
+
|
|
352
|
+
def items(self):
|
|
353
|
+
return self._session_data.items()
|
|
354
|
+
|
|
355
|
+
def update(self, data: Dict[str, Any]) -> None:
|
|
356
|
+
"""Bulk-set session data and emit a single sync message.
|
|
357
|
+
|
|
358
|
+
Prefer this over repeated ``gui[key] = value`` assignments when
|
|
359
|
+
updating several keys at once.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
data: Key/value pairs to merge into the session data.
|
|
363
|
+
"""
|
|
364
|
+
changed = False
|
|
365
|
+
for key, value in data.items():
|
|
366
|
+
if self._session_data.get(key) != value:
|
|
367
|
+
if isinstance(value, dict) and not isinstance(value, _GUIDict):
|
|
368
|
+
value = _GUIDict(self, **value)
|
|
369
|
+
self._session_data[key] = value
|
|
370
|
+
changed = True
|
|
371
|
+
|
|
372
|
+
if changed and self.page:
|
|
373
|
+
self._sync_data()
|
|
374
|
+
|
|
375
|
+
# ------------------------------------------------------------------
|
|
376
|
+
# Event handlers
|
|
377
|
+
# ------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
def _setup_default_handlers(self) -> None:
|
|
380
|
+
"""Register built-in message-bus handlers for this namespace."""
|
|
381
|
+
event = f"{self.skill_id}.set"
|
|
382
|
+
self._bus.on(event, self._on_gui_set)
|
|
383
|
+
self._events.append((event, self._on_gui_set))
|
|
384
|
+
|
|
385
|
+
def register_handler(self, event: str, handler: Callable) -> None:
|
|
386
|
+
"""Register a handler for a GUI-originated event.
|
|
387
|
+
|
|
388
|
+
The ``event`` string will be automatically prefixed with
|
|
389
|
+
``<skill_id>.`` when not already present, matching the convention
|
|
390
|
+
used by the display layer to fire events back to the skill.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
event: Event name (with or without namespace prefix).
|
|
394
|
+
handler: Callable that accepts a single :class:`Message` arg.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
RuntimeError: When no bus has been set.
|
|
398
|
+
"""
|
|
399
|
+
if not self._bus:
|
|
400
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
401
|
+
if not event.startswith(f"{self.skill_id}."):
|
|
402
|
+
event = f"{self.skill_id}.{event}"
|
|
403
|
+
self._events.append((event, handler))
|
|
404
|
+
self._bus.on(event, handler)
|
|
405
|
+
|
|
406
|
+
def set_on_gui_changed(self, callback: Callable) -> None:
|
|
407
|
+
"""Register a callback to invoke whenever the display layer changes a session value.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
callback: Zero-argument callable.
|
|
411
|
+
"""
|
|
412
|
+
self._on_gui_changed_callback = callback
|
|
413
|
+
|
|
414
|
+
def _on_gui_set(self, message: Message) -> None:
|
|
415
|
+
"""Handle a ``<skill_id>.set`` message from the GUI service.
|
|
416
|
+
|
|
417
|
+
The display layer can push value changes back to the skill via this
|
|
418
|
+
channel (e.g. when the user interacts with an input element).
|
|
419
|
+
"""
|
|
420
|
+
for key, value in message.data.items():
|
|
421
|
+
# Use dict.__setitem__ directly to avoid double-sync; we sync once
|
|
422
|
+
# after all keys are applied.
|
|
423
|
+
if isinstance(value, dict) and not isinstance(value, _GUIDict):
|
|
424
|
+
value = _GUIDict(self, **value)
|
|
425
|
+
self._session_data[key] = value
|
|
426
|
+
|
|
427
|
+
self._sync_data()
|
|
428
|
+
|
|
429
|
+
if self._on_gui_changed_callback:
|
|
430
|
+
self._on_gui_changed_callback()
|
|
431
|
+
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
# Data sync
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def _sync_data(self) -> None:
|
|
437
|
+
"""Emit a ``gui.value.set`` message with the current session data.
|
|
438
|
+
|
|
439
|
+
No-op when the GUI is disabled or no bus is attached.
|
|
440
|
+
"""
|
|
441
|
+
if self.gui_disabled:
|
|
442
|
+
return
|
|
443
|
+
if not self._bus:
|
|
444
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
445
|
+
data = dict(self._session_data, __from=self.skill_id)
|
|
446
|
+
self._bus.emit(Message("gui.value.set", data))
|
|
447
|
+
|
|
448
|
+
# ------------------------------------------------------------------
|
|
449
|
+
# Page management (internal)
|
|
450
|
+
# ------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def _show_page(
|
|
453
|
+
self,
|
|
454
|
+
page: PageTemplates,
|
|
455
|
+
override_idle: Union[bool, int, None] = None,
|
|
456
|
+
override_animations: bool = False,
|
|
457
|
+
index: int = 0,
|
|
458
|
+
remove_others: bool = False,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Show a single page. Delegates to :meth:`_show_pages`."""
|
|
461
|
+
self._show_pages([page], index, override_idle, override_animations, remove_others)
|
|
462
|
+
|
|
463
|
+
def _show_pages(
|
|
464
|
+
self,
|
|
465
|
+
page_names: List[PageTemplates],
|
|
466
|
+
index: int = 0,
|
|
467
|
+
override_idle: Union[bool, int, None] = None,
|
|
468
|
+
override_animations: bool = False,
|
|
469
|
+
remove_others: bool = False,
|
|
470
|
+
) -> None:
|
|
471
|
+
"""Request the GUI service to display one or more pages.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
page_names: Ordered list of pages to show.
|
|
475
|
+
index: Which page in the list to make active
|
|
476
|
+
(0-based).
|
|
477
|
+
override_idle: ``True`` to take over the idle display
|
|
478
|
+
indefinitely; an ``int`` to do so for that
|
|
479
|
+
many seconds; ``None`` to use the default
|
|
480
|
+
timeout.
|
|
481
|
+
override_animations: ``True`` to suppress platform animations.
|
|
482
|
+
remove_others: When ``True``, all pages *not* in
|
|
483
|
+
*page_names* are removed first.
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
RuntimeError: When no bus has been set.
|
|
487
|
+
ValueError: When *page_names* is not a list.
|
|
488
|
+
"""
|
|
489
|
+
if not self._bus:
|
|
490
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
491
|
+
|
|
492
|
+
if isinstance(page_names, (str, PageTemplates)):
|
|
493
|
+
page_names = [page_names]
|
|
494
|
+
if not isinstance(page_names, list):
|
|
495
|
+
raise ValueError(f"page_names must be a list, got {type(page_names).__name__}")
|
|
496
|
+
|
|
497
|
+
if index >= len(page_names):
|
|
498
|
+
LOG.warning(
|
|
499
|
+
f"index={index} is out of range for page list of length "
|
|
500
|
+
f"{len(page_names)}; clamping to last page."
|
|
501
|
+
)
|
|
502
|
+
index = len(page_names) - 1
|
|
503
|
+
|
|
504
|
+
if remove_others:
|
|
505
|
+
self._remove_all_pages(except_pages=page_names)
|
|
506
|
+
|
|
507
|
+
self._pages = page_names
|
|
508
|
+
self.current_page_idx = index
|
|
509
|
+
|
|
510
|
+
if self.gui_disabled:
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
# Sync data first so the page renders with the latest values.
|
|
514
|
+
self._sync_data()
|
|
515
|
+
|
|
516
|
+
self._bus.emit(
|
|
517
|
+
Message(
|
|
518
|
+
"gui.page.show",
|
|
519
|
+
{
|
|
520
|
+
"page_names": page_names,
|
|
521
|
+
"index": index,
|
|
522
|
+
"__from": self.skill_id,
|
|
523
|
+
"__idle": override_idle,
|
|
524
|
+
"__animations": override_animations,
|
|
525
|
+
},
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def _remove_page(self, page: PageTemplates) -> None:
|
|
530
|
+
"""Remove a single page from the GUI display stack."""
|
|
531
|
+
self._remove_pages([page])
|
|
532
|
+
|
|
533
|
+
def _remove_pages(self, page_names: List[PageTemplates]) -> None:
|
|
534
|
+
"""Remove specific pages from the GUI display stack.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
page_names: Pages to remove.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
RuntimeError: When no bus has been set.
|
|
541
|
+
"""
|
|
542
|
+
if self.gui_disabled:
|
|
543
|
+
return
|
|
544
|
+
if not self._bus:
|
|
545
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
546
|
+
if isinstance(page_names, (str, PageTemplates)):
|
|
547
|
+
page_names = [page_names]
|
|
548
|
+
if not isinstance(page_names, list):
|
|
549
|
+
raise ValueError(f"page_names must be a list, got {type(page_names).__name__}")
|
|
550
|
+
|
|
551
|
+
self._bus.emit(
|
|
552
|
+
Message("gui.page.delete", {"page_names": page_names, "__from": self.skill_id})
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def _remove_all_pages(self, except_pages: Optional[List[PageTemplates]] = None) -> None:
|
|
556
|
+
"""Remove all pages managed by this interface.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
except_pages: Optional list of pages to keep.
|
|
560
|
+
"""
|
|
561
|
+
if self.gui_disabled:
|
|
562
|
+
return
|
|
563
|
+
if not self._bus:
|
|
564
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
565
|
+
self._bus.emit(
|
|
566
|
+
Message(
|
|
567
|
+
"gui.page.delete.all",
|
|
568
|
+
{"__from": self.skill_id, "except": except_pages or []},
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# ------------------------------------------------------------------
|
|
573
|
+
# Lifecycle
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
def clear(self) -> None:
|
|
577
|
+
"""Clear all session data and remove all pages from the GUI.
|
|
578
|
+
|
|
579
|
+
This resets the interface to its initial state without releasing
|
|
580
|
+
the namespace. Use :meth:`release` to fully hand back control.
|
|
581
|
+
"""
|
|
582
|
+
self._session_data = {}
|
|
583
|
+
self._pages = []
|
|
584
|
+
self.current_page_idx = -1
|
|
585
|
+
if self.gui_disabled:
|
|
586
|
+
return
|
|
587
|
+
if not self._bus:
|
|
588
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
589
|
+
self._bus.emit(Message("gui.clear.namespace", {"__from": self.skill_id}))
|
|
590
|
+
|
|
591
|
+
def release(self) -> None:
|
|
592
|
+
"""Signal that this skill is done with the GUI.
|
|
593
|
+
|
|
594
|
+
Clears all session data and pages, then notifies the GUI service so
|
|
595
|
+
that it can return to the idle/resting screen or the previous view.
|
|
596
|
+
"""
|
|
597
|
+
if not self._bus:
|
|
598
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
599
|
+
self.clear()
|
|
600
|
+
self._bus.emit(Message("ovos.gui.screen.close", {"skill_id": self.skill_id}))
|
|
601
|
+
|
|
602
|
+
def shutdown(self) -> None:
|
|
603
|
+
"""Shut down this interface and deregister all event handlers.
|
|
604
|
+
|
|
605
|
+
Called automatically when the owning skill is unloaded.
|
|
606
|
+
"""
|
|
607
|
+
if self._bus:
|
|
608
|
+
self.release()
|
|
609
|
+
for event, handler in self._events:
|
|
610
|
+
self._bus.remove(event, handler)
|
|
611
|
+
self._events.clear()
|
|
612
|
+
|
|
613
|
+
# ------------------------------------------------------------------
|
|
614
|
+
# High-level display helpers
|
|
615
|
+
# ------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
def send_event(
|
|
618
|
+
self,
|
|
619
|
+
event_name: str,
|
|
620
|
+
params: Union[Dict, list, str, int, float, bool, None] = None,
|
|
621
|
+
) -> None:
|
|
622
|
+
"""Trigger a named event in the active display-layer page.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
event_name: Name of the event to fire.
|
|
626
|
+
params: JSON-serialisable payload delivered alongside the
|
|
627
|
+
event. Defaults to an empty dict.
|
|
628
|
+
|
|
629
|
+
Raises:
|
|
630
|
+
RuntimeError: When no bus has been set.
|
|
631
|
+
"""
|
|
632
|
+
if self.gui_disabled:
|
|
633
|
+
return
|
|
634
|
+
if not self._bus:
|
|
635
|
+
raise RuntimeError("Bus not set – call set_bus() or pass bus= to the constructor.")
|
|
636
|
+
self._bus.emit(
|
|
637
|
+
Message(
|
|
638
|
+
"gui.event.send",
|
|
639
|
+
{
|
|
640
|
+
"__from": self.skill_id,
|
|
641
|
+
"event_name": event_name,
|
|
642
|
+
"params": params or {},
|
|
643
|
+
},
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# ------------------------------------------------------------------
|
|
648
|
+
# Template helpers
|
|
649
|
+
# ------------------------------------------------------------------
|
|
650
|
+
|
|
651
|
+
def show_face(
|
|
652
|
+
self,
|
|
653
|
+
awake: bool = True,
|
|
654
|
+
override_idle: Union[int, bool] = True,
|
|
655
|
+
override_animations: bool = True,
|
|
656
|
+
) -> None:
|
|
657
|
+
"""Display an avatar face in awake or sleeping state.
|
|
658
|
+
|
|
659
|
+
Intended for GUI frontends that render a character/avatar rather
|
|
660
|
+
than a traditional screen layout.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
awake: ``True`` for open eyes, ``False`` for
|
|
664
|
+
sleeping / closed eyes.
|
|
665
|
+
override_idle: ``True`` to hold the display indefinitely;
|
|
666
|
+
an ``int`` for a timed override.
|
|
667
|
+
override_animations: ``True`` to suppress platform animations.
|
|
668
|
+
"""
|
|
669
|
+
self["sleeping"] = not awake
|
|
670
|
+
self._show_page(PageTemplates.FACE, override_idle, override_animations)
|
|
671
|
+
|
|
672
|
+
def show_loading(
|
|
673
|
+
self,
|
|
674
|
+
text: str = "",
|
|
675
|
+
override_idle: Union[int, bool, None] = None,
|
|
676
|
+
override_animations: bool = False,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""Display a loading / progress animation with an optional label.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
text: Label shown below the spinner.
|
|
682
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
683
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
684
|
+
"""
|
|
685
|
+
self["label"] = text
|
|
686
|
+
self._show_page(PageTemplates.LOADING, override_idle, override_animations)
|
|
687
|
+
|
|
688
|
+
def show_status(
|
|
689
|
+
self,
|
|
690
|
+
text: str,
|
|
691
|
+
success: bool,
|
|
692
|
+
override_idle: Union[int, bool, None] = None,
|
|
693
|
+
override_animations: bool = False,
|
|
694
|
+
) -> None:
|
|
695
|
+
"""Display a success or failure result animation.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
text: Message to display.
|
|
699
|
+
success: ``True`` for success, ``False`` for failure.
|
|
700
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
701
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
702
|
+
"""
|
|
703
|
+
self["success"] = success
|
|
704
|
+
self["label"] = text
|
|
705
|
+
self._show_page(PageTemplates.STATUS, override_idle, override_animations)
|
|
706
|
+
|
|
707
|
+
def show_error(
|
|
708
|
+
self,
|
|
709
|
+
text: str,
|
|
710
|
+
detail: Optional[str] = None,
|
|
711
|
+
override_idle: Union[int, bool, None] = None,
|
|
712
|
+
override_animations: bool = False,
|
|
713
|
+
) -> None:
|
|
714
|
+
"""Display an error message.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
text: Primary error message.
|
|
718
|
+
detail: Optional secondary detail / traceback text.
|
|
719
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
720
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
721
|
+
"""
|
|
722
|
+
self["label"] = text
|
|
723
|
+
self["detail"] = detail
|
|
724
|
+
self._show_page(PageTemplates.ERROR, override_idle, override_animations)
|
|
725
|
+
|
|
726
|
+
def show_text(
|
|
727
|
+
self,
|
|
728
|
+
text: str,
|
|
729
|
+
title: Optional[str] = None,
|
|
730
|
+
override_idle: Union[int, bool, None] = None,
|
|
731
|
+
override_animations: bool = False,
|
|
732
|
+
) -> None:
|
|
733
|
+
"""Display a scrollable text view.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
text: Body text (auto-paginates for long content).
|
|
737
|
+
title: Optional heading displayed above the text.
|
|
738
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
739
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
740
|
+
"""
|
|
741
|
+
self["text"] = text
|
|
742
|
+
self["title"] = title
|
|
743
|
+
self._show_page(PageTemplates.TEXT, override_idle, override_animations)
|
|
744
|
+
|
|
745
|
+
def show_image(
|
|
746
|
+
self,
|
|
747
|
+
url: str,
|
|
748
|
+
caption: Optional[str] = None,
|
|
749
|
+
title: Optional[str] = None,
|
|
750
|
+
fill: Optional[FillMode] = None,
|
|
751
|
+
background_color: Optional[str] = None,
|
|
752
|
+
override_idle: Union[int, bool, None] = None,
|
|
753
|
+
override_animations: bool = False,
|
|
754
|
+
animated: bool = False,
|
|
755
|
+
) -> None:
|
|
756
|
+
"""Display a static or animated image.
|
|
757
|
+
|
|
758
|
+
Accepts a remote URL or a path to a local file. Local file paths
|
|
759
|
+
must exist at call time.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
url: HTTP(S) URL or absolute local file path.
|
|
763
|
+
caption: Caption shown below the image.
|
|
764
|
+
title: Heading shown above the image.
|
|
765
|
+
fill: How to scale the image within its display
|
|
766
|
+
area (see :class:`FillMode`). Defaults to
|
|
767
|
+
the display layer's own default.
|
|
768
|
+
background_color: Page background colour as a hex string,
|
|
769
|
+
e.g. ``"#000000"``.
|
|
770
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
771
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
772
|
+
animated: Set ``True`` to use the animated-image
|
|
773
|
+
template (GIF / WebP).
|
|
774
|
+
"""
|
|
775
|
+
if not url.startswith(("http://", "https://")) and not os.path.isfile(url):
|
|
776
|
+
LOG.error(f"Image not found: '{url}'")
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
self["image"] = url
|
|
780
|
+
self["title"] = title
|
|
781
|
+
self["caption"] = caption
|
|
782
|
+
self["fill"] = fill.value if isinstance(fill, FillMode) else fill
|
|
783
|
+
self["background_color"] = background_color
|
|
784
|
+
|
|
785
|
+
template = PageTemplates.ANIMATED_IMAGE if animated else PageTemplates.IMAGE
|
|
786
|
+
self._show_page(template, override_idle, override_animations)
|
|
787
|
+
|
|
788
|
+
def show_animated_image(
|
|
789
|
+
self,
|
|
790
|
+
url: str,
|
|
791
|
+
caption: Optional[str] = None,
|
|
792
|
+
title: Optional[str] = None,
|
|
793
|
+
fill: Optional[FillMode] = None,
|
|
794
|
+
background_color: Optional[str] = None,
|
|
795
|
+
override_idle: Union[int, bool, None] = None,
|
|
796
|
+
override_animations: bool = False,
|
|
797
|
+
) -> None:
|
|
798
|
+
"""Display an animated image (GIF or WebP).
|
|
799
|
+
|
|
800
|
+
Convenience wrapper around :meth:`show_image` with ``animated=True``.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
url: HTTP(S) URL or absolute local file path.
|
|
804
|
+
caption: Caption shown below the image.
|
|
805
|
+
title: Heading shown above the image.
|
|
806
|
+
fill: How to scale the image (see :class:`FillMode`).
|
|
807
|
+
background_color: Page background colour as a hex string.
|
|
808
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
809
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
810
|
+
"""
|
|
811
|
+
self.show_image(
|
|
812
|
+
url, caption, title, fill, background_color,
|
|
813
|
+
override_idle, override_animations, animated=True,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
def show_html(
|
|
817
|
+
self,
|
|
818
|
+
html: str,
|
|
819
|
+
resource_url: Optional[str] = None,
|
|
820
|
+
override_idle: Union[int, bool, None] = None,
|
|
821
|
+
override_animations: bool = False,
|
|
822
|
+
) -> None:
|
|
823
|
+
"""Render an HTML string in the GUI.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
html: Raw HTML content to display.
|
|
827
|
+
resource_url: Base URL used to resolve relative resource
|
|
828
|
+
references inside the HTML.
|
|
829
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
830
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
831
|
+
"""
|
|
832
|
+
self["html"] = html
|
|
833
|
+
self["resource_url"] = resource_url
|
|
834
|
+
self._show_page(PageTemplates.HTML, override_idle, override_animations)
|
|
835
|
+
|
|
836
|
+
def show_url(
|
|
837
|
+
self,
|
|
838
|
+
url: str,
|
|
839
|
+
override_idle: Union[int, bool, None] = None,
|
|
840
|
+
override_animations: bool = False,
|
|
841
|
+
) -> None:
|
|
842
|
+
"""Open a URL in the GUI's web renderer.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
url: Fully-qualified URL to load.
|
|
846
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
847
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
848
|
+
"""
|
|
849
|
+
self["url"] = url
|
|
850
|
+
self._show_page(PageTemplates.URL, override_idle, override_animations)
|
|
851
|
+
|
|
852
|
+
def show_weather(
|
|
853
|
+
self,
|
|
854
|
+
current_temp: Union[int, float],
|
|
855
|
+
min_temp: Union[int, float],
|
|
856
|
+
max_temp: Union[int, float],
|
|
857
|
+
condition: str,
|
|
858
|
+
icon: Optional[str] = None,
|
|
859
|
+
location: Optional[str] = None,
|
|
860
|
+
override_idle: Union[int, bool, None] = None,
|
|
861
|
+
override_animations: bool = False,
|
|
862
|
+
) -> None:
|
|
863
|
+
"""Display a weather summary card.
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
current_temp: Current temperature value.
|
|
867
|
+
min_temp: Daily low temperature.
|
|
868
|
+
max_temp: Daily high temperature.
|
|
869
|
+
condition: Human-readable weather condition label
|
|
870
|
+
(e.g. ``"Partly cloudy"``).
|
|
871
|
+
icon: Optional URL or path to a weather icon.
|
|
872
|
+
location: Optional location name to display.
|
|
873
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
874
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
875
|
+
"""
|
|
876
|
+
self["current_temp"] = current_temp
|
|
877
|
+
self["min_temp"] = min_temp
|
|
878
|
+
self["max_temp"] = max_temp
|
|
879
|
+
self["condition"] = condition
|
|
880
|
+
self["icon"] = icon
|
|
881
|
+
self["location"] = location
|
|
882
|
+
self._show_page(PageTemplates.WEATHER, override_idle, override_animations)
|
|
883
|
+
|
|
884
|
+
def show_list(
|
|
885
|
+
self,
|
|
886
|
+
items: List[Union[ListItem, Dict[str, Any]]],
|
|
887
|
+
title: Optional[str] = None,
|
|
888
|
+
override_idle: Union[int, bool, None] = None,
|
|
889
|
+
override_animations: bool = False,
|
|
890
|
+
) -> None:
|
|
891
|
+
"""Display a scrollable list of items.
|
|
892
|
+
|
|
893
|
+
Each item must have at least a ``title``. An optional ``subtitle``
|
|
894
|
+
and ``image`` thumbnail are also supported.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
items: List of :class:`ListItem` instances or plain
|
|
898
|
+
dicts with at minimum a ``"title"`` key.
|
|
899
|
+
title: Optional heading for the list page.
|
|
900
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
901
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
902
|
+
"""
|
|
903
|
+
serialised = [
|
|
904
|
+
i.as_dict() if isinstance(i, ListItem) else i
|
|
905
|
+
for i in items
|
|
906
|
+
]
|
|
907
|
+
self["title"] = title
|
|
908
|
+
self["items"] = serialised
|
|
909
|
+
self._show_page(PageTemplates.LIST, override_idle, override_animations)
|
|
910
|
+
|
|
911
|
+
def show_grid(
|
|
912
|
+
self,
|
|
913
|
+
items: List[Union[GridItem, Dict[str, Any]]],
|
|
914
|
+
title: Optional[str] = None,
|
|
915
|
+
override_idle: Union[int, bool, None] = None,
|
|
916
|
+
override_animations: bool = False,
|
|
917
|
+
) -> None:
|
|
918
|
+
"""Display a 2-D tile grid of image-primary items.
|
|
919
|
+
|
|
920
|
+
Unlike :meth:`show_list`, items in a grid carry no inherent hierarchy.
|
|
921
|
+
They are visually equal-weight tiles; the display layer decides the
|
|
922
|
+
column count based on the screen size. Use a grid for content where
|
|
923
|
+
the image is the primary identifier — album covers, photo libraries,
|
|
924
|
+
skill icons.
|
|
925
|
+
|
|
926
|
+
Args:
|
|
927
|
+
items: List of :class:`GridItem` instances or plain
|
|
928
|
+
dicts with at minimum an ``"image"`` key.
|
|
929
|
+
title: Optional heading for the grid page.
|
|
930
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
931
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
932
|
+
"""
|
|
933
|
+
serialised = [
|
|
934
|
+
i.as_dict() if isinstance(i, GridItem) else i
|
|
935
|
+
for i in items
|
|
936
|
+
]
|
|
937
|
+
self["title"] = title
|
|
938
|
+
self["items"] = serialised
|
|
939
|
+
self._show_page(PageTemplates.GRID, override_idle, override_animations)
|
|
940
|
+
|
|
941
|
+
def show_table(
|
|
942
|
+
self,
|
|
943
|
+
columns: List[str],
|
|
944
|
+
rows: List[List[Any]],
|
|
945
|
+
title: Optional[str] = None,
|
|
946
|
+
override_idle: Union[int, bool, None] = None,
|
|
947
|
+
override_animations: bool = False,
|
|
948
|
+
) -> None:
|
|
949
|
+
"""Display a columnar data table with named headers.
|
|
950
|
+
|
|
951
|
+
Use a table for relational or comparative data where column identity
|
|
952
|
+
is meaningful — schedules, scores, comparisons, prices. Each row
|
|
953
|
+
must have the same number of values as there are columns.
|
|
954
|
+
|
|
955
|
+
Args:
|
|
956
|
+
columns: Ordered list of column header strings.
|
|
957
|
+
rows: List of rows; each row is an ordered list of
|
|
958
|
+
values aligned to *columns*. Values must be
|
|
959
|
+
JSON-serialisable.
|
|
960
|
+
title: Optional heading for the table page.
|
|
961
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
962
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
963
|
+
|
|
964
|
+
Raises:
|
|
965
|
+
ValueError: When any row length does not match the column count.
|
|
966
|
+
"""
|
|
967
|
+
col_count = len(columns)
|
|
968
|
+
for i, row in enumerate(rows):
|
|
969
|
+
if len(row) != col_count:
|
|
970
|
+
raise ValueError(
|
|
971
|
+
f"Row {i} has {len(row)} value(s) but there are "
|
|
972
|
+
f"{col_count} column(s)."
|
|
973
|
+
)
|
|
974
|
+
self["title"] = title
|
|
975
|
+
self["columns"] = columns
|
|
976
|
+
self["rows"] = rows
|
|
977
|
+
self._show_page(PageTemplates.TABLE, override_idle, override_animations)
|
|
978
|
+
|
|
979
|
+
def show_audio_player(
|
|
980
|
+
self,
|
|
981
|
+
title: str,
|
|
982
|
+
artist: Optional[str] = None,
|
|
983
|
+
album: Optional[str] = None,
|
|
984
|
+
image: Optional[str] = None,
|
|
985
|
+
position: float = 0.0,
|
|
986
|
+
duration: float = 0.0,
|
|
987
|
+
playing: bool = True,
|
|
988
|
+
override_idle: Union[int, bool, None] = True,
|
|
989
|
+
override_animations: bool = False,
|
|
990
|
+
) -> None:
|
|
991
|
+
"""Display a now-playing card for audio playback.
|
|
992
|
+
|
|
993
|
+
This template shows track metadata. The actual audio is managed by
|
|
994
|
+
the audio service; this call only updates the visual layer.
|
|
995
|
+
|
|
996
|
+
Call this method again whenever playback state changes (e.g. track
|
|
997
|
+
changes, pause/resume) to keep the display in sync.
|
|
998
|
+
|
|
999
|
+
Args:
|
|
1000
|
+
title: Track title.
|
|
1001
|
+
artist: Artist name.
|
|
1002
|
+
album: Album name.
|
|
1003
|
+
image: URL or path to album art.
|
|
1004
|
+
position: Current playback position in seconds.
|
|
1005
|
+
duration: Total track duration in seconds.
|
|
1006
|
+
``0`` means unknown / streaming.
|
|
1007
|
+
playing: ``True`` if currently playing,
|
|
1008
|
+
``False`` if paused.
|
|
1009
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1010
|
+
Defaults to ``True`` (hold while playing).
|
|
1011
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1012
|
+
"""
|
|
1013
|
+
self["title"] = title
|
|
1014
|
+
self["artist"] = artist
|
|
1015
|
+
self["album"] = album
|
|
1016
|
+
self["image"] = image
|
|
1017
|
+
self["position"] = position
|
|
1018
|
+
self["duration"] = duration
|
|
1019
|
+
self["playing"] = playing
|
|
1020
|
+
self._show_page(PageTemplates.AUDIO_PLAYER, override_idle, override_animations)
|
|
1021
|
+
|
|
1022
|
+
def show_video_player(
|
|
1023
|
+
self,
|
|
1024
|
+
uri: str,
|
|
1025
|
+
title: Optional[str] = None,
|
|
1026
|
+
playing: bool = True,
|
|
1027
|
+
override_idle: Union[int, bool, None] = True,
|
|
1028
|
+
override_animations: bool = False,
|
|
1029
|
+
) -> None:
|
|
1030
|
+
"""Display an embedded video playback surface.
|
|
1031
|
+
|
|
1032
|
+
Unlike :meth:`show_audio_player`, the display layer is responsible
|
|
1033
|
+
for rendering the video stream itself.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
uri: URI of the video stream or file to play.
|
|
1037
|
+
title: Optional title overlay.
|
|
1038
|
+
playing: ``True`` to start playing immediately,
|
|
1039
|
+
``False`` to start paused.
|
|
1040
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1041
|
+
Defaults to ``True`` (hold while playing).
|
|
1042
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1043
|
+
"""
|
|
1044
|
+
self["uri"] = uri
|
|
1045
|
+
self["title"] = title
|
|
1046
|
+
self["playing"] = playing
|
|
1047
|
+
self._show_page(PageTemplates.VIDEO_PLAYER, override_idle, override_animations)
|
|
1048
|
+
|
|
1049
|
+
def show_clock(
|
|
1050
|
+
self,
|
|
1051
|
+
override_idle: Union[int, bool, None] = True,
|
|
1052
|
+
override_animations: bool = False,
|
|
1053
|
+
) -> None:
|
|
1054
|
+
"""Display the clock / time page.
|
|
1055
|
+
|
|
1056
|
+
The clock page is self-updating on the display layer; no session data
|
|
1057
|
+
is required.
|
|
1058
|
+
|
|
1059
|
+
Args:
|
|
1060
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1061
|
+
Defaults to ``True`` (hold indefinitely).
|
|
1062
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1063
|
+
"""
|
|
1064
|
+
self._show_page(PageTemplates.CLOCK, override_idle, override_animations)
|
|
1065
|
+
|
|
1066
|
+
def show_timer(
|
|
1067
|
+
self,
|
|
1068
|
+
end_time: float,
|
|
1069
|
+
label: Optional[str] = None,
|
|
1070
|
+
count_up: bool = False,
|
|
1071
|
+
override_idle: Union[int, bool, None] = True,
|
|
1072
|
+
override_animations: bool = False,
|
|
1073
|
+
) -> None:
|
|
1074
|
+
"""Display a countdown or count-up timer.
|
|
1075
|
+
|
|
1076
|
+
The display layer is responsible for updating the displayed time; it
|
|
1077
|
+
derives the remaining/elapsed duration from ``end_time`` and the
|
|
1078
|
+
device clock, so no polling from the skill is needed.
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
end_time: Unix timestamp (seconds since epoch) at which
|
|
1082
|
+
the timer expires. Use
|
|
1083
|
+
``time.time() + seconds`` to set a countdown.
|
|
1084
|
+
label: Optional label shown alongside the timer
|
|
1085
|
+
(e.g. ``"Pasta"``).
|
|
1086
|
+
count_up: ``False`` (default) counts down to zero;
|
|
1087
|
+
``True`` counts up from zero (stopwatch mode,
|
|
1088
|
+
where ``end_time`` is the start time).
|
|
1089
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1090
|
+
Defaults to ``True`` (hold until dismissed).
|
|
1091
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1092
|
+
"""
|
|
1093
|
+
self["end_time"] = end_time
|
|
1094
|
+
self["label"] = label
|
|
1095
|
+
self["count_up"] = count_up
|
|
1096
|
+
self._show_page(PageTemplates.TIMER, override_idle, override_animations)
|
|
1097
|
+
|
|
1098
|
+
def show_map(
|
|
1099
|
+
self,
|
|
1100
|
+
latitude: float,
|
|
1101
|
+
longitude: float,
|
|
1102
|
+
zoom: int = 12,
|
|
1103
|
+
label: Optional[str] = None,
|
|
1104
|
+
override_idle: Union[int, bool, None] = None,
|
|
1105
|
+
override_animations: bool = False,
|
|
1106
|
+
) -> None:
|
|
1107
|
+
"""Display a geographic location on a map.
|
|
1108
|
+
|
|
1109
|
+
The display layer is responsible for choosing the map provider and
|
|
1110
|
+
rendering strategy.
|
|
1111
|
+
|
|
1112
|
+
Args:
|
|
1113
|
+
latitude: WGS-84 latitude in decimal degrees.
|
|
1114
|
+
longitude: WGS-84 longitude in decimal degrees.
|
|
1115
|
+
zoom: Map zoom level (1 = whole world,
|
|
1116
|
+
20 = building detail). Default 12.
|
|
1117
|
+
label: Optional place name or annotation shown on
|
|
1118
|
+
the map.
|
|
1119
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1120
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1121
|
+
"""
|
|
1122
|
+
self["latitude"] = latitude
|
|
1123
|
+
self["longitude"] = longitude
|
|
1124
|
+
self["zoom"] = zoom
|
|
1125
|
+
self["label"] = label
|
|
1126
|
+
self._show_page(PageTemplates.MAP, override_idle, override_animations)
|
|
1127
|
+
|
|
1128
|
+
def show_confirm(
|
|
1129
|
+
self,
|
|
1130
|
+
question: str,
|
|
1131
|
+
override_idle: Union[int, bool, None] = None,
|
|
1132
|
+
override_animations: bool = False,
|
|
1133
|
+
) -> None:
|
|
1134
|
+
"""Display a yes/no confirmation page alongside a voice dialogue.
|
|
1135
|
+
|
|
1136
|
+
OVOS is voice-first. This method only provides a visual accompaniment
|
|
1137
|
+
to a spoken question that the skill must ask concurrently. On
|
|
1138
|
+
display-only devices the voice response is the only available path.
|
|
1139
|
+
|
|
1140
|
+
A touch shortcut (if supported by the display layer) fires:
|
|
1141
|
+
``<skill_id>.confirm.response`` with ``{"confirmed": bool}``.
|
|
1142
|
+
|
|
1143
|
+
The skill is responsible for registering a handler for that event
|
|
1144
|
+
*and* for the spoken yes/no response, and must not block waiting
|
|
1145
|
+
exclusively for a GUI event.
|
|
1146
|
+
|
|
1147
|
+
Args:
|
|
1148
|
+
question: The question being spoken to the user.
|
|
1149
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1150
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1151
|
+
"""
|
|
1152
|
+
self["question"] = question
|
|
1153
|
+
self._show_page(PageTemplates.CONFIRM, override_idle, override_animations)
|
|
1154
|
+
|
|
1155
|
+
def show_select(
|
|
1156
|
+
self,
|
|
1157
|
+
items: List[Union[SelectItem, Dict[str, Any]]],
|
|
1158
|
+
prompt: Optional[str] = None,
|
|
1159
|
+
override_idle: Union[int, bool, None] = None,
|
|
1160
|
+
override_animations: bool = False,
|
|
1161
|
+
) -> None:
|
|
1162
|
+
"""Display a list of options alongside a voice choice dialogue.
|
|
1163
|
+
|
|
1164
|
+
OVOS is voice-first. This method only provides a visual accompaniment
|
|
1165
|
+
to spoken options that the skill must present concurrently. On
|
|
1166
|
+
display-only devices the voice response is the only available path.
|
|
1167
|
+
|
|
1168
|
+
A touch shortcut (if supported by the display layer) fires:
|
|
1169
|
+
``<skill_id>.select.response`` with ``{"value": <selected value>}``.
|
|
1170
|
+
|
|
1171
|
+
The skill is responsible for registering a handler for that event
|
|
1172
|
+
*and* for the spoken selection, and must not block waiting exclusively
|
|
1173
|
+
for a GUI event.
|
|
1174
|
+
|
|
1175
|
+
Args:
|
|
1176
|
+
items: Ordered list of :class:`SelectItem` instances
|
|
1177
|
+
or plain dicts with ``"label"`` and
|
|
1178
|
+
``"value"`` keys.
|
|
1179
|
+
prompt: Optional spoken prompt echoed on screen.
|
|
1180
|
+
override_idle: Idle override (see :meth:`_show_pages`).
|
|
1181
|
+
override_animations: Animation override (see :meth:`_show_pages`).
|
|
1182
|
+
"""
|
|
1183
|
+
serialised = [
|
|
1184
|
+
i.as_dict() if isinstance(i, SelectItem) else i
|
|
1185
|
+
for i in items
|
|
1186
|
+
]
|
|
1187
|
+
self["prompt"] = prompt
|
|
1188
|
+
self["items"] = serialised
|
|
1189
|
+
self._show_page(PageTemplates.SELECT, override_idle, override_animations)
|