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.