Skip to content

Commit

Permalink
bus upload qml (#140)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel McKnight <daniel@neongecko.com>
  • Loading branch information
JarbasAl and NeonDaniel authored Jun 13, 2023
1 parent 5b33202 commit e407808
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 13 deletions.
70 changes: 59 additions & 11 deletions ovos_utils/gui.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from os import walk
from typing import List, Union, Optional, Callable, Any

import time
Expand Down Expand Up @@ -499,8 +500,18 @@ class GUIInterface:
text: sessionData.time
"""

def __init__(self, skill_id, bus=None, remote_server=None, config=None,
resource_dir=None):
def __init__(self, skill_id: str, bus = None,
remote_server: str = 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
"""
if not config:
LOG.warning(f"Expected a dict config and got None. This config"
f"fallback behavior will be deprecated in a future "
Expand All @@ -521,7 +532,7 @@ def __init__(self, skill_id, bus=None, remote_server=None, config=None,
self._skill_id = skill_id
self.on_gui_changed_callback = None
self._events = []
self.resource_dir = resource_dir
self.ui_directories = ui_directories
if bus:
self.set_bus(bus)

Expand All @@ -540,6 +551,9 @@ def set_bus(self, bus=None):

@property
def bus(self):
"""
Return the attached MessageBusClient
"""
return self._bus

@bus.setter
Expand All @@ -548,6 +562,9 @@ def bus(self, val):

@property
def skill_id(self) -> str:
"""
Return the ID of the module implementing this interface
"""
return self._skill_id

@skill_id.setter
Expand All @@ -557,23 +574,23 @@ def skill_id(self, val: str):
@property
def page(self) -> Optional[str]:
"""
Return the current page
Return the active GUI page (file path) to show
"""
# the active GUI page (e.g. QML template) to show
return self.pages[self.current_page_idx] if len(self.pages) else None

@property
def connected(self) -> bool:
"""
Returns True if at least 1 remote gui is connected or if gui is
installed and running locally, else False"""
installed and running locally, else False
"""
if not self.bus:
return False
return can_use_gui(self.bus)

def build_message_type(self, event: str) -> str:
"""
Builds a message matching the output from the enclosure.
Ensure the specified event prepends this interface's `skill_id`
"""
if not event.startswith(f'{self.skill_id}.'):
event = f'{self.skill_id}.' + event
Expand All @@ -587,6 +604,35 @@ 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)

def upload_gui_pages(self, message: Message):
"""
Emit a response Message with all known GUI resources managed by
this interface.
@param message: `gui.request_page_upload` Message requesting pages
"""
if not self.ui_directories:
LOG.debug("No UI resources to upload")
return
request_res_type = message.data.get("framework", "qt5")
if request_res_type not in self.ui_directories:
LOG.warning(f"Requested UI files not available: {request_res_type}")
return

pages = dict()
res_dir = self.ui_directories[request_res_type]
for path, _, files in walk(res_dir):
for file in files:
full_path: str = join(path, file)
rel_path = full_path.replace(f"{res_dir}/", "", 1)
fname = join(self.skill_id, rel_path)
with open(full_path, 'r') as f:
pages[fname] = f.read()

self.bus.emit(message.forward("gui.page.upload",
{"__from": self.skill_id,
"pages": pages}))

def register_handler(self, event: str, handler: Callable):
"""
Expand Down Expand Up @@ -654,10 +700,8 @@ def __getitem__(self, key):
"""Implements get part of dict-like behaviour with named keys."""
return self.__session_data[key]

def get(self, *args, **kwargs) -> Any:
"""
Implements the get method for accessing dict keys.
"""
def get(self, *args, **kwargs):
"""Implements the get method for accessing dict keys."""
return self.__session_data.get(*args, **kwargs)

def __contains__(self, key):
Expand Down Expand Up @@ -778,7 +822,10 @@ def show_pages(self, page_names: List[str], index: int = 0,
data.update({'__from': self.skill_id})
LOG.debug(f"Updating gui data: {data}")
self.bus.emit(Message("gui.value.set", data))

page_urls = self._pages2uri(page_names)

# finally tell gui what to show
self.bus.emit(Message("gui.page.show",
{"page": page_urls,
"index": index,
Expand Down Expand Up @@ -822,6 +869,7 @@ def show_notification(self, content: str, duration: int = 10,
Arguments:
content (str): Main text content of a notification, Limited
to two visual lines.
duration (int): seconds to display notification for
action (str): Callback to any event registered by the skill
to perform a certain action when notification is clicked.
noticetype (str):
Expand Down
160 changes: 158 additions & 2 deletions test/unittests/test_gui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import unittest
from os.path import join, dirname
from threading import Event
from unittest.mock import patch, call

from ovos_bus_client.message import Message


class TestGui(unittest.TestCase):
@patch("ovos_utils.gui.has_screen")
Expand Down Expand Up @@ -75,6 +79,158 @@ def test_gui_dict(self):
from ovos_utils.gui import _GUIDict
# TODO

def test_gui_interface(self):
from ovos_utils.gui import GUIInterface

class TestGuiInterface(unittest.TestCase):
from ovos_utils.messagebus import FakeBus
from ovos_utils.gui import GUIInterface
bus = FakeBus()
config = {"extension": "test"}
ui_base_dir = join(dirname(__file__), "test_ui")
ui_dirs = {'qt5': join(ui_base_dir, 'ui')}
iface_name = "test_interface"
interface = GUIInterface(iface_name, bus, None, config, ui_dirs)

def test_00_gui_interface_init(self):
self.assertEqual(self.interface.config, self.config)
self.assertEqual(self.interface.bus, self.bus)
self.assertIsNone(self.interface.remote_url)
self.assertIsNone(self.interface.on_gui_changed_callback)
self.assertEqual(self.interface.ui_directories, self.ui_dirs)
self.assertEqual(self.interface.skill_id, self.iface_name)
self.assertIsNone(self.interface.page)
self.assertIsInstance(self.interface.connected, bool)

def test_build_message_type(self):
name = "test"
self.assertEqual(self.interface.build_message_type(name),
f"{self.iface_name}.{name}")

name = f"{self.iface_name}.{name}"
self.assertEqual(self.interface.build_message_type(name), name)

def test_setup_default_handlers(self):
# TODO
pass

def test_upload_gui_pages(self):
msg = None
handled = Event()

def on_pages(message):
nonlocal msg
msg = message
handled.set()

self.bus.once('gui.page.upload', on_pages)
message = Message('test', {}, {'context': "Test"})
self.interface.upload_gui_pages(message)
self.assertTrue(handled.wait(10))

self.assertEqual(msg.context['context'], message.context['context'])
self.assertEqual(msg.msg_type, "gui.page.upload")
self.assertEqual(msg.data['__from'], self.iface_name)

pages = msg.data['pages']
self.assertIsInstance(pages, dict)
for key, val in pages.items():
self.assertIsInstance(key, str)
self.assertIsInstance(val, str)

test_file_key = join(self.iface_name, "test.qml")
self.assertEqual(pages.get(test_file_key), "Mock File Contents", pages)

test_file_key = join(self.iface_name, "subdir", "test.qml")
self.assertEqual(pages.get(test_file_key), "Nested Mock", pages)
# TODO: Test other frameworks

def test_register_handler(self):
# TODO
pass

def test_set_on_gui_changed(self):
# TODO
pass

def test_gui_set(self):
# TODO
pass

def test_sync_data(self):
# TODO
pass

def test_get(self):
# TODO
pass

def test_clear(self):
# TODO
pass

def test_send_event(self):
# TODO
pass

def test_pages2uri(self):
# TODO
pass

def test_show_page(self):
# TODO
pass

def test_show_pages(self):
# TODO
pass

def test_remove_page(self):
# TODO
pass

def test_remove_pages(self):
# TODO
pass

def test_show_notification(self):
# TODO
pass

def test_show_controlled_notification(self):
# TODO
pass

def test_remove_controlled_notification(self):
# TODO
pass

def test_show_text(self):
# TODO
pass

def test_show_image(self):
# TODO
pass

def test_show_animated_image(self):
# TODO
pass

def test_show_html(self):
# TODO
pass

def test_show_url(self):
# TODO
pass

def test_input_box(self):
# TODO
pass

def test_release(self):
# TODO
pass

def test_shutdown(self):
# TODO
pass
1 change: 1 addition & 0 deletions test/unittests/test_ui/ui/subdir/test.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Nested Mock
1 change: 1 addition & 0 deletions test/unittests/test_ui/ui/test.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Mock File Contents

0 comments on commit e407808

Please sign in to comment.