#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. import inspect import os import time from fenrirscreenreader.core import debug from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.utils import module_utils currentdir = os.path.dirname( os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe()))) ) fenrir_path = os.path.dirname(currentdir) class VmenuManager: def __init__(self): self.menuDict = {} self.curr_index = None self.currMenu = "" self.active = False self.reset = True self.useTimeout = True self.searchText = "" self.lastSearchTime = time.time() def initialize(self, environment): self.env = environment # use default path self.defaultVMenuPath = ( fenrir_path + "/commands/vmenu-profiles/" + self.env["runtime"]["InputManager"].get_shortcut_type() ) # if there is no user configuration if ( self.env["runtime"]["SettingsManager"].get_setting( "menu", "vmenuPath" ) != "" ): self.defaultVMenuPath = self.env["runtime"][ "SettingsManager" ].get_setting("menu", "vmenuPath") if not self.defaultVMenuPath.endswith("/"): self.defaultVMenuPath += "/" self.defaultVMenuPath += self.env["runtime"][ "InputManager" ].get_shortcut_type() self.create_menu_tree() self.closeAfterAction = False def shutdown(self): pass def clear_search_text(self): self.searchText = "" def search_entry(self, value, forceReset=False): if self.curr_index is None: return "" if self.reset or forceReset: self.clear_search_text() else: if self.useTimeout: if time.time() - self.lastSearchTime > 1: self.clear_search_text() self.searchText += value.upper() self.lastSearchTime = time.time() start_index = self.get_curr_index() while True: if not self.next_index(): return "" entry = self.get_current_entry() if entry.upper().startswith(self.searchText): return entry if start_index == self.get_curr_index(): return "" def set_curr_menu(self, currMenu=""): self.curr_index = None self.currMenu = "" if currMenu != "": currMenu += " " + _("Menu") try: t = self.menuDict[currMenu] l = list(self.menuDict.keys()) self.curr_index = [l.index(currMenu)] except Exception as e: print(e) self.currMenu = "" self.curr_index = None return if self.inc_level(): self.currMenu = currMenu else: self.currMenu = "" self.curr_index = None def get_curr_menu(self): return self.currMenu def get_active(self): return self.active def toggle_vmenu_mode(self, closeAfterAction=True): self.set_active(not self.get_active(), closeAfterAction) def set_active(self, active, closeAfterAction=True): if self.env["runtime"]["HelpManager"].is_tutorial_mode(): return self.active = active if self.active: self.closeAfterAction = closeAfterAction try: self.create_menu_tree() except Exception as e: print(e) try: if self.currMenu != "": self.set_curr_menu(self.currMenu) if self.curr_index is None: if len(self.menuDict) > 0: self.curr_index = [0] except Exception as e: print(e) try: # navigation self.env["bindings"][ str([1, ["KEY_ESC"]]) ] = "TOGGLE_VMENU_MODE" self.env["bindings"][str([1, ["KEY_UP"]])] = "PREV_VMENU_ENTRY" self.env["bindings"][ str([1, ["KEY_DOWN"]]) ] = "NEXT_VMENU_ENTRY" self.env["bindings"][ str([1, ["KEY_SPACE"]]) ] = "CURR_VMENU_ENTRY" self.env["bindings"][ str([1, ["KEY_LEFT"]]) ] = "DEC_LEVEL_VMENU" self.env["bindings"][ str([1, ["KEY_RIGHT"]]) ] = "INC_LEVEL_VMENU" self.env["bindings"][ str([1, ["KEY_ENTER"]]) ] = "EXEC_VMENU_ENTRY" # search self.env["bindings"][str([1, ["KEY_A"]])] = "SEARCH_A" self.env["bindings"][str([1, ["KEY_B"]])] = "SEARCH_B" self.env["bindings"][str([1, ["KEY_C"]])] = "SEARCH_C" self.env["bindings"][str([1, ["KEY_D"]])] = "SEARCH_D" self.env["bindings"][str([1, ["KEY_E"]])] = "SEARCH_E" self.env["bindings"][str([1, ["KEY_F"]])] = "SEARCH_F" self.env["bindings"][str([1, ["KEY_G"]])] = "SEARCH_G" self.env["bindings"][str([1, ["KEY_H"]])] = "SEARCH_H" self.env["bindings"][str([1, ["KEY_I"]])] = "SEARCH_I" self.env["bindings"][str([1, ["KEY_J"]])] = "SEARCH_J" self.env["bindings"][str([1, ["KEY_K"]])] = "SEARCH_K" self.env["bindings"][str([1, ["KEY_L"]])] = "SEARCH_L" self.env["bindings"][str([1, ["KEY_M"]])] = "SEARCH_M" self.env["bindings"][str([1, ["KEY_N"]])] = "SEARCH_N" self.env["bindings"][str([1, ["KEY_O"]])] = "SEARCH_O" self.env["bindings"][str([1, ["KEY_P"]])] = "SEARCH_P" self.env["bindings"][str([1, ["KEY_Q"]])] = "SEARCH_Q" self.env["bindings"][str([1, ["KEY_R"]])] = "SEARCH_R" self.env["bindings"][str([1, ["KEY_S"]])] = "SEARCH_S" self.env["bindings"][str([1, ["KEY_T"]])] = "SEARCH_T" self.env["bindings"][str([1, ["KEY_U"]])] = "SEARCH_U" self.env["bindings"][str([1, ["KEY_V"]])] = "SEARCH_V" self.env["bindings"][str([1, ["KEY_W"]])] = "SEARCH_W" self.env["bindings"][str([1, ["KEY_X"]])] = "SEARCH_X" self.env["bindings"][str([1, ["KEY_Y"]])] = "SEARCH_Y" self.env["bindings"][str([1, ["KEY_Z"]])] = "SEARCH_Z" # page navigation self.env["bindings"][ str([1, ["KEY_PAGEUP"]]) ] = "PAGE_UP_VMENU" self.env["bindings"][ str([1, ["KEY_PAGEDOWN"]]) ] = "PAGE_DOWN_VMENU" except Exception as e: print(e) else: try: self.curr_index = None self.env["bindings"] = self.env["runtime"][ "SettingsManager" ].get_binding_backup() except Exception as e: self.env["runtime"]["DebugManager"].write_debug_out( "VmenuManager set_active: Error loading binding backup: " + str(e), debug.DebugLevel.ERROR, ) def create_menu_tree(self, resetIndex=True): if resetIndex: self.curr_index = None menu = self.fs_tree_to_dict(self.defaultVMenuPath) if menu: self.menuDict = menu # Add dynamic voice menus try: from fenrirscreenreader.core.dynamicVoiceMenu import ( add_dynamic_voice_menus, ) add_dynamic_voice_menus(self) except Exception as e: print(f"Error adding dynamic voice menus: {e}") # Add dynamic keyboard layout menus try: from fenrirscreenreader.core.dynamicKeyboardLayoutMenu import ( add_dynamic_keyboard_layout_menus, ) add_dynamic_keyboard_layout_menus(self) except Exception as e: print(f"Error adding dynamic keyboard layout menus: {e}") # index still valid? if self.curr_index is not None: try: r = self.get_value_by_path(self.menuDict, self.curr_index) if r == {}: self.curr_index = None except Exception as e: self.env["runtime"]["DebugManager"].write_debug_out( "VmenuManager create_menu_tree: Error checking menu index validity: " + str(e), debug.DebugLevel.ERROR, ) self.curr_index = None def execute_menu(self): if self.curr_index is None: return try: command = self.get_value_by_path(self.menuDict, self.curr_index) if command is not None: command.run() if self.closeAfterAction: self.set_active(False) except Exception as e: try: self.inc_level() text = self.get_current_entry() self.env["runtime"]["OutputManager"].present_text( text, interrupt=True ) except Exception as ex: self.env["runtime"]["DebugManager"].write_debug_out( "VmenuManager execute_menu: Error presenting menu text: " + str(ex), debug.DebugLevel.ERROR, ) def inc_level(self): if self.curr_index is None: return False try: r = self.get_value_by_path(self.menuDict, self.curr_index + [0]) if r == {}: return False except Exception as e: self.env["runtime"]["DebugManager"].write_debug_out( "VmenuManager inc_level: Error accessing menu path: " + str(e), debug.DebugLevel.ERROR, ) return False self.curr_index.append(0) return True def dec_level(self): if self.curr_index is None: return False if self.currMenu != "": if len(self.curr_index) <= 2: return False elif len(self.curr_index) == 1: return False self.curr_index = self.curr_index[: len(self.curr_index) - 1] return True def next_index(self): if self.curr_index is None: return False if self.curr_index[len(self.curr_index) - 1] + 1 >= len( self.get_nested_by_path(self.menuDict, self.curr_index[:-1]) ): self.curr_index[len(self.curr_index) - 1] = 0 else: self.curr_index[len(self.curr_index) - 1] += 1 return True def get_curr_index(self): if self.curr_index is None: return 0 return self.curr_index[len(self.curr_index) - 1] def prev_index(self): if self.curr_index is None: return False if self.curr_index[len(self.curr_index) - 1] == 0: self.curr_index[len(self.curr_index) - 1] = ( len( self.get_nested_by_path( self.menuDict, self.curr_index[:-1] ) ) - 1 ) else: self.curr_index[len(self.curr_index) - 1] -= 1 return True def page_up(self): if self.curr_index is None: return False menu_size = len( self.get_nested_by_path(self.menuDict, self.curr_index[:-1]) ) if menu_size <= 1: return False jump_size = max(1, int(menu_size * 0.1)) # 10% of menu size, minimum 1 new_index = self.curr_index[len(self.curr_index) - 1] - jump_size if new_index < 0: new_index = 0 self.curr_index[len(self.curr_index) - 1] = new_index return True def page_down(self): if self.curr_index is None: return False menu_size = len( self.get_nested_by_path(self.menuDict, self.curr_index[:-1]) ) if menu_size <= 1: return False jump_size = max(1, int(menu_size * 0.1)) # 10% of menu size, minimum 1 new_index = self.curr_index[len(self.curr_index) - 1] + jump_size if new_index >= menu_size: new_index = menu_size - 1 self.curr_index[len(self.curr_index) - 1] = new_index return True def get_current_entry(self): return self.get_keys_by_path(self.menuDict, self.curr_index)[ self.curr_index[-1] ] def fs_tree_to_dict(self, path_): for root, dirs, files in os.walk(path_): tree = { d + " " + _("Menu"): self.fs_tree_to_dict(os.path.join(root, d)) for d in dirs if not d.startswith("__") } for f in files: try: file_name, file_extension = os.path.splitext(f) file_name = file_name.split("/")[-1] if file_name.startswith("__"): continue # Skip base classes that shouldn't be loaded as commands if file_name.endswith("_base"): continue command = self.env["runtime"]["CommandManager"].load_file( root + "/" + f ) tree.update({file_name + " " + _("Action"): command}) except Exception as e: print(e) return tree # note we discontinue iteration trough os.walk def get_nested_by_path(self, complete, path): path = path.copy() if path != []: index = list(complete.keys())[path[0]] nested = self.get_nested_by_path(complete[index], path[1:]) return nested else: return complete def get_keys_by_path(self, complete, path): if not isinstance(complete, dict): return [] d = complete for i in path[:-1]: d = d[list(d.keys())[i]] return list(d.keys()) def get_value_by_path(self, complete, path): if not isinstance(complete, dict): return complete d = complete.copy() for i in path: d = d[list(d.keys())[i]] return d