How do I get information about UI changes from within a Kodi addon?

106 Views Asked by At

I am developing a screen reader add-on for Kodi. main.py is the only non-empty .py file in the project (I also have an addon.xml file):

import xbmc
import xbmcgui
import subprocess


class ScreenReader(xbmc.Monitor):

    def __init__(self):
        xbmc.Monitor.__init__(self)
        self.last_focused_control_id = None
        self.last_selected_list_item = -1
        self.window_id = xbmcgui.getCurrentWindowId()
        xbmc.log("Screen Reader Debug: Monitor initialized.", level=xbmc.LOGINFO)

    def onScreensaverActivated(self) -> None:
        self.read_aloud("Screen saver active")

    def onScreensaverDeactivated(self) -> None:
        self.read_aloud("Screen saver deactivated")

    def onNotification(self, sender: str, method: str, data: str) -> None:
        xbmc.log(
            f"OnNotification triggered: sender: {sender}, method: {method}, data: {data}",
            level=xbmc.LOGINFO,
        )

    def checkFocusChange(self):
        new_window_id = xbmcgui.getCurrentWindowId()
        if self.window_id != new_window_id:
            self.window_id = new_window_id
            self.last_focused_control_id = None
            self.last_selected_list_item = None
        window = xbmcgui.Window(self.window_id)

        # Get currently focused control
        try:
            control = window.getFocus()
        except RuntimeError:
            self.read_aloud("No focus or runtime error")
            return

        # Check if it's the same as the last one
        if self.last_focused_control_id == control.getId():
            if not isinstance(control, xbmcgui.ControlList):
                return
            else:
                new_item_label = self.try_get_label(control)
                if new_item_label == self.last_selected_list_item:
                    return
                else:
                    self.last_selected_list_item = new_item_label

        # If the control has changed, store this new control and get its label
        self.last_focused_control_id = control.getId()
        element_label = self.try_get_label(control)

        # Log the label (this is just for debugging)
        xbmc.log(
            "Screen Reader Debug: Control label is "
            + str(element_label)
            + "Control type is "
            + str(type(control)),
            level=xbmc.LOGINFO,
        )

        # Read out the label
        if element_label:
            self.read_aloud(element_label)

    def try_get_label(self, control: xbmcgui.Control) -> str:
        if isinstance(control, xbmcgui.ControlButton):
            return f"{control.getLabel()}, button"
        elif isinstance(control, xbmcgui.ControlEdit):
            return f"{control.getLabel()}, edit field, {control.getText()}"
        elif isinstance(control, xbmcgui.ControlFadeLabel):
            return "Fade label not implemented"
        elif isinstance(control, xbmcgui.ControlGroup):
            return "Control group"
        elif isinstance(control, xbmcgui.ControlImage):
            return "Image"
        elif isinstance(control, xbmcgui.ControlLabel):
            return f"{control.getLabel()}, label"
        elif isinstance(control, xbmcgui.ControlList):
            label = "List, "
            item = control.getSelectedItem()
            if item is None:
                label += "No selection"
            else:
                label += item.getLabel() + ", " + item.getLabel2()
            return label
        elif isinstance(control, xbmcgui.ControlProgress):
            return f"{control.getPercent():.2f} percent, progress indicator"
        elif isinstance(control, xbmcgui.ControlRadioButton):
            return f"{'Checked' if control.isSelected() else 'Unchecked'}, radio button"
        elif isinstance(control, xbmcgui.ControlSlider):
            return f"{control.getPercent():.2f} percent, slider"
        elif isinstance(control, xbmcgui.ControlSpin):
            return "spin control"
        elif isinstance(control, xbmcgui.ControlTextBox):
            return f"{control.getText()}, text box"
        else:
            return f"Not implemented control type, {str(type(control))}"

    def read_aloud(self, text):
        if not text:  # If text is None or empty
            return
        cmd = [
            "powershell.exe",
            "-command",
            'Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.Speak("'
            + text
            + '")',
        ]
        subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            creationflags=subprocess.CREATE_NO_WINDOW,
        )


if __name__ == "__main__":
    xbmc.log("Screen Reader Debug: Script started.", level=xbmc.LOGINFO)
    screen_reader = ScreenReader()
    screen_reader.read_aloud("Welcome to screen reader")
    while not screen_reader.waitForAbort(0.1):
        screen_reader.checkFocusChange()
        if screen_reader.abortRequested():
            xbmc.log("Screen Reader Debug: Abort requested.", level=xbmc.LOGINFO)
            break

onNotification is never called and there are no log entries about it. What do I need to subscribe to for notifications to work properly?

Because of this I created a function that checks if focus changed and TTS announces newly focused button, image or radio button. But if control is a list TTS just reads "list, no selection" and does not react to focus changes within that list. Both getSelectedItem and getSelectedPosition always return None and -1 (even if the list is not empty), so my add-on never knows which list item is selected. How to solve this?

Of course the notification way is preferred.

0

There are 0 best solutions below