Files
fenrir/src/fenrirscreenreader/core/vmenuManager.py
2025-07-07 00:42:23 -04:00

401 lines
14 KiB
Python
Executable File

#!/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}")
# 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