From 7b988db8da110455a002dc11291c94aa1cd23957 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 18 Sep 2024 23:18:37 +0100 Subject: [PATCH 01/13] refactor!:deprecate QML upload from bus never worked right, causes more issues than it helps --- README.md | 7 -- ovos_gui/bus.py | 11 +- ovos_gui/constants.py | 4 + ovos_gui/gui_file_server.py | 1 - ovos_gui/namespace.py | 172 ++++--------------------------- ovos_gui/page.py | 57 ++-------- test/unittests/test_namespace.py | 4 +- 7 files changed, 41 insertions(+), 215 deletions(-) create mode 100644 ovos_gui/constants.py diff --git a/README.md b/README.md index 45de849..c374dff 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,6 @@ under mycroft.conf // "gui_file_server": true, // "file_server_port": 8000, - // Optional support for collecting GUI files for container support - // The ovos-gui container path for these files will be {XDG_CACHE_HOME}/ovos_gui_file_server. - // With the below configuration, the GUI client will have files prefixed with the configured host path, - // so the example below describes a situation where `{XDG_CACHE_HOME}/ovos_gui_file_server` maps - // to `/tmp/gui_files` on the filesystem where the GUI client is running. - // "gui_file_host_path": "/tmp/gui_files", - // Optionally specify a default qt version for connected clients that don't report it "default_qt_version": 5 }, diff --git a/ovos_gui/bus.py b/ovos_gui/bus.py index 0c82233..de38995 100644 --- a/ovos_gui/bus.py +++ b/ovos_gui/bus.py @@ -131,13 +131,9 @@ def get_client_pages(self, namespace): @return: list of page URIs for this GUI Client """ client_pages = [] - server_url = self.ns_manager.gui_file_server.url if \ - self.ns_manager.gui_file_server else \ - self.ns_manager.gui_file_host_path for page in namespace.pages: - uri = page.get_uri(self.framework, server_url) + uri = page.get_uri(self.framework) client_pages.append(uri) - return client_pages def synchronize(self): @@ -260,16 +256,13 @@ def send_gui_pages(self, pages: List[GuiPage], namespace: str, @param namespace: namespace to put GuiPages in @param position: position to insert pages at """ - server_url = self.ns_manager.gui_file_server.url if \ - self.ns_manager.gui_file_server else \ - self.ns_manager.gui_file_host_path framework = self.framework message = { "type": "mycroft.gui.list.insert", "namespace": namespace, "position": position, - "data": [{"url": page.get_uri(framework, server_url)} + "data": [{"url": page.get_uri(framework)} for page in pages] } LOG.debug(f"Showing pages: {message['data']}") diff --git a/ovos_gui/constants.py b/ovos_gui/constants.py new file mode 100644 index 0000000..c3551f1 --- /dev/null +++ b/ovos_gui/constants.py @@ -0,0 +1,4 @@ +from ovos_config.locations import get_xdg_cache_save_path + +GUI_CACHE_PATH = get_xdg_cache_save_path('ovos_gui') + diff --git a/ovos_gui/gui_file_server.py b/ovos_gui/gui_file_server.py index 8b352c1..869fc8d 100644 --- a/ovos_gui/gui_file_server.py +++ b/ovos_gui/gui_file_server.py @@ -5,7 +5,6 @@ from threading import Thread, Event from ovos_config import Configuration -from ovos_utils.file_utils import get_temp_path from ovos_utils.log import LOG _HTTP_SERVER = None diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 3aeb009..1725eb4 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -40,13 +40,12 @@ over the GUI message bus. """ import shutil -from os import makedirs from os.path import join, dirname, isfile, exists from threading import Event, Lock, Timer from typing import List, Union, Optional, Dict from ovos_config.config import Configuration -from ovos_utils.log import LOG, log_deprecation +from ovos_utils.log import LOG from ovos_bus_client import Message, MessageBusClient from ovos_gui.bus import ( @@ -57,7 +56,7 @@ ) from ovos_gui.gui_file_server import start_gui_http_server from ovos_gui.page import GuiPage - +from ovos_gui.constants import GUI_CACHE_PATH namespace_lock = Lock() RESERVED_KEYS = ['__from', '__idle'] @@ -72,9 +71,9 @@ def _validate_page_message(message: Message) -> bool: @returns: True if request is valid, else False """ valid = ( - "page" in message.data + "page_names" in message.data and "__from" in message.data - and isinstance(message.data["page"], list) + and isinstance(message.data["page_names"], list) ) if not valid: if message.msg_type == "gui.page.show": @@ -296,7 +295,7 @@ def load_pages(self, pages: List[GuiPage], show_index: int = 0): target_page = pages[show_index] for page in pages: - if page.id not in [p.id for p in self.pages]: + if page.name not in [p.name for p in self.pages]: new_pages.append(page) self.pages.extend(new_pages) @@ -335,7 +334,7 @@ def focus_page(self, page): # set the index of the page in the self.pages list page_index = None for i, p in enumerate(self.pages): - if p.id == page.id: + if p.name == page.name: # save page index page_index = i break @@ -433,9 +432,6 @@ def __init__(self, core_bus: MessageBusClient): self._system_res_dir = join(dirname(__file__), "res", "gui") self._ready_event = Event() self.gui_file_server = None - self.gui_file_path = None # HTTP Server local path - self.gui_file_host_path = None # Docker host path - self._connected_frameworks: List[str] = list() self._init_gui_file_share() self._define_message_handlers() @@ -450,20 +446,10 @@ def _init_gui_file_share(self): If `gui_file_server` is defined, resources will be served via HTTP """ config = Configuration().get("gui", {}) - self.gui_file_host_path = config.get("gui_file_host_path") - - # Check for GUI file sharing via HTTP server or mounted host path - if config.get("gui_file_server") or self.gui_file_host_path: - from ovos_utils.xdg_utils import xdg_cache_home - if config.get("server_path"): - log_deprecation("`server_path` configuration is deprecated. " - "Files will always be saved to " - "XDG_CACHE_HOME/ovos_gui_file_server", "0.1.0") - self.gui_file_path = config.get("server_path") or \ - join(xdg_cache_home(), "ovos_gui_file_server") - if config.get("gui_file_server"): - self.gui_file_server = start_gui_http_server(self.gui_file_path) - self._upload_system_resources() + # Check for GUI file sharing via HTTP server + if config.get("gui_file_server"): + self.gui_file_server = start_gui_http_server(GUI_CACHE_PATH) + self._cache_system_resources() def _define_message_handlers(self): """ @@ -474,7 +460,6 @@ def _define_message_handlers(self): self.core_bus.on("gui.page.delete", self.handle_delete_page) self.core_bus.on("gui.page.delete.all", self.handle_delete_all_pages) self.core_bus.on("gui.page.show", self.handle_show_page) - self.core_bus.on("gui.page.upload", self.handle_receive_gui_pages) self.core_bus.on("gui.status.request", self.handle_status_request) self.core_bus.on("gui.value.set", self.handle_set_value) self.core_bus.on("mycroft.gui.connected", self.handle_client_connected) @@ -497,66 +482,6 @@ def _define_message_handlers(self): def handle_ready(self, message): self._ready_event.set() - def handle_gui_pages_available(self, message: Message): - """ - Handle a skill or plugin advertising that it has GUI pages available to - upload. If there are connected clients, request pages for each connected - GUI framework. - @param message: `gui.volunteer_page_upload` message - """ - if not any((self.gui_file_host_path, self.gui_file_server)): - LOG.debug("No GUI file server running or host path configured") - return - - LOG.debug(f"Requesting resources for {self._connected_frameworks}") - for framework in self._connected_frameworks: - skill_id = message.data.get("skill_id") - self.core_bus.emit(message.reply("gui.request_page_upload", - {'skill_id': skill_id, - 'framework': framework}, - {"source": "gui", - "destination": ["skills", - "PHAL"]})) - - def handle_receive_gui_pages(self, message: Message): - """ - Handle GUI resources from a skill or plugin. Pages are written to - `self.server_path` which is accessible via a lightweight HTTP server and - may additionally be mounted to a host path/volume in container setups. - @param message: Message containing UI resource file contents and meta - message.data: - pages: dict page_filename to encoded bytes content; - paths are relative to the `framework` directory, so a page - for framework `all` could be `qt5/subdir/file.qml` and the - equivalent page for framework `qt5` would be - `subdir/file.qml` - framework: `all` if all GUI resources are included, else the - specific GUI framework (i.e. `qt5`, `qt6`) - __from: skill_id of module uploading GUI resources - """ - for page, contents in message.data["pages"].items(): - try: - if message.data.get("framework") == "all": - # All GUI resources are uploaded - resource_base_path = join(self.gui_file_path, - message.data['__from']) - else: - resource_base_path = join(self.gui_file_path, - message.data['__from'], - message.data.get('framework') or - "qt5") - byte_contents = bytes.fromhex(contents) - file_path = join(resource_base_path, page) - LOG.debug(f"writing UI file: {file_path}") - makedirs(dirname(file_path), exist_ok=True) - with open(file_path, 'wb+') as f: - f.write(byte_contents) - except Exception as e: - LOG.exception(f"Failed to write {page}: {e}") - if message.data["__from"] == self._active_homescreen: - # Configured home screen skill just uploaded pages, show it again - self.core_bus.emit(message.forward("homescreen.manager.show_active")) - def handle_clear_namespace(self, message: Message): """ Handles a request to remove a namespace. @@ -621,8 +546,7 @@ def handle_delete_page(self, message: Message): message_is_valid = _validate_page_message(message) if message_is_valid: namespace_name = message.data["__from"] - pages_to_remove = message.data.get("page_names") or \ - message.data.get("page") # backwards compat + pages_to_remove = message.data.get("page_names") LOG.debug(f"Got {namespace_name} request to delete: {pages_to_remove}") with namespace_lock: self._remove_pages(namespace_name, pages_to_remove) @@ -665,24 +589,6 @@ def _parse_persistence(persistence: Optional[Union[int, bool]]) -> \ # Defines default behavior as displaying for 30 seconds return False, 30 - def _legacy_show_page(self, message: Message) -> List[GuiPage]: - """ - Backwards-compat method to handle messages without ui_directories and - page_names. - @param message: message requesting to display pages - @return: list of GuiPage objects - """ - pages_to_show = message.data["page"] - LOG.info(f"Handling legacy page show request. pages={pages_to_show}") - - pages_to_load = list() - persist, duration = self._parse_persistence(message.data["__idle"]) - for page in pages_to_show: - name = page.split('/')[-1] - # check if persistence is type of int or bool - pages_to_load.append(GuiPage(page, name, persist, duration)) - return pages_to_load - def handle_show_page(self, message: Message): """ Handles a request to show one or more pages on the screen. @@ -695,39 +601,19 @@ def handle_show_page(self, message: Message): namespace_name = message.data["__from"] page_ids_to_show = message.data.get('page_names') - page_resource_dirs = message.data.get('ui_directories') persistence = message.data["__idle"] - show_index = message.data.get("index", None) + show_index = message.data.get("index", 0) LOG.debug(f"Got {namespace_name} request to show: {page_ids_to_show} at index: {show_index}") - if not page_resource_dirs and page_ids_to_show and \ - all((x.startswith("SYSTEM") for x in page_ids_to_show)): - page_resource_dirs = {"all": self._system_res_dir} - - if not all((page_ids_to_show, page_resource_dirs)): - LOG.warning(f"GUI resources have not yet been uploaded for namespace: {namespace_name}") - pages = self._legacy_show_page(message) - else: - pages = list() - persist, duration = self._parse_persistence(message.data["__idle"]) - for page in page_ids_to_show: - url = None - name = page - if isfile(page): - LOG.warning(f"Expected resource name but got file: {url}") - name = page.split('/')[-1] - url = f"file://{page}" - elif "://" in page: - LOG.warning(f"Expected resource name but got URI: {page}") - name = page.split('/')[-1] - url = page - pages.append(GuiPage(url, name, persist, duration, - page, namespace_name, page_resource_dirs)) + pages = list() + persist, duration = self._parse_persistence(message.data["__idle"]) + for page in page_ids_to_show: + pages.append(GuiPage(name=page, persistent=persist, duration=duration, + namespace=namespace_name)) if not pages: - LOG.error(f"Activated namespace '{namespace_name}' has no pages! " - f"Did you provide 'ui_directories' ?") + LOG.error(f"Activated namespace '{namespace_name}' has no pages!") LOG.error(f"Can't show page, bad message: {message.data}") return @@ -961,23 +847,9 @@ def handle_client_connected(self, message: Message): websocket_config = get_gui_websocket_config() port = websocket_config["base_port"] message = message.forward("mycroft.gui.port", - dict(port=port, gui_id=gui_id)) + dict(port=port, gui_id=gui_id, framework=framework)) self.core_bus.emit(message) - if self.gui_file_path or self.gui_file_host_path: - if not self._ready_event.wait(90): - LOG.warning("Not reported ready after 90s") - if framework not in self._connected_frameworks: - LOG.debug(f"Requesting page upload for {framework}") - self.core_bus.emit(Message("gui.request_page_upload", - {'framework': framework}, - {"source": "gui", - "destination": ["skills", "PHAL"]})) - - if framework not in self._connected_frameworks: - LOG.debug(f"Connecting framework: {framework}") - self._connected_frameworks.append(framework) - def handle_page_interaction(self, message: Message): """ Handles an event from the GUI indicating a page has been interacted with. @@ -1046,13 +918,13 @@ def _del_namespace_in_remove_timers(self, namespace_name: str): if namespace_name in self.remove_namespace_timers: del self.remove_namespace_timers[namespace_name] - def _upload_system_resources(self): + def _cache_system_resources(self): """ Copy system GUI resources to the served file path """ - output_path = join(self.gui_file_path, "system") + output_path = f"{GUI_CACHE_PATH}/system" if exists(output_path): LOG.info(f"Removing existing system resources before updating") shutil.rmtree(output_path) shutil.copytree(self._system_res_dir, output_path) - LOG.debug(f"Copied system resources to {self.gui_file_path}") + LOG.debug(f"Copied system resources from {self._system_res_dir} to {output_path}") diff --git a/ovos_gui/page.py b/ovos_gui/page.py index b6eeef7..5085aec 100644 --- a/ovos_gui/page.py +++ b/ovos_gui/page.py @@ -2,6 +2,7 @@ from typing import Union, Optional from dataclasses import dataclass from ovos_utils.log import LOG +from ovos_gui.constants import GUI_CACHE_PATH @dataclass @@ -10,29 +11,17 @@ class GuiPage: A GuiPage represents a single GUI Display within a given namespace. A Page can either be `persistent` or be removed after some `duration`. Note that a page is generally framework-independent - @param url: URI (local or network path) of the GUI Page @param name: Name of the page as shown in its namespace (could @param persistent: If True, page is displayed indefinitely @param duration: Number of seconds to display the page for @param namespace: Skill/component identifier - @param page_id: Page identifier - (file path relative to gui_framework directory with no extension) """ - url: Optional[str] # This param is left for backwards-compat. name: str persistent: bool duration: Union[int, bool] - page_id: Optional[str] = None namespace: Optional[str] = None resource_dirs: Optional[dict] = None - @property - def id(self): - """ - Get a unique identifier for this page. - """ - return self.page_id or self.url - @staticmethod def get_file_extension(framework: str) -> str: """ @@ -44,45 +33,21 @@ def get_file_extension(framework: str) -> str: return "qml" return "" - def get_uri(self, framework: str = "qt5", server_url: str = None) -> str: + @property + def res_namespace(self): + return "system" if self.name.startswith("SYSTEM") else self.namespace + + def get_uri(self, framework: str = "qt5") -> str: """ Get a valid URI for this Page. - @param framework: String GUI framework to get resources for - @param server_url: String server URL if available; this could be for a - web server (http://), or a container host path (file://) + @param framework: String GUI framework to get resources for (currently only 'qt5') @return: Absolute path to the requested resource """ - if self.url: - LOG.warning(f"Static URI: {self.url}") - return self.url - - res_filename = f"{self.page_id}.{self.get_file_extension(framework)}" - res_namespace = "system" if self.page_id.startswith("SYSTEM") else \ - self.namespace - if server_url: - if "://" not in server_url: - if server_url.startswith("/"): - LOG.debug(f"No schema in server_url, assuming 'file'") - server_url = f"file://{server_url}" - else: - LOG.debug(f"No schema in server_url, assuming 'http'") - server_url = f"http://{server_url}" - path = f"{server_url}/{res_namespace}/{framework}/{res_filename}" - LOG.info(f"Resolved server URI: {path}") + res_filename = f"{self.name}.{self.get_file_extension(framework)}" + path = f"{GUI_CACHE_PATH}/{self.res_namespace}/{framework}/{res_filename}" + LOG.debug(f"Resolved page URI: {path}") + if isfile(path): return path - base_path = self.resource_dirs.get(framework) - if not base_path and self.resource_dirs.get("all"): - file_path = join(self.resource_dirs.get('all'), framework, - res_filename) - else: - file_path = join(base_path, res_filename) - if isfile(file_path): - return file_path - # Check system resources - file_path = join(dirname(__file__), "res", "gui", framework, - res_filename) - if isfile(file_path): - return file_path raise FileNotFoundError(f"Unable to resolve resource file for " f"resource {res_filename} for framework " f"{framework}") diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index e33a5cd..0f5b97c 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -458,12 +458,12 @@ def test_upload_system_resources(self): test_dir = join(dirname(__file__), "upload_test") makedirs(test_dir, exist_ok=True) self.namespace_manager.gui_file_path = test_dir - self.namespace_manager._upload_system_resources() + self.namespace_manager._cache_system_resources() self.assertTrue(isdir(join(test_dir, "system", "qt5"))) self.assertTrue(isfile(join(test_dir, "system", "qt5", "SYSTEM_TextFrame.qml"))) # Test repeated copy doesn't raise any exception - self.namespace_manager._upload_system_resources() + self.namespace_manager._cache_system_resources() self.assertTrue(isdir(join(test_dir, "system", "qt5"))) self.assertTrue(isfile(join(test_dir, "system", "qt5", "SYSTEM_TextFrame.qml"))) From 348ad79436612a75f720e0f3c98a48ec8ffd58dd Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 19:49:00 +0100 Subject: [PATCH 02/13] deprecate file server --- README.md | 5 +--- ovos_gui/gui_file_server.py | 57 ------------------------------------- ovos_gui/namespace.py | 6 ---- 3 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 ovos_gui/gui_file_server.py diff --git a/README.md b/README.md index c374dff..73be4bd 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,8 @@ under mycroft.conf "homescreen_supported": false }, - // Optional file server support for remote clients - // "gui_file_server": true, - // "file_server_port": 8000, - // Optionally specify a default qt version for connected clients that don't report it + // NOTE: currently only QT5 clients exist "default_qt_version": 5 }, diff --git a/ovos_gui/gui_file_server.py b/ovos_gui/gui_file_server.py deleted file mode 100644 index 869fc8d..0000000 --- a/ovos_gui/gui_file_server.py +++ /dev/null @@ -1,57 +0,0 @@ -import http.server -import os -import shutil -import socketserver -from threading import Thread, Event - -from ovos_config import Configuration -from ovos_utils.log import LOG - -_HTTP_SERVER = None - - -class GuiFileHandler(http.server.SimpleHTTPRequestHandler): - def end_headers(self) -> None: - mimetype = self.guess_type(self.path) - is_file = not self.path.endswith('/') - if is_file and any([mimetype.startswith(prefix) for - prefix in ("text/", "application/octet-stream")]): - self.send_header('Content-Type', "text/plain") - self.send_header('Content-Disposition', 'inline') - super().end_headers() - - -def start_gui_http_server(qml_path: str, port: int = None): - """ - Start an http server to host GUI Resources - @param qml_path: Local file path to server - @param port: Host port to run file server on - @return: Initialized HTTP Server - """ - port = port or Configuration().get("gui", {}).get("file_server_port", 8089) - - if os.path.exists(qml_path): - shutil.rmtree(qml_path, ignore_errors=True) - os.makedirs(qml_path, exist_ok=True) - - started_event = Event() - http_daemon = Thread(target=_initialize_http_server, - args=(started_event, qml_path, port), - daemon=True) - http_daemon.start() - started_event.wait(30) - return _HTTP_SERVER - - -def _initialize_http_server(started: Event, directory: str, port: int): - global _HTTP_SERVER - os.chdir(directory) - handler = GuiFileHandler - http_server = socketserver.TCPServer(("", port), handler) - _HTTP_SERVER = http_server - _HTTP_SERVER.qml_path = directory - _HTTP_SERVER.url = \ - f"{_HTTP_SERVER.server_address[0]}:{_HTTP_SERVER.server_address[1]}" - LOG.info(f"GUI file server started: {_HTTP_SERVER.url}") - started.set() - http_server.serve_forever() diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 1725eb4..15cf3bd 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -54,7 +54,6 @@ get_gui_websocket_config, send_message_to_gui, GUIWebsocketHandler ) -from ovos_gui.gui_file_server import start_gui_http_server from ovos_gui.page import GuiPage from ovos_gui.constants import GUI_CACHE_PATH namespace_lock = Lock() @@ -431,7 +430,6 @@ def __init__(self, core_bus: MessageBusClient): self.active_extension = _get_active_gui_extension() self._system_res_dir = join(dirname(__file__), "res", "gui") self._ready_event = Event() - self.gui_file_server = None self._init_gui_file_share() self._define_message_handlers() @@ -443,12 +441,8 @@ def _init_gui_file_share(self): """ Initialize optional GUI file collection. if `gui_file_path` is defined, resources are assumed to be referenced outside this container. - If `gui_file_server` is defined, resources will be served via HTTP """ config = Configuration().get("gui", {}) - # Check for GUI file sharing via HTTP server - if config.get("gui_file_server"): - self.gui_file_server = start_gui_http_server(GUI_CACHE_PATH) self._cache_system_resources() def _define_message_handlers(self): From 61a9ec27fa81339acc916f2e7aa1785c8ab17143 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 19:56:42 +0100 Subject: [PATCH 03/13] docs --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 73be4bd..cc560de 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,6 @@ GUI messagebus service, manages GUI state and implements the [gui protocol](./pr GUI clients (the application that actually draws the GUI) connect to this service -# Plugins - -plugins provide platform specific GUI functionality, such as determining when to show a homescreen or close a window - -you should usually not need any of these unless instructed to install it from a GUI client application - -- https://github.com/OpenVoiceOS/ovos-gui-plugin-shell-companion -- https://github.com/OpenVoiceOS/ovos-gui-plugin-mobile -- https://github.com/OpenVoiceOS/ovos-gui-plugin-plasmoid -- https://github.com/OpenVoiceOS/ovos-gui-plugin-bigscreen - # Configuration under mycroft.conf @@ -50,3 +39,26 @@ under mycroft.conf } } ``` + +# Plugins + +plugins provide platform specific GUI functionality, such as determining when to show a homescreen or close a window + +you should usually not need any of these unless instructed to install it from a GUI client application + +- https://github.com/OpenVoiceOS/ovos-gui-plugin-shell-companion +- https://github.com/OpenVoiceOS/ovos-gui-plugin-mobile +- https://github.com/OpenVoiceOS/ovos-gui-plugin-plasmoid +- https://github.com/OpenVoiceOS/ovos-gui-plugin-bigscreen + + +# Limitations + +gui resources files are populated under `~/.cache/mycrot/ovos-gui` by skills and other OVOS components and are expectd to be accessible by GUI client applications + +This means GUI clients are expected to be running under the same machine or implement their own access to the resource files (resolving page names to uris is the client app responsibility) + +> TODO: new repository with the removed GUI file server, serve files from `~/.cache/mycrot/ovos-gui` to be handled by client apps + +In case of containers a shared volume should be mounted between ovos-gui, skills and gui client apps + From 822635e0d6e774497a18e6138fb037eba11c5f5d Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 20:25:31 +0100 Subject: [PATCH 04/13] . --- ovos_gui/bus.py | 23 +++++++---------------- ovos_gui/page.py | 9 +++++---- test/unittests/test_bus.py | 25 +++++++++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/ovos_gui/bus.py b/ovos_gui/bus.py index de38995..3d5b78b 100644 --- a/ovos_gui/bus.py +++ b/ovos_gui/bus.py @@ -124,18 +124,6 @@ def on_close(self): LOG.info('Closing {}'.format(id(self))) GUIWebsocketHandler.clients.remove(self) - def get_client_pages(self, namespace): - """ - Get a list of client page URLs for the given namespace - @param namespace: Namespace to get pages for - @return: list of page URIs for this GUI Client - """ - client_pages = [] - for page in namespace.pages: - uri = page.get_uri(self.framework) - client_pages.append(uri) - return client_pages - def synchronize(self): """ Upload namespaces, pages and data to the last connected client. @@ -151,11 +139,13 @@ def synchronize(self): "data": [{"skill_id": namespace.skill_id}] }) # Insert pages + # if uri (path) can not be resolved, it might exist client side + # if path doesn't exist in client side, client is responsible for resolving page by namespace/name self.send({"type": "mycroft.gui.list.insert", "namespace": namespace.skill_id, "position": 0, - "data": [{"url": url} for url in - self.get_client_pages(namespace)] + "data": [{"url": page.get_uri(self.framework), "page": page.name} + for page in namespace.pages] }) # Insert data for key, value in namespace.data.items(): @@ -257,12 +247,13 @@ def send_gui_pages(self, pages: List[GuiPage], namespace: str, @param position: position to insert pages at """ framework = self.framework - + # if uri (path) can not be resolved, it might exist client side + # if path doesn't exist in client side, client is responsible for resolving page by namespace/name message = { "type": "mycroft.gui.list.insert", "namespace": namespace, "position": position, - "data": [{"url": page.get_uri(framework)} + "data": [{"url": page.get_uri(framework), "page": page.name} for page in pages] } LOG.debug(f"Showing pages: {message['data']}") diff --git a/ovos_gui/page.py b/ovos_gui/page.py index 5085aec..a9e940a 100644 --- a/ovos_gui/page.py +++ b/ovos_gui/page.py @@ -37,7 +37,7 @@ def get_file_extension(framework: str) -> str: def res_namespace(self): return "system" if self.name.startswith("SYSTEM") else self.namespace - def get_uri(self, framework: str = "qt5") -> str: + def get_uri(self, framework: str = "qt5") -> Optional[str]: """ Get a valid URI for this Page. @param framework: String GUI framework to get resources for (currently only 'qt5') @@ -48,6 +48,7 @@ def get_uri(self, framework: str = "qt5") -> str: LOG.debug(f"Resolved page URI: {path}") if isfile(path): return path - raise FileNotFoundError(f"Unable to resolve resource file for " - f"resource {res_filename} for framework " - f"{framework}") + LOG.warning(f"Unable to resolve resource file for " + f"resource {res_filename} for framework " + f"{framework}") + return None diff --git a/test/unittests/test_bus.py b/test/unittests/test_bus.py index c6ecd41..9b9df37 100644 --- a/test/unittests/test_bus.py +++ b/test/unittests/test_bus.py @@ -80,6 +80,19 @@ def test_on_close(self): # TODO pass + def _get_client_pages(self, namespace) -> List[str]: + """ + Get a list of client page URLs for the given namespace + @param namespace: Namespace to get pages for + @return: list of page URIs for this GUI Client + """ + client_pages = [] + for page in namespace.pages: + # NOTE: in here page is resolved to a full URI (path) + uri = page.get_uri("qt5") + client_pages.append(uri) + return client_pages + def test_get_client_pages(self): from ovos_gui.namespace import Namespace test_namespace = Namespace("test") @@ -94,7 +107,7 @@ def test_get_client_pages(self): # Test no server_url self.handler.ns_manager.gui_file_server = None - pages = self.handler.get_client_pages(test_namespace) + pages = self._get_client_pages(test_namespace) page_1.get_uri.assert_called_once_with(self.handler.framework, None) page_2.get_uri.assert_called_once_with(self.handler.framework, None) self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) @@ -102,19 +115,11 @@ def test_get_client_pages(self): # Test host path mapping test_path = "/test/ovos-gui-file-server" self.handler.ns_manager.gui_file_host_path = test_path - pages = self.handler.get_client_pages(test_namespace) + pages = self._get_client_pages(test_namespace) page_1.get_uri.assert_called_with(self.handler.framework, test_path) page_2.get_uri.assert_called_with(self.handler.framework, test_path) self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) - # Test with server_url - self.handler.ns_manager.gui_file_server = Mock() - self.handler.ns_manager.gui_file_server.url = "server_url" - pages = self.handler.get_client_pages(test_namespace) - page_1.get_uri.assert_called_with(self.handler.framework, "server_url") - page_2.get_uri.assert_called_with(self.handler.framework, "server_url") - self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) - def test_synchronize(self): # TODO pass From 4a0702babdeefcd4d2b692c41747a0df86355f02 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 20:47:47 +0100 Subject: [PATCH 05/13] document protocol --- protocol.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/protocol.md b/protocol.md index eae7a00..919c087 100644 --- a/protocol.md +++ b/protocol.md @@ -61,13 +61,17 @@ Each active skill is associated with a list of uris to the QML files of all gui Non QT GUIS get sent other file extensions such as .jsx or .html using the same message format +If a gui resource can not be resolved to a url (*url* may be `None`!), it might still exist client side, it is the clients responsibility to handle the namespace/page in that case + +> eg, a client could map namespaces/page to a remote http server url + ## Insert new page at position ```javascript { "type": "mycroft.gui.list.insert", "namespace": "mycroft.weather" "position": 2 - "values": [{"url": "file://..../currentWeather.qml"}, ...] //values must always be in array form + "values": [{"url": "file://..../currentWeather.qml", "page": "currentWeather"}, ...] //values must always be in array form } ``` From 3105ddde6a62eda8bb9afaae39b4931a094ead12 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 15 Oct 2024 21:10:30 +0100 Subject: [PATCH 06/13] tests --- test/unittests/test_bus.py | 49 ++++---------- test/unittests/test_gui_file_server.py | 15 ----- test/unittests/test_namespace.py | 72 +++++++------------- test/unittests/test_page.py | 91 -------------------------- 4 files changed, 34 insertions(+), 193 deletions(-) delete mode 100644 test/unittests/test_gui_file_server.py delete mode 100644 test/unittests/test_page.py diff --git a/test/unittests/test_bus.py b/test/unittests/test_bus.py index 9b9df37..d8469e7 100644 --- a/test/unittests/test_bus.py +++ b/test/unittests/test_bus.py @@ -1,6 +1,6 @@ import unittest from unittest.mock import patch, Mock - +from typing import List import ovos_gui.bus @@ -102,23 +102,11 @@ def test_get_client_pages(self): page_2.get_uri.return_value = "page_2_uri" test_namespace.pages = [page_1, page_2] - # Specify no host path mapping - self.handler.ns_manager.gui_file_host_path = None - - # Test no server_url - self.handler.ns_manager.gui_file_server = None pages = self._get_client_pages(test_namespace) - page_1.get_uri.assert_called_once_with(self.handler.framework, None) - page_2.get_uri.assert_called_once_with(self.handler.framework, None) + page_1.get_uri.assert_called_once_with(self.handler.framework) + page_2.get_uri.assert_called_once_with(self.handler.framework) self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) - # Test host path mapping - test_path = "/test/ovos-gui-file-server" - self.handler.ns_manager.gui_file_host_path = test_path - pages = self._get_client_pages(test_namespace) - page_1.get_uri.assert_called_with(self.handler.framework, test_path) - page_2.get_uri.assert_called_with(self.handler.framework, test_path) - self.assertEqual(pages, ["page_1_uri", "page_2_uri"]) def test_synchronize(self): # TODO @@ -139,47 +127,32 @@ def test_send_gui_pages(self): test_pos = 0 from ovos_gui.page import GuiPage - page_1 = GuiPage(None, "", False, False) + page_1 = GuiPage("p1", "", False, False) page_1.get_uri = Mock(return_value="page_1") - page_2 = GuiPage(None, "", False, False) + page_2 = GuiPage("p2", "", False, False) page_2.get_uri = Mock(return_value="page_2") - # Specify no host path mapping - self.handler.ns_manager.gui_file_host_path = None - - # Test no server_url - self.handler.ns_manager.gui_file_server = None self.handler._framework = "qt5" self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos) - page_1.get_uri.assert_called_once_with("qt5", None) - page_2.get_uri.assert_called_once_with("qt5", None) + page_1.get_uri.assert_called_once_with("qt5") + page_2.get_uri.assert_called_once_with("qt5") self.handler.send.assert_called_once_with( {"type": "mycroft.gui.list.insert", "namespace": test_ns, "position": test_pos, - "data": [{"url": "page_1"}, {"url": "page_2"}]}) - - # Test host path mapping - test_path = "/test/ovos-gui-file-server" - self.handler.ns_manager.gui_file_host_path = test_path - self.handler.send_gui_pages([page_1, page_2], test_ns, test_pos) - page_1.get_uri.assert_called_with(self.handler.framework, test_path) - page_2.get_uri.assert_called_with(self.handler.framework, test_path) + "data": [{"url": "page_1", "page": "p1"}, {"url": "page_2", "page": "p2"}]}) - # Test with server_url - self.handler.ns_manager.gui_file_server = Mock() - self.handler.ns_manager.gui_file_server.url = "server_url" self.handler._framework = "qt6" test_pos = 3 self.handler.send_gui_pages([page_2, page_1], test_ns, test_pos) - page_1.get_uri.assert_called_with("qt6", "server_url") - page_2.get_uri.assert_called_with("qt6", "server_url") + page_1.get_uri.assert_called_with("qt6") + page_2.get_uri.assert_called_with("qt6") self.handler.send.assert_called_with( {"type": "mycroft.gui.list.insert", "namespace": test_ns, "position": test_pos, - "data": [{"url": "page_2"}, {"url": "page_1"}]}) + "data": [{"url": "page_2", "page": "p2"}, {"url": "page_1", "page": "p1"}]}) self.handler.send = real_send diff --git a/test/unittests/test_gui_file_server.py b/test/unittests/test_gui_file_server.py deleted file mode 100644 index bfaf81b..0000000 --- a/test/unittests/test_gui_file_server.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest - - -class TestGuiFileServer(unittest.TestCase): - def test_gui_file_handler(self): - from ovos_gui.gui_file_server import GuiFileHandler - # TODO - - def test_start_gui_http_server(self): - from ovos_gui.gui_file_server import start_gui_http_server - # TODO - - def test_initialize_http_server(self): - from ovos_gui.gui_file_server import _initialize_http_server - # TODO diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index 0f5b97c..fb47411 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -22,6 +22,7 @@ from ovos_bus_client.message import Message from ovos_utils.messagebus import FakeBus +from ovos_gui.constants import GUI_CACHE_PATH from ovos_gui.namespace import Namespace from ovos_gui.page import GuiPage @@ -64,11 +65,11 @@ def test_add(self): def test_activate(self): self.namespace.load_pages([ - GuiPage(name="foo", url="", persistent=False, duration=False), - GuiPage(name="bar", url="", persistent=False, duration=False), - GuiPage(name="foobar", url="", persistent=False, duration=False), - GuiPage(name="baz", url="", persistent=False, duration=False), - GuiPage(name="foobaz", url="", persistent=False, duration=False) + GuiPage(name="foo", persistent=False, duration=False), + GuiPage(name="bar",persistent=False, duration=False), + GuiPage(name="foobar", persistent=False, duration=False), + GuiPage(name="baz", persistent=False, duration=False), + GuiPage(name="foobaz", persistent=False, duration=False) ]) activate_namespace_message = { "type": "mycroft.session.list.move", @@ -129,9 +130,9 @@ def test_set_persistence_boolean(self): self.assertTrue(self.namespace.persistent) def test_load_pages_new(self): - self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0), - GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)] - new_pages = [GuiPage(name="foobar", url="foobar.qml", persistent=False, duration=30)] + self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0), + GuiPage(name="bar", persistent=False, duration=30)] + new_pages = [GuiPage(name="foobar", persistent=False, duration=30)] load_page_message = dict( type="mycroft.events.triggered", namespace="foo", @@ -146,9 +147,9 @@ def test_load_pages_new(self): self.assertListEqual(self.namespace.pages, self.namespace.pages) def test_load_pages_existing(self): - self.namespace.pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0), - GuiPage(name="bar", url="bar.qml", persistent=False, duration=30)] - new_pages = [GuiPage(name="foo", url="foo.qml", persistent=True, duration=0)] + self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0), + GuiPage(name="bar", persistent=False, duration=30)] + new_pages = [GuiPage(name="foo", persistent=True, duration=0)] load_page_message = dict( type="mycroft.events.triggered", namespace="foo", @@ -171,9 +172,9 @@ def test_activate_page(self): pass def test_remove_pages(self): - self.namespace.pages = [GuiPage(name="foo", url="", persistent=False, duration=False), - GuiPage(name="bar", url="", persistent=False, duration=False), - GuiPage(name="foobar", url="", persistent=False, duration=False)] + self.namespace.pages = [GuiPage(name="foo", persistent=False, duration=False), + GuiPage(name="bar",persistent=False, duration=False), + GuiPage(name="foobar",persistent=False, duration=False)] remove_page_message = dict( type="mycroft.gui.list.remove", namespace="foo", @@ -217,18 +218,6 @@ def setUp(self): with mock.patch(PATCH_MODULE + ".create_gui_service"): self.namespace_manager = NamespaceManager(FakeBus()) - def test_init_gui_file_share(self): - # TODO - pass - - def test_handle_gui_pages_available(self): - # TODO - pass - - def test_handle_receive_gui_pages(self): - # TODO - pass - def test_handle_clear_namespace_active(self): namespace = Namespace("foo") namespace.remove = mock.Mock() @@ -264,7 +253,7 @@ def test_handle_send_event(self): def test_handle_delete_page_active_namespace(self): namespace = Namespace("foo") - namespace.pages = [GuiPage(name="bar", url="bar.qml", persistent=True, duration=0)] + namespace.pages = [GuiPage(name="bar", persistent=True, duration=0)] namespace.remove_pages = mock.Mock() self.namespace_manager.loaded_namespaces = dict(foo=namespace) self.namespace_manager.active_namespaces = [namespace] @@ -302,20 +291,10 @@ def test_parse_persistence(self): with self.assertRaises(ValueError): self.namespace_manager._parse_persistence(-10) - def test_legacy_show_page(self): - message = Message("gui.page.show", data={"__from": "foo", - "__idle": 10, - "page": ["bar", "test/baz"]}) - pages = self.namespace_manager._legacy_show_page(message) - self.assertEqual(pages, [GuiPage('bar', 'bar', False, 10), - GuiPage('test/baz', 'baz', False, 10)]) - def test_handle_show_page(self): - real_legacy_show_page = self.namespace_manager._legacy_show_page real_activate_namespace = self.namespace_manager._activate_namespace real_load_pages = self.namespace_manager._load_pages real_update_persistence = self.namespace_manager._update_namespace_persistence - self.namespace_manager._legacy_show_page = Mock(return_value=["pages"]) self.namespace_manager._activate_namespace = Mock() self.namespace_manager._load_pages = Mock() self.namespace_manager._update_namespace_persistence = Mock() @@ -325,7 +304,6 @@ def test_handle_show_page(self): "__idle": 10, "page": ["bar", "test/baz"]}) self.namespace_manager.handle_show_page(message) - self.namespace_manager._legacy_show_page.assert_called_once_with(message) self.namespace_manager._activate_namespace.assert_called_with("foo") self.namespace_manager._load_pages.assert_called_with(["pages"], None) self.namespace_manager._update_namespace_persistence. \ @@ -344,7 +322,6 @@ def test_handle_show_page(self): ui_directories) expected_page2 = GuiPage(None, "test/page_2", False, 0, "test/page_2", "skill", ui_directories) - self.namespace_manager._legacy_show_page.assert_called_once() self.namespace_manager._activate_namespace.assert_called_with("skill") self.namespace_manager._load_pages.assert_called_with([expected_page1, expected_page2], @@ -362,7 +339,6 @@ def test_handle_show_page(self): expected_page = GuiPage(None, "SYSTEM_TextFrame", True, 0, "SYSTEM_TextFrame", "skill_no_res", {"all": self.namespace_manager._system_res_dir}) - self.namespace_manager._legacy_show_page.assert_called_once() self.namespace_manager._activate_namespace.assert_called_with( "skill_no_res") self.namespace_manager._load_pages.assert_called_with([expected_page], @@ -371,7 +347,6 @@ def test_handle_show_page(self): assert_called_with(True) # TODO: Test page_names with files and URIs - self.namespace_manager._legacy_show_page = real_legacy_show_page self.namespace_manager._activate_namespace = real_activate_namespace self.namespace_manager._load_pages = real_load_pages self.namespace_manager._update_namespace_persistence = \ @@ -455,16 +430,15 @@ def test_del_namespace_in_remove_timers(self): pass def test_upload_system_resources(self): - test_dir = join(dirname(__file__), "upload_test") - makedirs(test_dir, exist_ok=True) - self.namespace_manager.gui_file_path = test_dir + p = f"{GUI_CACHE_PATH}/system" + rmtree(p) self.namespace_manager._cache_system_resources() - self.assertTrue(isdir(join(test_dir, "system", "qt5"))) - self.assertTrue(isfile(join(test_dir, "system", "qt5", + self.assertTrue(isdir(join(p, "qt5"))) + self.assertTrue(isfile(join(p, "qt5", "SYSTEM_TextFrame.qml"))) # Test repeated copy doesn't raise any exception self.namespace_manager._cache_system_resources() - self.assertTrue(isdir(join(test_dir, "system", "qt5"))) - self.assertTrue(isfile(join(test_dir, "system", "qt5", + self.assertTrue(isdir(join(p, "qt5"))) + self.assertTrue(isfile(join(p, "qt5", "SYSTEM_TextFrame.qml"))) - rmtree(test_dir) + rmtree(p) diff --git a/test/unittests/test_page.py b/test/unittests/test_page.py deleted file mode 100644 index fe3abec..0000000 --- a/test/unittests/test_page.py +++ /dev/null @@ -1,91 +0,0 @@ -import unittest -from os.path import join, dirname, isfile -from ovos_gui.page import GuiPage - - -class TestGuiPage(unittest.TestCase): - def test_gui_page_legacy(self): - uri = __file__ - name = "test" - persistent = True - duration = 0 - page = GuiPage(uri, name, persistent, duration) - self.assertEqual(page.url, uri) - self.assertEqual(page.name, name) - self.assertEqual(page.persistent, persistent) - self.assertEqual(page.duration, 0) - self.assertEqual(page.id, page.url) - self.assertEqual(page.get_uri(), page.url) - self.assertEqual(page.get_uri("qt6", "http://0.0.0.0:80"), page.url) - self.assertEqual(page.get_uri("qt6", "/var/www/app"), page.url) - - def test_gui_page_from_server(self): - name = "test_page" - persistent = False - duration = 60 - page_id = "test_page" - namespace = "skill.test" - - page = GuiPage(None, name, persistent, duration, page_id, namespace) - qt5 = page.get_uri(server_url="localhost:80") - self.assertEqual(qt5, - f"http://localhost:80/{namespace}/qt5/{page_id}.qml") - - qt6 = page.get_uri(server_url="https://files.local") - self.assertEqual(qt6, - f"https://files.local/{namespace}/qt5/{page_id}.qml") - - def test_gui_page_from_mapped_path(self): - name = "test_page" - persistent = False - duration = 60 - page_id = "test_page" - namespace = "skill.test" - - page = GuiPage(None, name, persistent, duration, page_id, namespace) - qt5 = page.get_uri(server_url="/path/for/gui/client") - self.assertEqual(qt5, - f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml") - - qt6 = page.get_uri(server_url="/path/for/gui/client") - self.assertEqual(qt6, - f"file:///path/for/gui/client/{namespace}/qt5/{page_id}.qml") - - def test_gui_page_from_local_path(self): - name = "test" - persistent = True - duration = True - page_id = "test" - namespace = "skill.test" - res_dirs = {"all": join(dirname(__file__), "mock_data", "gui")} - # Modern GUI File organization - page = GuiPage(None, name, persistent, duration, page_id, namespace, - res_dirs) - qt5 = page.get_uri("qt5") - qt6 = page.get_uri("qt6") - self.assertTrue(isfile(qt5)) - self.assertTrue(isfile(qt6)) - - qt6_only_name = "six" - qt6_page = GuiPage(None, qt6_only_name, persistent, duration, - qt6_only_name, namespace, res_dirs) - with self.assertRaises(FileNotFoundError): - qt6_page.get_uri("qt5") - qt6 = qt6_page.get_uri("qt6") - self.assertTrue(isfile(qt6)) - - # System page - system_page = GuiPage(None, "SYSTEM_ImageFrame", False, 30, - "SYSTEM_ImageFrame", namespace, res_dirs) - qt5 = system_page.get_uri("qt5") - self.assertTrue(isfile(qt5)) - - # Legacy GUI File organization - res_dirs = {"qt5": join(dirname(__file__), "mock_data", "gui", "qt5"), - "qt6": join(dirname(__file__), "mock_data", "gui", "qt6")} - page = GuiPage(None, name, persistent, duration, page_id, namespace, - res_dirs) - qt5 = page.get_uri("qt5") - qt6 = page.get_uri("qt6") - self.assertTrue(isfile(qt5)) - self.assertTrue(isfile(qt6)) From 19a89077029be0152e3115df7aa5ee6d3a953774 Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 21 Oct 2024 15:18:26 +0100 Subject: [PATCH 07/13] fix:phal config loading --- ovos_gui/namespace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 15cf3bd..0243643 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -460,7 +460,6 @@ def _define_message_handlers(self): self.core_bus.on("gui.page_interaction", self.handle_page_interaction) self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus) self.core_bus.on("mycroft.gui.screen.close", self.handle_namespace_global_back) - self.core_bus.on("gui.volunteer_page_upload", self.handle_gui_pages_available) # TODO - deprecate this, only needed for gui bus upload # Bus is connected, check if the skills service is ready From 145d3601a2bacb71a7aed32309514fca1bd7f10b Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 21 Oct 2024 15:50:33 +0100 Subject: [PATCH 08/13] tests --- test/unittests/test_namespace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index fb47411..15d5a97 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -258,7 +258,7 @@ def test_handle_delete_page_active_namespace(self): self.namespace_manager.loaded_namespaces = dict(foo=namespace) self.namespace_manager.active_namespaces = [namespace] - message_data = {"__from": "foo", "page": ["bar"]} + message_data = {"__from": "foo", "page_names": ["bar"]} message = Message("gui.clear.namespace", data=message_data) self.namespace_manager.handle_delete_page(message) namespace.remove_pages.assert_called_with([0]) @@ -302,7 +302,7 @@ def test_handle_show_page(self): # Legacy message message = Message("gui.page.show", data={"__from": "foo", "__idle": 10, - "page": ["bar", "test/baz"]}) + "page_names": ["bar", "test/baz"]}) self.namespace_manager.handle_show_page(message) self.namespace_manager._activate_namespace.assert_called_with("foo") self.namespace_manager._load_pages.assert_called_with(["pages"], None) From 0150d748885623b768bab0456a136de29adedefc Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 21 Oct 2024 15:58:02 +0100 Subject: [PATCH 09/13] tests --- ovos_gui/page.py | 1 - test/unittests/test_namespace.py | 36 ++++++++++++++------------------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/ovos_gui/page.py b/ovos_gui/page.py index a9e940a..3420450 100644 --- a/ovos_gui/page.py +++ b/ovos_gui/page.py @@ -20,7 +20,6 @@ class GuiPage: persistent: bool duration: Union[int, bool] namespace: Optional[str] = None - resource_dirs: Optional[dict] = None @staticmethod def get_file_extension(framework: str) -> str: diff --git a/test/unittests/test_namespace.py b/test/unittests/test_namespace.py index 15d5a97..e340fef 100644 --- a/test/unittests/test_namespace.py +++ b/test/unittests/test_namespace.py @@ -13,8 +13,7 @@ # limitations under the License. # """Tests for the GUI namespace helper class.""" -from os import makedirs -from os.path import join, dirname, isdir, isfile +from os.path import join, isdir, isfile from shutil import rmtree from unittest import TestCase, mock from unittest.mock import Mock @@ -65,11 +64,11 @@ def test_add(self): def test_activate(self): self.namespace.load_pages([ - GuiPage(name="foo", persistent=False, duration=False), - GuiPage(name="bar",persistent=False, duration=False), - GuiPage(name="foobar", persistent=False, duration=False), - GuiPage(name="baz", persistent=False, duration=False), - GuiPage(name="foobaz", persistent=False, duration=False) + GuiPage(name="foo", persistent=False, duration=False), + GuiPage(name="bar", persistent=False, duration=False), + GuiPage(name="foobar", persistent=False, duration=False), + GuiPage(name="baz", persistent=False, duration=False), + GuiPage(name="foobaz", persistent=False, duration=False) ]) activate_namespace_message = { "type": "mycroft.session.list.move", @@ -130,9 +129,9 @@ def test_set_persistence_boolean(self): self.assertTrue(self.namespace.persistent) def test_load_pages_new(self): - self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0), + self.namespace.pages = [GuiPage(name="foo", persistent=True, duration=0), GuiPage(name="bar", persistent=False, duration=30)] - new_pages = [GuiPage(name="foobar", persistent=False, duration=30)] + new_pages = [GuiPage(name="foobar", persistent=False, duration=30)] load_page_message = dict( type="mycroft.events.triggered", namespace="foo", @@ -173,8 +172,8 @@ def test_activate_page(self): def test_remove_pages(self): self.namespace.pages = [GuiPage(name="foo", persistent=False, duration=False), - GuiPage(name="bar",persistent=False, duration=False), - GuiPage(name="foobar",persistent=False, duration=False)] + GuiPage(name="bar", persistent=False, duration=False), + GuiPage(name="foobar", persistent=False, duration=False)] remove_page_message = dict( type="mycroft.gui.list.remove", namespace="foo", @@ -305,7 +304,9 @@ def test_handle_show_page(self): "page_names": ["bar", "test/baz"]}) self.namespace_manager.handle_show_page(message) self.namespace_manager._activate_namespace.assert_called_with("foo") - self.namespace_manager._load_pages.assert_called_with(["pages"], None) + self.namespace_manager._load_pages.assert_called_with( + [GuiPage(name='bar', persistent=False, duration=10, namespace='foo'), + GuiPage(name='test/baz', persistent=False, duration=10, namespace='foo')], 0) self.namespace_manager._update_namespace_persistence. \ assert_called_with(10) @@ -314,14 +315,11 @@ def test_handle_show_page(self): message = Message("test", {"__from": "skill", "__idle": False, "index": 1, - "page": ["/gui/page_1", "/gui/test/page_2"], "page_names": ["page_1", "test/page_2"], "ui_directories": ui_directories}) self.namespace_manager.handle_show_page(message) - expected_page1 = GuiPage(None, "page_1", False, 0, "page_1", "skill", - ui_directories) - expected_page2 = GuiPage(None, "test/page_2", False, 0, "test/page_2", - "skill", ui_directories) + expected_page1 = GuiPage("page_1", False, 0, "skill") + expected_page2 = GuiPage("test/page_2", False, 0, "skill") self.namespace_manager._activate_namespace.assert_called_with("skill") self.namespace_manager._load_pages.assert_called_with([expected_page1, expected_page2], @@ -336,9 +334,7 @@ def test_handle_show_page(self): "page": ["/gui/SYSTEM_TextFrame.qml"], "page_names": ["SYSTEM_TextFrame"]}) self.namespace_manager.handle_show_page(message) - expected_page = GuiPage(None, "SYSTEM_TextFrame", True, 0, - "SYSTEM_TextFrame", "skill_no_res", - {"all": self.namespace_manager._system_res_dir}) + expected_page = GuiPage("SYSTEM_TextFrame", True, 0, "skill_no_res") self.namespace_manager._activate_namespace.assert_called_with( "skill_no_res") self.namespace_manager._load_pages.assert_called_with([expected_page], From 9cf3144289f7b49ae009a892fc979b7eab805d6c Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 4 Nov 2024 17:23:34 +0000 Subject: [PATCH 10/13] rm dead code + handle homescreen edge case --- ovos_gui/namespace.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/ovos_gui/namespace.py b/ovos_gui/namespace.py index 0243643..5113aa8 100644 --- a/ovos_gui/namespace.py +++ b/ovos_gui/namespace.py @@ -429,14 +429,9 @@ def __init__(self, core_bus: MessageBusClient): self.idle_display_skill = _get_idle_display_config() self.active_extension = _get_active_gui_extension() self._system_res_dir = join(dirname(__file__), "res", "gui") - self._ready_event = Event() self._init_gui_file_share() self._define_message_handlers() - @property - def _active_homescreen(self) -> str: - return Configuration().get('gui', {}).get('idle_display_skill') - def _init_gui_file_share(self): """ Initialize optional GUI file collection. if `gui_file_path` is @@ -461,20 +456,6 @@ def _define_message_handlers(self): self.core_bus.on("gui.page_gained_focus", self.handle_page_gained_focus) self.core_bus.on("mycroft.gui.screen.close", self.handle_namespace_global_back) - # TODO - deprecate this, only needed for gui bus upload - # Bus is connected, check if the skills service is ready - resp = self.core_bus.wait_for_response( - Message("mycroft.skills.is_ready", - context={"source": "gui", "destination": ["skills"]})) - if resp and resp.data.get("status"): - LOG.debug("Skills service already running") - self._ready_event.set() - else: - self.core_bus.on("mycroft.skills.trained", self.handle_ready) - - def handle_ready(self, message): - self._ready_event.set() - def handle_clear_namespace(self, message: Message): """ Handles a request to remove a namespace. @@ -890,7 +871,8 @@ def handle_namespace_global_back(self, message: Optional[Message]): @param message: the event sent by the GUI """ if not self.active_namespaces: - LOG.error("received 'back' signal but there are no active namespaces") + LOG.debug("received 'back' signal but there are no active namespaces, attempting to show homescreen") + self.core_bus.emit(Message("homescreen.manager.show_active")) return namespace_name = self.active_namespaces[0].skill_id From be7767688c87d29af17b6dbbd72056b18e51d65f Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 5 Nov 2024 00:51:12 +0000 Subject: [PATCH 11/13] fix: handle invalid GUI directories provided --- .../res/gui/qt5/SYSTEM_AdditionalSettings.qml | 60 ------------------- requirements/requirements.txt | 2 +- 2 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 ovos_gui/res/gui/qt5/SYSTEM_AdditionalSettings.qml diff --git a/ovos_gui/res/gui/qt5/SYSTEM_AdditionalSettings.qml b/ovos_gui/res/gui/qt5/SYSTEM_AdditionalSettings.qml deleted file mode 100644 index 9f55318..0000000 --- a/ovos_gui/res/gui/qt5/SYSTEM_AdditionalSettings.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick.Layouts 1.4 -import QtQuick 2.4 -import QtQuick.Controls 2.0 -import org.kde.kirigami 2.5 as Kirigami -import Mycroft 1.0 as Mycroft -import QtGraphicalEffects 1.12 - -Mycroft.Delegate { - id: mainLoaderView - property var pageToLoad: sessionData.state - property var idleScreenList: sessionData.idleScreenList - property var activeIdle: sessionData.selectedScreen - property var imageUrl - - function getCurrentWallpaper() { - Mycroft.MycroftController.sendRequest("ovos.wallpaper.manager.get.wallpaper", {}) - } - - Component.onCompleted: { - getCurrentWallpaper() - } - - Connections { - target: Mycroft.MycroftController - onIntentRecevied: { - if (type == "ovos.wallpaper.manager.get.wallpaper.response") { - imageUrl = data.url - } - if (type == "homescreen.wallpaper.set") { - imageUrl = data.url - } - } - } - - background: Item { - Image { - id: bgModelImage - anchors.fill: parent - source: Qt.resolvedUrl(mainLoaderView.imageUrl) - fillMode: Image.PreserveAspectCrop - } - - Rectangle { - anchors.fill: parent - color: Kirigami.Theme.backgroundColor - opacity: 0.6 - z: 1 - } - } - - contentItem: Loader { - id: rootLoader - z: 2 - } - - onPageToLoadChanged: { - console.log(sessionData.state) - rootLoader.setSource(sessionData.state + ".qml") - } -} diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 185eda9..19b9444 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,4 +2,4 @@ ovos_bus_client>=0.0.7,<1.0.0 ovos-utils>=0.0.37,<1.0.0 ovos-config>=0.0.12,<1.0.0 tornado~=6.0, >=6.0.3 -ovos-plugin-manager>=0.0.24,<1.0.0 +ovos-plugin-manager>=0.5.5,<1.0.0 \ No newline at end of file From 2dfb9ba39c06575acfa76becccc610834dd23ba2 Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 5 Nov 2024 00:51:58 +0000 Subject: [PATCH 12/13] fix: shell companion min version --- requirements/extras.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/extras.txt b/requirements/extras.txt index 893dd18..34b7479 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -1 +1 @@ -ovos-gui-plugin-shell-companion>=1.0.0,<2.0.0 +ovos-gui-plugin-shell-companion>=1.0.1,<2.0.0 From 8e10c9fdbaa61a60cc1843f11d585a07e9ef109c Mon Sep 17 00:00:00 2001 From: miro Date: Tue, 5 Nov 2024 03:36:47 +0000 Subject: [PATCH 13/13] fix: requirements.txt --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 19b9444..0523f10 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos_bus_client>=0.0.7,<1.0.0 +ovos_bus_client>=1.0.0,<2.0.0 ovos-utils>=0.0.37,<1.0.0 ovos-config>=0.0.12,<1.0.0 tornado~=6.0, >=6.0.3