401 lines
14 KiB
Python
Executable File
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
|