From dba95498608626ae5116d58925dc2aa2b882eb94 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 18 Sep 2024 21:56:39 +0100 Subject: [PATCH 1/9] refactor!:deprecate QML upload from bus never worked right, causes more issues than it helps --- ovos_bus_client/apis/gui.py | 132 ++---------------------------------- 1 file changed, 4 insertions(+), 128 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index 3286999..bcfe4b9 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -1,9 +1,6 @@ -from os import walk -from os.path import join, splitext, isfile from typing import List, Union, Optional, Callable -from ovos_utils.file_utils import resolve_ovos_resource_file, resolve_resource_file -from ovos_utils.log import LOG, log_deprecation +from ovos_utils.log import LOG from ovos_bus_client.util import get_mycroft_bus from ovos_utils.gui import can_use_gui from ovos_config import Configuration @@ -78,14 +75,13 @@ class GUIInterface: """ def __init__(self, skill_id: str, bus=None, - remote_server: str = None, config: dict = None, + config: dict = None, ui_directories: dict = None): """ Create an interface to the GUI module. Values set here are exposed to the GUI client as sessionData @param skill_id: ID of this interface @param bus: MessagebusClient object to connect to - @param remote_server: Optional URL of a remote GUI server @param config: dict gui Configuration @param ui_directories: dict framework to directory containing resources `all` key should reference a `gui` directory containing all @@ -93,8 +89,6 @@ def __init__(self, skill_id: str, bus=None, """ config = config or Configuration().get("gui", {}) self.config = config - if remote_server: - self.config["remote-server"] = remote_server self._bus = bus self.__session_data = {} # synced to GUI for use by this skill's pages self._pages = [] @@ -106,15 +100,6 @@ def __init__(self, skill_id: str, bus=None, if bus: self.set_bus(bus) - @property - def remote_url(self) -> Optional[str]: - """Returns configuration value for url of remote-server.""" - return self.config.get('remote-server') - - @remote_url.setter - def remote_url(self, val: str): - self.config["remote-server"] = val - def set_bus(self, bus=None): self._bus = bus or get_mycroft_bus() self.setup_default_handlers() @@ -183,55 +168,6 @@ def setup_default_handlers(self): msg_type = self.build_message_type('set') self.bus.on(msg_type, self.gui_set) self._events.append((msg_type, self.gui_set)) - self.bus.on("gui.request_page_upload", self.upload_gui_pages) - if self.ui_directories: - LOG.debug("Volunteering gui page upload") - self.bus.emit(Message("gui.volunteer_page_upload", - {'skill_id': self.skill_id}, - {'source': self.skill_id, "destination": ["gui"]})) - - def upload_gui_pages(self, message: Message): - """ - Emit a response Message with all known GUI files managed by - this interface for the requested infrastructure - @param message: `gui.request_page_upload` Message requesting pages - """ - if not self.ui_directories: - LOG.debug("No UI resources to upload") - return - - requested_skill = message.data.get("skill_id") or self._skill_id - if requested_skill != self._skill_id: - # GUI requesting a specific skill to upload other than this one - return - - request_res_type = message.data.get("framework") or "all" if "all" in \ - self.ui_directories else "qt5" - # Note that ui_directory "all" is a special case that will upload all - # gui files, including all framework subdirectories - if request_res_type not in self.ui_directories: - LOG.warning(f"Requested UI files not available: {request_res_type}") - return - LOG.debug(f"Requested upload resources for: {request_res_type}") - pages = dict() - # `pages` keys are unique identifiers in the scope of this interface; - # if ui_directory is "all", then pages are prefixed with `/` - res_dir = self.ui_directories[request_res_type] - for path, _, files in walk(res_dir): - for file in files: - try: - full_path: str = join(path, file) - page_name = full_path.replace(f"{res_dir}/", "", 1) - with open(full_path, 'rb') as f: - file_bytes = f.read() - pages[page_name] = file_bytes.hex() - except Exception as e: - LOG.exception(f"{file} not uploaded: {e}") - # Note that `pages` in this context include file extensions - self.bus.emit(message.forward("gui.page.upload", - {"__from": self.skill_id, - "framework": request_res_type, - "pages": pages})) def register_handler(self, event: str, handler: Callable): """ @@ -345,57 +281,6 @@ def send_event(self, event_name: str, "event_name": event_name, "params": params})) - def _pages2uri(self, page_names: List[str]) -> List[str]: - """ - Get a list of resolved URIs from a list of string page names. - @param page_names: List of GUI resource names (file basenames) to locate - @return: List of resolved paths to the requested pages - """ - # TODO: This method resolves absolute file paths. These will no longer - # be used with the implementation of `ovos-gui` - page_urls = [] - extra_dirs = list(self.ui_directories.values()) or list() - for name in page_names: - # Prefer plugin-specific resources first, then fallback to core - page = resolve_ovos_resource_file(name, extra_dirs) or \ - resolve_ovos_resource_file(join('ui', name), extra_dirs) or \ - resolve_resource_file(name, config=self.config) or \ - resolve_resource_file(join('ui', name), config=self.config) - - if page: - if self.remote_url: - page_urls.append(self.remote_url + "/" + page) - elif page.startswith("file://"): - page_urls.append(page) - else: - page_urls.append("file://" + page) - else: - # This is expected; with `ovos-gui`, pages are referenced by ID - # rather than filename in order to support multiple frameworks - LOG.debug(f"Requested page not resolved to a file: {page}") - LOG.debug(f"Resolved pages: {page_urls}") - return page_urls - - @staticmethod - def _normalize_page_name(page_name: str) -> str: - """ - Normalize a requested GUI resource - @param page_name: string name of a GUI resource - @return: normalized string name (`.qml` removed for other GUI support) - """ - if isfile(page_name): - log_deprecation("GUI resources should specify a resource name and " - "not a file path.", "0.1.0") - return page_name - file, ext = splitext(page_name) - if ext == ".qml": - log_deprecation("GUI resources should exclude gui-specific file " - f"extensions. This call should probably pass " - f"`{file}`, instead of `{page_name}`", "0.1.0") - return file - - return page_name - # base gui interactions def show_page(self, name: str, override_idle: Union[bool, int] = None, override_animations: bool = False, index: int = 0, @@ -432,10 +317,6 @@ def show_pages(self, page_names: List[str], index: int = 0, LOG.error('Default index is larger than page list length') index = len(page_names) - 1 - # TODO: deprecate sending page_urls after ovos_gui implementation - page_urls = self._pages2uri(page_names) - page_names = [self._normalize_page_name(n) for n in page_names] - if remove_others: self.remove_all_pages(except_pages=page_names) @@ -450,8 +331,7 @@ def show_pages(self, page_names: List[str], index: int = 0, # finally tell gui what to show self.bus.emit(Message("gui.page.show", - {"page": page_urls, - "page_names": page_names, + {"page_names": page_names, "ui_directories": self.ui_directories, "index": index, "__from": self.skill_id, @@ -476,12 +356,8 @@ def remove_pages(self, page_names: List[str]): page_names = [page_names] if not isinstance(page_names, list): raise ValueError('page_names must be a list') - # TODO: deprecate sending page_urls after ovos_gui implementation - page_urls = self._pages2uri(page_names) - page_names = [self._normalize_page_name(n) for n in page_names] self.bus.emit(Message("gui.page.delete", - {"page": page_urls, - "page_names": page_names, + {"page_names": page_names, "__from": self.skill_id})) def remove_all_pages(self, except_pages=None): From 42117ddf5f46a4a6877823a29fcc9eb53c084b84 Mon Sep 17 00:00:00 2001 From: miro Date: Wed, 18 Sep 2024 22:29:49 +0100 Subject: [PATCH 2/9] refactor!:deprecate QML upload from bus never worked right, causes more issues than it helps --- ovos_bus_client/apis/gui.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index bcfe4b9..ae265f7 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -75,17 +75,13 @@ class GUIInterface: """ def __init__(self, skill_id: str, bus=None, - config: dict = None, - ui_directories: dict = None): + config: dict = None): """ Create an interface to the GUI module. Values set here are exposed to the GUI client as sessionData @param skill_id: ID of this interface @param bus: MessagebusClient object to connect to @param config: dict gui Configuration - @param ui_directories: dict framework to directory containing resources - `all` key should reference a `gui` directory containing all - specific resource subdirectories """ config = config or Configuration().get("gui", {}) self.config = config @@ -96,7 +92,6 @@ def __init__(self, skill_id: str, bus=None, self._skill_id = skill_id self.on_gui_changed_callback = None self._events = [] - self.ui_directories = ui_directories or dict() if bus: self.set_bus(bus) @@ -332,7 +327,6 @@ def show_pages(self, page_names: List[str], index: int = 0, # finally tell gui what to show self.bus.emit(Message("gui.page.show", {"page_names": page_names, - "ui_directories": self.ui_directories, "index": index, "__from": self.skill_id, "__idle": override_idle, From c06d2b7146876c0ba0694e2cc4973cfbad594a60 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 20:23:25 +0100 Subject: [PATCH 3/9] add some backwards compat to help transition --- ovos_bus_client/apis/gui.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index ae265f7..0a97aa6 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -312,6 +312,12 @@ def show_pages(self, page_names: List[str], index: int = 0, LOG.error('Default index is larger than page list length') index = len(page_names) - 1 + # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already + if any(p.endswith(".qml") for p in page_names): + LOG.warning("received invalid page, please remove '.qml' extension from your code, " + "this has been deprecated in ovos-workshop and may stop working anytime") + page_names = [p.replace(".qml", "") for p in page_names] + if remove_others: self.remove_all_pages(except_pages=page_names) @@ -350,6 +356,11 @@ def remove_pages(self, page_names: List[str]): page_names = [page_names] if not isinstance(page_names, list): raise ValueError('page_names must be a list') + # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already + if any(p.endswith(".qml") for p in page_names): + LOG.warning("received invalid page, please remove '.qml' extension from your code, " + "this has been deprecated in ovos-workshop and may stop working anytime") + page_names = [p.replace(".qml", "") for p in page_names] self.bus.emit(Message("gui.page.delete", {"page_names": page_names, "__from": self.skill_id})) From 7897ee208b9844d5ca28669eeb07c572a6e7a240 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 20:39:15 +0100 Subject: [PATCH 4/9] restore ui_directories kwarg :vomit: --- ovos_bus_client/apis/gui.py | 42 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index 0a97aa6..f676115 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -1,10 +1,14 @@ +import os +import shutil from typing import List, Union, Optional, Callable -from ovos_utils.log import LOG -from ovos_bus_client.util import get_mycroft_bus -from ovos_utils.gui import can_use_gui from ovos_config import Configuration +from ovos_config.locations import get_xdg_cache_save_path +from ovos_utils.gui import can_use_gui +from ovos_utils.log import LOG + from ovos_bus_client.message import Message +from ovos_bus_client.util import get_mycroft_bus def extend_about_data(about_data: Union[list, dict], @@ -75,13 +79,15 @@ class GUIInterface: """ def __init__(self, skill_id: str, bus=None, - config: dict = None): + config: dict = None, + ui_directories: dict = None): """ Create an interface to the GUI module. Values set here are exposed to the GUI client as sessionData @param skill_id: ID of this interface @param bus: MessagebusClient object to connect to @param config: dict gui Configuration + @param ui_directories: dict framework to directory containing resources """ config = config or Configuration().get("gui", {}) self.config = config @@ -92,8 +98,32 @@ def __init__(self, skill_id: str, bus=None, self._skill_id = skill_id self.on_gui_changed_callback = None self._events = [] + self.ui_directories = ui_directories or dict() if bus: self.set_bus(bus) + self._cache_gui_files() + + def _cache_gui_files(self): + if not self.ui_directories: + LOG.debug(f"{self.skill_id} has no GUI resources") + return + + # this path is hardcoded in ovos_gui.constants and follows XDG spec + GUI_CACHE_PATH = get_xdg_cache_save_path('ovos_gui') + + output_path = f"{GUI_CACHE_PATH}/{self.skill_id}" + if os.path.exists(output_path): + LOG.info(f"Removing existing {self.skill_id} cached GUI resources before updating") + shutil.rmtree(output_path) + + for framework, bpath in self.ui_directories.items(): + if framework == "all": + LOG.warning("Assuming 'all' means 'qt5'!") + LOG.error(f"GUI resources moved to platform specific subfolder, " + f"please move contents from {bpath} to 'qt5' subfolder") + framework = "qt5" + shutil.copytree(bpath, f"{output_path}/{framework}") + LOG.debug(f"Copied {self.skill_id} resources from {bpath} to {output_path}/{framework}") def set_bus(self, bus=None): self._bus = bus or get_mycroft_bus() @@ -315,7 +345,7 @@ def show_pages(self, page_names: List[str], index: int = 0, # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already if any(p.endswith(".qml") for p in page_names): LOG.warning("received invalid page, please remove '.qml' extension from your code, " - "this has been deprecated in ovos-workshop and may stop working anytime") + "this has been deprecated in ovos-gui and may stop working anytime") page_names = [p.replace(".qml", "") for p in page_names] if remove_others: @@ -359,7 +389,7 @@ def remove_pages(self, page_names: List[str]): # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already if any(p.endswith(".qml") for p in page_names): LOG.warning("received invalid page, please remove '.qml' extension from your code, " - "this has been deprecated in ovos-workshop and may stop working anytime") + "this has been deprecated in ovos-gui and may stop working anytime") page_names = [p.replace(".qml", "") for p in page_names] self.bus.emit(Message("gui.page.delete", {"page_names": page_names, From 30d323fcdfaa5e39292a39dad42f36b519968cfb Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 20:53:02 +0100 Subject: [PATCH 5/9] 'all' --- ovos_bus_client/apis/gui.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index f676115..13a9e58 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -118,10 +118,8 @@ def _cache_gui_files(self): for framework, bpath in self.ui_directories.items(): if framework == "all": - LOG.warning("Assuming 'all' means 'qt5'!") - LOG.error(f"GUI resources moved to platform specific subfolder, " - f"please move contents from {bpath} to 'qt5' subfolder") - framework = "qt5" + LOG.warning(f"'all' is deprecated! ignoring path: {bpath}") + continue shutil.copytree(bpath, f"{output_path}/{framework}") LOG.debug(f"Copied {self.skill_id} resources from {bpath} to {output_path}/{framework}") From 53cf2bd8d946e4b7ee4aa26efa8935b8198699dc Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 22:17:37 +0100 Subject: [PATCH 6/9] better compat --- ovos_bus_client/apis/gui.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index 13a9e58..de2a3e8 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -1,5 +1,6 @@ import os import shutil +from os.path import splitext, isfile from typing import List, Union, Optional, Callable from ovos_config import Configuration @@ -304,10 +305,30 @@ def send_event(self, event_name: str, "event_name": event_name, "params": params})) + @staticmethod + def _normalize_page_name(page_name: str) -> str: + """ + Normalize a requested GUI resource + @param page_name: string name of a GUI resource + @return: normalized string name (`.qml` removed for other GUI support) + """ + if isfile(page_name): + LOG.error("GUI resources should specify a resource name and " + "not a file path.", "0.1.0") + return page_name + file, ext = splitext(page_name) + if ext == ".qml": + LOG.error("GUI resources should exclude gui-specific file " + f"extensions. This call should probably pass " + f"`{file}`, instead of `{page_name}`", "0.1.0") + return file + + return page_name + # base gui interactions def show_page(self, name: str, override_idle: Union[bool, int] = None, override_animations: bool = False, index: int = 0, - remove_others=False): + remove_others=False): """ Request to show a page in the GUI. @param name: page resource requested @@ -340,11 +361,10 @@ def show_pages(self, page_names: List[str], index: int = 0, LOG.error('Default index is larger than page list length') index = len(page_names) - 1 - # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already if any(p.endswith(".qml") for p in page_names): LOG.warning("received invalid page, please remove '.qml' extension from your code, " "this has been deprecated in ovos-gui and may stop working anytime") - page_names = [p.replace(".qml", "") for p in page_names] + page_names = [self._normalize_page_name(n) for n in page_names] if remove_others: self.remove_all_pages(except_pages=page_names) @@ -384,11 +404,11 @@ def remove_pages(self, page_names: List[str]): page_names = [page_names] if not isinstance(page_names, list): raise ValueError('page_names must be a list') - # TODO backwards compat, remove eventually, the deprecation happened in ovos-workshop already if any(p.endswith(".qml") for p in page_names): LOG.warning("received invalid page, please remove '.qml' extension from your code, " "this has been deprecated in ovos-gui and may stop working anytime") - page_names = [p.replace(".qml", "") for p in page_names] + page_names = [self._normalize_page_name(n) for n in page_names] + self.bus.emit(Message("gui.page.delete", {"page_names": page_names, "__from": self.skill_id})) From 0b2b765d753e701454fabcc9d8d36df2a283e166 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 22:19:00 +0100 Subject: [PATCH 7/9] better compat --- ovos_bus_client/apis/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index de2a3e8..5b8b225 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -314,13 +314,13 @@ def _normalize_page_name(page_name: str) -> str: """ if isfile(page_name): LOG.error("GUI resources should specify a resource name and " - "not a file path.", "0.1.0") + "not a file path.") return page_name file, ext = splitext(page_name) if ext == ".qml": LOG.error("GUI resources should exclude gui-specific file " f"extensions. This call should probably pass " - f"`{file}`, instead of `{page_name}`", "0.1.0") + f"`{file}`, instead of `{page_name}`") return file return page_name From fa11187fc2e38df202bdef26aafdfc380ee85064 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 20 Sep 2024 22:20:02 +0100 Subject: [PATCH 8/9] better compat --- ovos_bus_client/apis/gui.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index 5b8b225..406f062 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -313,16 +313,13 @@ def _normalize_page_name(page_name: str) -> str: @return: normalized string name (`.qml` removed for other GUI support) """ if isfile(page_name): - LOG.error("GUI resources should specify a resource name and " - "not a file path.") - return page_name + raise ValueError("GUI resources should specify a resource name and not a file path.") file, ext = splitext(page_name) if ext == ".qml": LOG.error("GUI resources should exclude gui-specific file " f"extensions. This call should probably pass " f"`{file}`, instead of `{page_name}`") return file - return page_name # base gui interactions From 202b416383c4f00bd48eec665d6916712c6a9305 Mon Sep 17 00:00:00 2001 From: miro Date: Mon, 21 Oct 2024 23:01:23 +0100 Subject: [PATCH 9/9] fix:qml_deprecated_extension --- ovos_bus_client/apis/gui.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ovos_bus_client/apis/gui.py b/ovos_bus_client/apis/gui.py index 406f062..4f8bb38 100644 --- a/ovos_bus_client/apis/gui.py +++ b/ovos_bus_client/apis/gui.py @@ -74,7 +74,7 @@ class GUIInterface: via the built-in sessionData mechanism. For example, in Python you can write in a skill: self.gui['temp'] = 33 - self.gui.show_page('Weather.qml') + self.gui.show_page('Weather') Then in the Weather.qml you'd access the temp via code such as: text: sessionData.time """ @@ -511,7 +511,7 @@ def show_text(self, text: str, title: Optional[str] = None, """ self["text"] = text self["title"] = title - self.show_page("SYSTEM_TextFrame.qml", override_idle, + self.show_page("SYSTEM_TextFrame", override_idle, override_animations) def show_image(self, url: str, caption: Optional[str] = None, @@ -543,7 +543,7 @@ def show_image(self, url: str, caption: Optional[str] = None, self["caption"] = caption self["fill"] = fill self["background_color"] = background_color - self.show_page("SYSTEM_ImageFrame.qml", override_idle, + self.show_page("SYSTEM_ImageFrame", override_idle, override_animations) def show_animated_image(self, url: str, caption: Optional[str] = None, @@ -575,7 +575,7 @@ def show_animated_image(self, url: str, caption: Optional[str] = None, self["caption"] = caption self["fill"] = fill self["background_color"] = background_color - self.show_page("SYSTEM_AnimatedImageFrame.qml", override_idle, + self.show_page("SYSTEM_AnimatedImageFrame", override_idle, override_animations) def show_html(self, html: str, resource_url: Optional[str] = None, @@ -597,7 +597,7 @@ def show_html(self, html: str, resource_url: Optional[str] = None, """ self["html"] = html self["resourceLocation"] = resource_url - self.show_page("SYSTEM_HtmlFrame.qml", override_idle, + self.show_page("SYSTEM_HtmlFrame", override_idle, override_animations) def show_url(self, url: str, override_idle: Union[int, bool] = None, @@ -616,7 +616,7 @@ def show_url(self, url: str, override_idle: Union[int, bool] = None, False: 'Default' always show animations. """ self["url"] = url - self.show_page("SYSTEM_UrlFrame.qml", override_idle, + self.show_page("SYSTEM_UrlFrame", override_idle, override_animations) def show_input_box(self, title: Optional[str] = None, @@ -648,7 +648,7 @@ def show_input_box(self, title: Optional[str] = None, else: self["exit_text"] = exit_text - self.show_page("SYSTEM_InputBox.qml", override_idle, + self.show_page("SYSTEM_InputBox", override_idle, override_animations) def remove_input_box(self): @@ -657,7 +657,7 @@ def remove_input_box(self): """ LOG.info(f"GUI pages length {len(self._pages)}") if len(self._pages) > 1: - self.remove_page("SYSTEM_InputBox.qml") + self.remove_page("SYSTEM_InputBox") else: self.release()