Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bus api for homescreen apps and examples #130

Merged
merged 18 commits into from
Nov 15, 2024
164 changes: 61 additions & 103 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
import datetime
import os
import tempfile
from os import environ, listdir, path
from os import path
from typing import Dict, List

from ovos_bus_client import Message
from ovos_config.locations import get_xdg_cache_save_path
from ovos_date_parser import get_date_strings
from ovos_utils import classproperty
from ovos_utils.lang import standardize_lang_tag
from ovos_utils.log import LOG
from ovos_utils.process_utils import RuntimeRequirements
from ovos_utils.time import now_local
Expand Down Expand Up @@ -50,6 +52,12 @@ def __init__(self, *args, **kwargs):
# Offline / Online State
self.system_connectivity = None

# Bus apis for skills to register with homescreen, ovos-workshop provides util methods
# "skill_id": {"lang-code": ["utterance"]}
self.skill_examples: Dict[str, Dict[str, List[str]]] = {}
# "skill_id": {"icon": "xx.png", "event": "emit.this.bus.event"}
self.homescreen_apps: Dict[str, Dict[str, str]] = {}

super().__init__(*args, **kwargs)

@classproperty
Expand Down Expand Up @@ -142,8 +150,45 @@ def initialize(self):
# self.selected_wallpaper = self.settings.get(
# "wallpaper") or "default.jpg"

self.add_event("homescreen.register.examples",
self.handle_register_sample_utterances)
self.add_event("homescreen.register.app",
self.handle_register_homescreen_app)
self.add_event("detach_skill",
self.handle_deregister_skill)

self.bus.emit(Message("homescreen.metadata.get"))
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
self.bus.emit(Message("mycroft.device.show.idle"))

#############
# bus apis
def handle_register_sample_utterances(self, message: Message):
"""a skill is registering utterance examples to render on idle screen"""
lang = standardize_lang_tag(message.data["lang"])
skill_id = message.data["skill_id"]
examples = message.data["utterances"]
if skill_id not in self.skill_examples:
self.skill_examples[skill_id] = {}
self.skill_examples[skill_id][lang] = examples

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
def handle_register_homescreen_app(self, message: Message):
"""a skill is registering an icon + bus event to show in app drawer (bottom pill button)"""
skill_id = message.data["skill_id"]
icon = message.data["icon"]
event = message.data["event"]
self.homescreen_apps[skill_id] = {"icon": icon, "event": event}

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
def handle_deregister_skill(self, message: Message):
"""skill unloaded, stop showing it's example utterances and app launcher icon"""
skill_id = message.data["skill_id"]
if skill_id in self.skill_examples:
self.skill_examples.pop(skill_id)
if skill_id in self.homescreen_apps:
self.homescreen_apps.pop(skill_id)

JarbasAl marked this conversation as resolved.
Show resolved Hide resolved
#############
# skill properties
@property
def examples_enabled(self):
# A variable to turn on/off the example text
Expand All @@ -166,13 +211,15 @@ def datetime_skill_id(self):
def handle_idle(self, message):
self._load_skill_apis()
LOG.debug('Activating OVOSHomescreen')
apps = self.build_voice_applications_model()
self.gui['wallpaper_path'] = self.selected_wallpaper_path
self.gui['selected_wallpaper'] = self.selected_wallpaper
self.gui['notification'] = {}
self.gui["notification_model"] = self.notifications_storage_model
self.gui["system_connectivity"] = "offline"
self.gui["applications_model"] = self.build_voice_applications_model()
self.gui["applications_model"] = apps
self.gui["persistent_menu_hint"] = self.settings.get("persistent_menu_hint", False)
self.gui["apps_enabled"] = bool(apps)

try:
self.update_dt()
Expand Down Expand Up @@ -202,14 +249,14 @@ def update_examples(self):
if self.skill_info_api:
examples = self.skill_info_api.skill_info_examples()
elif self.settings.get("examples_enabled"):
LOG.warning("NOT IMPLEMENTED ERROR: utterance examples enabled in settings.json but not yet implemented! "
"use an external skill_id via 'examples_skill' setting as an alternative")
self.settings["examples_enabled"] = False
for _skill_id, data in self.skill_examples.items():
examples += data.get(self.lang, [])

if examples:
self.gui['skill_examples'] = {"examples": examples}
self.gui['skill_info_enabled'] = self.examples_enabled
else:
LOG.warning("no utterance examples registered with homescreen")
self.gui['skill_info_enabled'] = False
self.gui['skill_info_prefix'] = self.settings.get("examples_prefix", False)

Expand Down Expand Up @@ -250,7 +297,7 @@ def update_dt(self):
date_format=self.config_core.get("date_format", "DMY"),
time_format=self.config_core.get("time_format", "full"),
lang=self.lang)
LOG.debug(f"Date info {self.lang}: {date_string_object}")
#LOG.debug(f"Date info {self.lang}: {date_string_object}")
time_string = date_string_object.get("time_string")
date_string = date_string_object.get("date_string")
weekday_string = date_string_object.get("weekday_string")
Expand Down Expand Up @@ -423,107 +470,18 @@ def _load_skill_apis(self):
LOG.error(f"Failed to import Info Skill: {e}")
self.skill_info_api = None

#####################################################################
# Build Voice Applications Model
# TODO - handle this via bus, this was a standard from plasma bigscreen which we never really adopted,
# and they dropped "voice apps" so there is nothing left to be compatible with

def find_icon_full_path(self, icon_name):
localuser = environ.get('USER')
folder_search_paths = ["/usr/share/icons/", "/usr/local/share/icons/",
f"/home/{localuser}/.local/share/icons/"]
for folder_search_path in folder_search_paths:
# SVG extension
icon_full_path = folder_search_path + icon_name + ".svg"
if path.exists(icon_full_path):
return icon_full_path
# PNG extension
icon_full_path = folder_search_path + icon_name + ".png"
if path.exists(icon_full_path):
return icon_full_path
# JPEG extension
icon_full_path = folder_search_path + icon_name + ".jpg"
if path.exists(icon_full_path):
return icon_full_path

def parse_desktop_file(self, file_path):
# TODO - handle this via bus, this was a standard from plasma bigscreen which we never really adopted,
# and they dropped "voice apps" so there is nothing left to be compatible with
if path.isfile(file_path) and path.splitext(file_path)[1] == ".desktop":

if path.isfile(file_path) and path.isfile(file_path) and path.getsize(file_path) > 0:

with open(file_path, "r") as f:
file_contents = f.read()

name_start = file_contents.find("Name=")
name_end = file_contents.find("\n", name_start)
name = file_contents[name_start + 5:name_end]

icon_start = file_contents.find("Icon=")
icon_end = file_contents.find("\n", icon_start)
icon_name = file_contents[icon_start + 5:icon_end]
icon = self.find_icon_full_path(icon_name)

exec_start = file_contents.find("Exec=")
exec_end = file_contents.find("\n", exec_start)
exec_line = file_contents[exec_start + 5:exec_end]
exec_array = exec_line.split(" ")
for arg in exec_array:
if arg.find("--skill=") == 0:
skill_name = arg.split("=")[1]
break
else:
skill_name = "None"
exec_path = skill_name

categories_start = file_contents.find("Categories=")
categories_end = file_contents.find("\n", categories_start)
categories = file_contents[categories_start +
11:categories_end]

categories_list = categories.split(";")

if "VoiceApp" in categories_list:
app_entry = {
"name": name,
"thumbnail": icon,
"action": exec_path
}
return app_entry
else:
return None
else:
return None
else:
return None
def build_voice_applications_model(self) -> List[Dict[str, str]]:
"""Build a list of voice applications for the GUI model.

def build_voice_applications_model(self):
voiceApplicationsList = []
localuser = environ.get('USER')
file_list = ["/usr/share/applications/", "/usr/local/share/applications/",
f"/home/{localuser}/.local/share/applications/"]
for file_path in file_list:
if os.path.isdir(file_path):
files = listdir(file_path)
for file in files:
app_dict = self.parse_desktop_file(file_path + file)
if app_dict is not None:
voiceApplicationsList.append(app_dict)

try:
sort_on = "name"
decorated = [(dict_[sort_on], dict_)
for dict_ in voiceApplicationsList]
decorated.sort()
return [dict_ for (key, dict_) in decorated]

except Exception:
return voiceApplicationsList
Returns:
List[Dict[str, str]]: List of application metadata containing
name, thumbnail path, and action event
"""
return [{"name": skill_id, "thumbnail": data["icon"], "action": data["event"]}
for skill_id, data in self.homescreen_apps.items()]
JarbasAl marked this conversation as resolved.
Show resolved Hide resolved

#####################################################################
# Handle Widgets

def handle_timer_widget_manager(self, message):
timerWidget = message.data.get("widget", {})
self.gui.send_event("ovos.timer.widget.manager.update", timerWidget)
Expand Down
27 changes: 0 additions & 27 deletions gui/qt5/AppsBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -118,33 +118,6 @@ Item {
}
}
}

Kirigami.Icon {
id: micListenIcon
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: parent.height
width: height
source: Qt.resolvedUrl("icons/mic-start.svg")
color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.5)

MouseArea {
anchors.fill: parent
onClicked: {
appBarRoot.close()
Mycroft.MycroftController.sendRequest("mycroft.mic.listen", {})
Mycroft.SoundEffects.playClickedSound(Qt.resolvedUrl("sounds/start-listening.wav"))
}

onPressed: {
micListenIcon.color = Kirigami.Theme.highlightColor
}

onReleased: {
micListenIcon.color = Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.5)
}
}
}
}

Kirigami.Separator {
Expand Down
4 changes: 3 additions & 1 deletion gui/qt5/idle.qml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Mycroft.CardDelegate {
property var textModel: sessionData.skill_examples ? sessionData.skill_examples.examples : []
property color shadowColor: Qt.rgba(0, 0, 0, 0.7)
property bool rtlMode: sessionData.rtl_mode ? Boolean(sessionData.rtl_mode) : false
property bool appsEnabled: sessionData.apps_enabled ? Boolean(sessionData.apps_enabled) : false
property bool examplesEnabled: sessionData.skill_info_enabled ? Boolean(sessionData.skill_info_enabled) : false
property bool examplesPrefix: sessionData.skill_info_prefix
property bool weatherEnabled: sessionData.weather_api_enabled ? Boolean(sessionData.weather_api_enabled) : false
Expand Down Expand Up @@ -83,6 +84,7 @@ Mycroft.CardDelegate {

controlBar: Local.AppsBar {
id: appBar
visible: idleRoot.appsEnabled
parentItem: idleRoot
appsModel: sessionData.applications_model
z: 100
Expand Down Expand Up @@ -271,7 +273,7 @@ Mycroft.CardDelegate {
anchors.bottom: parent.bottom
anchors.bottomMargin: -Mycroft.Units.gridUnit * 2
anchors.horizontalCenter: parent.horizontalCenter
visible: mainView.currentIndex == 1
visible: mainView.currentIndex == 1 && idleRoot.appsEnabled
enabled: mainView.currentIndex == 1
z: 2

Expand Down
Loading