Improve tutorial help navigation
This commit is contained in:
@@ -130,4 +130,3 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
||||
KEY_FENRIR,KEY_CTRL,KEY_DOWN=read_all_by_line
|
||||
KEY_FENRIR,KEY_CTRL,KEY_PAGEDOWN=read_all_by_page
|
||||
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
||||
KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout
|
||||
|
||||
@@ -130,4 +130,3 @@ KEY_FENRIR,KEY_F8=export_clipboard_to_x
|
||||
KEY_FENRIR,KEY_CTRL,KEY_DOWN=read_all_by_line
|
||||
KEY_FENRIR,KEY_CTRL,KEY_PAGEDOWN=read_all_by_page
|
||||
KEY_FENRIR,KEY_SHIFT,KEY_V=announce_fenrir_version
|
||||
KEY_FENRIR,KEY_LEFTCTRL,KEY_F4=cycle_keyboard_layout
|
||||
|
||||
@@ -13,6 +13,8 @@ from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
help_visible = False
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import os
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("cycles between available keyboard layouts")
|
||||
|
||||
def get_available_layouts(self):
|
||||
"""Get list of available keyboard layout files"""
|
||||
layouts = []
|
||||
|
||||
# Check standard locations for keyboard layouts
|
||||
settings_root = "/etc/fenrirscreenreader/"
|
||||
if not os.path.exists(settings_root):
|
||||
# Fallback to source directory
|
||||
import fenrirscreenreader
|
||||
|
||||
fenrir_path = os.path.dirname(fenrirscreenreader.__file__)
|
||||
settings_root = fenrir_path + "/../../config/"
|
||||
|
||||
keyboard_path = settings_root + "keyboard/"
|
||||
|
||||
if os.path.exists(keyboard_path):
|
||||
for file in os.listdir(keyboard_path):
|
||||
if (
|
||||
file.endswith(".conf")
|
||||
and not file.startswith("__")
|
||||
and not file.lower().startswith("pty")
|
||||
):
|
||||
layout_name = file.replace(".conf", "")
|
||||
if layout_name not in layouts:
|
||||
layouts.append(layout_name)
|
||||
|
||||
# Ensure we have at least basic layouts
|
||||
if not layouts:
|
||||
layouts = ["desktop", "laptop"]
|
||||
else:
|
||||
layouts.sort()
|
||||
|
||||
return layouts
|
||||
|
||||
def run(self):
|
||||
current_layout = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"keyboard", "keyboard_layout"
|
||||
)
|
||||
|
||||
# Extract layout name from full path if needed
|
||||
if "/" in current_layout:
|
||||
current_layout = os.path.basename(current_layout).replace(
|
||||
".conf", ""
|
||||
)
|
||||
|
||||
# Get available layouts
|
||||
available_layouts = self.get_available_layouts()
|
||||
|
||||
# Find next layout in cycle
|
||||
try:
|
||||
current_index = available_layouts.index(current_layout)
|
||||
next_index = (current_index + 1) % len(available_layouts)
|
||||
except ValueError:
|
||||
# If current layout not found, start from beginning
|
||||
next_index = 0
|
||||
|
||||
next_layout = available_layouts[next_index]
|
||||
|
||||
# Update setting and reload shortcuts
|
||||
self.env["runtime"]["SettingsManager"].set_setting(
|
||||
"keyboard", "keyboard_layout", next_layout
|
||||
)
|
||||
|
||||
# Reload shortcuts with new layout
|
||||
try:
|
||||
self.env["runtime"]["InputManager"].reload_shortcuts()
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("Switched to {} keyboard layout").format(next_layout),
|
||||
interrupt=True,
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Error reloading shortcuts: " + str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("Error switching keyboard layout"), interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -19,19 +19,47 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
self.env["runtime"]["HelpManager"].toggle_tutorial_mode()
|
||||
return _(
|
||||
"Exiting tutorial mode. To enter tutorial mode again press Fenrir+f1"
|
||||
)
|
||||
return _("enter or leave tutorial mode")
|
||||
|
||||
def run(self):
|
||||
self.env["runtime"]["HelpManager"].toggle_tutorial_mode()
|
||||
if self.env["runtime"]["HelpManager"].is_tutorial_mode():
|
||||
help_manager = self.env["runtime"]["HelpManager"]
|
||||
was_active = help_manager.is_tutorial_mode()
|
||||
help_manager.toggle_tutorial_mode()
|
||||
is_active = help_manager.is_tutorial_mode()
|
||||
if was_active and is_active:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_(
|
||||
"Entering tutorial mode. In this mode commands are described but not "
|
||||
"executed. You can move through the list of commands with the up and "
|
||||
"down arrow keys. To Exit tutorial mode press Fenrir+f1."
|
||||
"Unable to exit tutorial mode because exclusive keyboard "
|
||||
"capture could not be released. Press Fenrir+F1 or Escape "
|
||||
"to try again."
|
||||
),
|
||||
interrupt=True,
|
||||
)
|
||||
elif is_active:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_(
|
||||
"Entering tutorial mode. In this mode commands are "
|
||||
"described but not executed. Use up and down to browse "
|
||||
"actions and left and right to switch between Active "
|
||||
"actions, Unbound actions, and Plugins. Press Space to "
|
||||
"repeat the current item. To exit tutorial mode press "
|
||||
"Fenrir+F1 or Escape. Active actions."
|
||||
),
|
||||
interrupt=True,
|
||||
)
|
||||
if self.env["runtime"]["HelpManager"].is_capture_degraded():
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_(
|
||||
"Warning: full keyboard capture is unavailable. "
|
||||
"Unbound keys may reach the active application."
|
||||
),
|
||||
interrupt=False,
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_(
|
||||
"Exiting tutorial mode. To enter tutorial mode again "
|
||||
"press Fenrir+F1"
|
||||
),
|
||||
interrupt=True,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
self.env["runtime"]["VmenuManager"].toggle_vmenu_mode()
|
||||
return _("Entering or Leaving v menu mode.")
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("show next help category")
|
||||
|
||||
def run(self):
|
||||
text = self.env["runtime"]["HelpManager"].next_help_section()
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
text, interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("show previous help category")
|
||||
|
||||
def run(self):
|
||||
text = self.env["runtime"]["HelpManager"].prev_help_section()
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
text, interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -436,7 +436,10 @@ class CommandManager:
|
||||
)
|
||||
|
||||
def execute_command(self, command, section="commands"):
|
||||
if self.env["runtime"]["ScreenManager"].is_ignored_screen():
|
||||
if (
|
||||
self.env["runtime"]["ScreenManager"].is_ignored_screen()
|
||||
and not self.env["runtime"]["HelpManager"].is_tutorial_mode()
|
||||
):
|
||||
return
|
||||
if self.command_exists(command, section):
|
||||
try:
|
||||
|
||||
@@ -82,16 +82,25 @@ class FenrirManager:
|
||||
else:
|
||||
return
|
||||
|
||||
if self.environment["runtime"]["ScreenManager"].is_ignored_screen():
|
||||
tutorial_mode = self.environment["runtime"][
|
||||
"HelpManager"
|
||||
].is_tutorial_mode()
|
||||
if (
|
||||
self.environment["runtime"]["ScreenManager"].is_ignored_screen()
|
||||
and not tutorial_mode
|
||||
):
|
||||
self.environment["runtime"]["InputManager"].write_event_buffer()
|
||||
else:
|
||||
if self.environment["runtime"]["HelpManager"].is_tutorial_mode():
|
||||
if tutorial_mode:
|
||||
self.environment["runtime"][
|
||||
"InputManager"
|
||||
].clear_event_buffer()
|
||||
self.environment["runtime"]["InputManager"].key_echo(
|
||||
event["data"]
|
||||
)
|
||||
if self.environment["runtime"][
|
||||
"ScreenManager"
|
||||
].is_ignored_screen():
|
||||
self.environment["runtime"]["InputManager"].key_echo(
|
||||
event["data"]
|
||||
)
|
||||
|
||||
if self.environment["runtime"]["VmenuManager"].get_active():
|
||||
self.environment["runtime"][
|
||||
@@ -152,10 +161,15 @@ class FenrirManager:
|
||||
if self.environment["runtime"]["CommandManager"].command_exists(
|
||||
current_command, "help"
|
||||
):
|
||||
self.environment["runtime"]["CommandManager"].execute_command(
|
||||
self.environment["runtime"]["CommandManager"].run_command(
|
||||
current_command, "help"
|
||||
)
|
||||
return
|
||||
if current_command == "TOGGLE_TUTORIAL_MODE":
|
||||
self.environment["runtime"]["CommandManager"].run_command(
|
||||
current_command, "commands"
|
||||
)
|
||||
return
|
||||
elif self.environment["runtime"]["VmenuManager"].get_active():
|
||||
if self.environment["runtime"]["CommandManager"].command_exists(
|
||||
current_command, "vmenu-navigation"
|
||||
@@ -324,7 +338,13 @@ class FenrirManager:
|
||||
self.singleKeyCommand = True
|
||||
elif (
|
||||
(
|
||||
self.environment["runtime"]["VmenuManager"].get_active()
|
||||
(
|
||||
"HelpManager" in self.environment["runtime"]
|
||||
and self.environment["runtime"][
|
||||
"HelpManager"
|
||||
].is_tutorial_mode()
|
||||
)
|
||||
or self.environment["runtime"]["VmenuManager"].get_active()
|
||||
or self.environment["runtime"][
|
||||
"DiffReviewManager"
|
||||
].is_active()
|
||||
|
||||
@@ -4,99 +4,181 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import ast
|
||||
import copy
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class HelpManager:
|
||||
ACTIVE_SECTION = "active"
|
||||
UNBOUND_SECTION = "unbound"
|
||||
PLUGINS_SECTION = "plugins"
|
||||
HELP_SECTIONS = (
|
||||
ACTIVE_SECTION,
|
||||
UNBOUND_SECTION,
|
||||
PLUGINS_SECTION,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.helpDict = {}
|
||||
self.tutorialListIndex = None
|
||||
self.env = None
|
||||
self.help_lists = {
|
||||
self.ACTIVE_SECTION: [],
|
||||
self.UNBOUND_SECTION: [],
|
||||
self.PLUGINS_SECTION: [],
|
||||
}
|
||||
self.help_section = self.ACTIVE_SECTION
|
||||
self.tutorial_list_index = None
|
||||
self.bindings_backup = None
|
||||
self.raw_bindings_backup = None
|
||||
self.capture_degraded = False
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
if self.is_tutorial_mode():
|
||||
self.set_tutorial_mode(False)
|
||||
|
||||
def toggle_tutorial_mode(self):
|
||||
self.set_tutorial_mode(not self.env["general"]["tutorialMode"])
|
||||
self.set_tutorial_mode(not self.is_tutorial_mode())
|
||||
|
||||
def set_tutorial_mode(self, newTutorialMode):
|
||||
if self.env["runtime"]["VmenuManager"].get_active():
|
||||
return
|
||||
self.env["general"]["tutorialMode"] = newTutorialMode
|
||||
if newTutorialMode:
|
||||
def set_tutorial_mode(self, tutorial_mode):
|
||||
if tutorial_mode == self.is_tutorial_mode():
|
||||
return True
|
||||
if tutorial_mode and self.env["runtime"]["VmenuManager"].get_active():
|
||||
return False
|
||||
|
||||
if tutorial_mode:
|
||||
self.bindings_backup = self.env["bindings"].copy()
|
||||
self.raw_bindings_backup = copy.deepcopy(
|
||||
self.env["rawBindings"]
|
||||
)
|
||||
self.create_help_dict()
|
||||
self.env["bindings"][
|
||||
str([1, ["KEY_ESC"]])
|
||||
] = "TOGGLE_TUTORIAL_MODE"
|
||||
self.env["bindings"][str([1, ["KEY_UP"]])] = "PREV_HELP"
|
||||
self.env["bindings"][str([1, ["KEY_DOWN"]])] = "NEXT_HELP"
|
||||
self.env["bindings"][str([1, ["KEY_SPACE"]])] = "CURR_HELP"
|
||||
else:
|
||||
try:
|
||||
self.env["bindings"] = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_binding_backup()
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"HelpManager set_tutorial_mode: Error restoring binding backup: "
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["general"]["tutorialMode"] = True
|
||||
self._install_help_bindings()
|
||||
self._refresh_input_bindings()
|
||||
self.capture_degraded = not self.env["runtime"][
|
||||
"InputManager"
|
||||
].set_help_capture(True)
|
||||
return True
|
||||
|
||||
if not self.env["runtime"]["InputManager"].set_help_capture(False):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"HelpManager could not release exclusive input capture",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
return False
|
||||
self._restore_bindings()
|
||||
self.env["general"]["tutorialMode"] = False
|
||||
self.capture_degraded = False
|
||||
self.env["runtime"]["InputManager"].reset_input_state()
|
||||
self._refresh_input_bindings()
|
||||
return True
|
||||
|
||||
def _install_help_bindings(self):
|
||||
help_bindings = {
|
||||
str([1, ["KEY_ESC"]]): "TOGGLE_TUTORIAL_MODE",
|
||||
str([1, ["KEY_UP"]]): "PREV_HELP",
|
||||
str([1, ["KEY_DOWN"]]): "NEXT_HELP",
|
||||
str([1, ["KEY_LEFT"]]): "PREV_HELP_SECTION",
|
||||
str([1, ["KEY_RIGHT"]]): "NEXT_HELP_SECTION",
|
||||
str([1, ["KEY_SPACE"]]): "CURR_HELP",
|
||||
}
|
||||
self.env["bindings"].update(help_bindings)
|
||||
for shortcut in help_bindings:
|
||||
self.env["rawBindings"][shortcut] = ast.literal_eval(shortcut)
|
||||
|
||||
def _restore_bindings(self):
|
||||
if self.bindings_backup is not None:
|
||||
self.env["bindings"] = self.bindings_backup
|
||||
if self.raw_bindings_backup is not None:
|
||||
self.env["rawBindings"] = self.raw_bindings_backup
|
||||
self.bindings_backup = None
|
||||
self.raw_bindings_backup = None
|
||||
|
||||
def _refresh_input_bindings(self):
|
||||
try:
|
||||
refresh_grabs = getattr(
|
||||
self.env["runtime"]["InputDriver"], "refresh_grabs", None
|
||||
)
|
||||
if refresh_grabs:
|
||||
refresh_grabs(force=True)
|
||||
except Exception as error:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"HelpManager could not refresh input bindings: "
|
||||
+ str(error),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
|
||||
def is_tutorial_mode(self):
|
||||
if self.env is None:
|
||||
return False
|
||||
return self.env["general"]["tutorialMode"]
|
||||
|
||||
def get_formatted_shortcut_for_command(self, command):
|
||||
shortcut = []
|
||||
raw_shortcut = []
|
||||
try:
|
||||
raw_shortcut = list(self.env["bindings"].keys())[
|
||||
list(self.env["bindings"].values()).index(command)
|
||||
]
|
||||
raw_shortcut = self.env["rawBindings"][raw_shortcut]
|
||||
# prefer numbers for multitap
|
||||
if raw_shortcut[0] in range(2, 9):
|
||||
formatted_key = str(raw_shortcut[0]) + " times "
|
||||
shortcut.append(formatted_key)
|
||||
# prefer metha keys
|
||||
for k in [
|
||||
"KEY_FENRIR",
|
||||
"KEY_SCRIPT",
|
||||
"KEY_CTRL",
|
||||
"KEY_SHIFT",
|
||||
"KEY_ALT",
|
||||
"KEY_META",
|
||||
]:
|
||||
if k in raw_shortcut[1]:
|
||||
formatted_key = k
|
||||
formatted_key = formatted_key.lower()
|
||||
formatted_key = formatted_key.replace("key_kp", " keypad ")
|
||||
formatted_key = formatted_key.replace("key_", " ")
|
||||
shortcut.append(formatted_key)
|
||||
raw_shortcut[1].remove(k)
|
||||
# handle other keys
|
||||
for k in raw_shortcut[1]:
|
||||
formatted_key = k
|
||||
formatted_key = formatted_key.lower()
|
||||
formatted_key = formatted_key.replace("key_kp", " keypad ")
|
||||
formatted_key = formatted_key.replace("key_", " ")
|
||||
shortcut.append(formatted_key)
|
||||
except Exception as e:
|
||||
return ""
|
||||
shortcut = str(shortcut)
|
||||
shortcut = shortcut.replace("[", "")
|
||||
shortcut = shortcut.replace("]", "")
|
||||
shortcut = shortcut.replace("'", "")
|
||||
return shortcut
|
||||
def is_capture_degraded(self):
|
||||
return self.capture_degraded
|
||||
|
||||
def get_command_help_text(self, command, section="commands"):
|
||||
command_name = command.lower()
|
||||
command_name = command_name.split("__-__")[0]
|
||||
command_name = command_name.replace("_", " ")
|
||||
command_name = command_name.replace("_", " ")
|
||||
def _get_shortcuts_by_command(self):
|
||||
bindings = (
|
||||
self.bindings_backup
|
||||
if self.bindings_backup is not None
|
||||
else self.env["bindings"]
|
||||
)
|
||||
raw_bindings = (
|
||||
self.raw_bindings_backup
|
||||
if self.raw_bindings_backup is not None
|
||||
else self.env["rawBindings"]
|
||||
)
|
||||
shortcuts_by_command = {}
|
||||
for shortcut_key, command in bindings.items():
|
||||
raw_shortcut = raw_bindings.get(shortcut_key)
|
||||
if raw_shortcut is None:
|
||||
try:
|
||||
raw_shortcut = ast.literal_eval(shortcut_key)
|
||||
except (SyntaxError, ValueError):
|
||||
continue
|
||||
shortcuts_by_command.setdefault(command, []).append(
|
||||
copy.deepcopy(raw_shortcut)
|
||||
)
|
||||
return shortcuts_by_command
|
||||
|
||||
def _format_shortcut(self, raw_shortcut):
|
||||
shortcut_repeat, raw_keys = raw_shortcut
|
||||
keys = list(raw_keys)
|
||||
formatted_keys = []
|
||||
repeat_prefix = ""
|
||||
if shortcut_repeat in range(2, 9):
|
||||
repeat_prefix = _("{} times ").format(shortcut_repeat)
|
||||
for key_name in [
|
||||
"KEY_FENRIR",
|
||||
"KEY_SCRIPT",
|
||||
"KEY_CTRL",
|
||||
"KEY_SHIFT",
|
||||
"KEY_ALT",
|
||||
"KEY_META",
|
||||
]:
|
||||
if key_name in keys:
|
||||
formatted_keys.append(self._format_key_name(key_name))
|
||||
keys.remove(key_name)
|
||||
formatted_keys.extend(self._format_key_name(key) for key in keys)
|
||||
return repeat_prefix + ", ".join(formatted_keys)
|
||||
|
||||
def _format_key_name(self, key_name):
|
||||
formatted_key = key_name.lower()
|
||||
formatted_key = formatted_key.replace("key_kp", "keypad ")
|
||||
formatted_key = formatted_key.replace("key_", "")
|
||||
return _(formatted_key.strip())
|
||||
|
||||
def get_formatted_shortcut_for_command(self, command):
|
||||
shortcuts = self._get_shortcuts_by_command().get(command, [])
|
||||
return "; ".join(
|
||||
self._format_shortcut(shortcut) for shortcut in shortcuts
|
||||
)
|
||||
|
||||
def get_command_help_text(self, command, shortcuts=None):
|
||||
command_name = command.lower().split("__-__")[0].replace("_", " ")
|
||||
if command == "TOGGLE_TUTORIAL_MODE":
|
||||
command_description = _("toggles the tutorial mode")
|
||||
else:
|
||||
@@ -104,45 +186,96 @@ class HelpManager:
|
||||
"CommandManager"
|
||||
].get_command_description(command, section="commands")
|
||||
if command_description == "":
|
||||
command_description = "no Description available"
|
||||
command_shortcut = self.get_formatted_shortcut_for_command(command)
|
||||
if command_shortcut == "":
|
||||
command_shortcut = "unbound"
|
||||
helptext = (
|
||||
command_name
|
||||
+ ", Shortcut "
|
||||
+ command_shortcut
|
||||
+ ", Description "
|
||||
+ command_description
|
||||
command_description = _("no description available")
|
||||
if shortcuts is None:
|
||||
shortcuts = self._get_shortcuts_by_command().get(command, [])
|
||||
command_shortcuts = "; ".join(
|
||||
self._format_shortcut(shortcut) for shortcut in shortcuts
|
||||
)
|
||||
if command_shortcuts == "":
|
||||
command_shortcuts = _("unbound")
|
||||
return _(
|
||||
"{command_name}, Shortcuts {command_shortcuts}, Description "
|
||||
"{command_description}"
|
||||
).format(
|
||||
command_name=command_name,
|
||||
command_shortcuts=command_shortcuts,
|
||||
command_description=command_description,
|
||||
)
|
||||
return helptext
|
||||
|
||||
def create_help_dict(self, section="commands"):
|
||||
self.helpDict = {}
|
||||
for command in sorted(self.env["commands"][section].keys()):
|
||||
self.helpDict[len(self.helpDict)] = self.get_command_help_text(
|
||||
command, section
|
||||
)
|
||||
if len(self.helpDict) > 0:
|
||||
self.tutorialListIndex = 0
|
||||
else:
|
||||
self.tutorialListIndex = None
|
||||
shortcuts_by_command = self._get_shortcuts_by_command()
|
||||
self.help_lists = {
|
||||
self.ACTIVE_SECTION: [],
|
||||
self.UNBOUND_SECTION: [],
|
||||
self.PLUGINS_SECTION: [],
|
||||
}
|
||||
for command in sorted(self.env["commands"][section]):
|
||||
command_instance = self.env["commands"][section][command]
|
||||
if not getattr(command_instance, "help_visible", True):
|
||||
continue
|
||||
shortcuts = shortcuts_by_command.get(command, [])
|
||||
help_text = self.get_command_help_text(command, shortcuts)
|
||||
if any("KEY_SCRIPT" in shortcut[1] for shortcut in shortcuts):
|
||||
target_section = self.PLUGINS_SECTION
|
||||
elif shortcuts:
|
||||
target_section = self.ACTIVE_SECTION
|
||||
else:
|
||||
target_section = self.UNBOUND_SECTION
|
||||
self.help_lists[target_section].append(help_text)
|
||||
self.help_section = self.ACTIVE_SECTION
|
||||
self.tutorial_list_index = None
|
||||
|
||||
def get_help_section_name(self):
|
||||
if self.help_section == self.PLUGINS_SECTION:
|
||||
return _("Plugins")
|
||||
if self.help_section == self.UNBOUND_SECTION:
|
||||
return _("Unbound actions")
|
||||
return _("Active actions")
|
||||
|
||||
def select_help_section(self, section):
|
||||
if section not in self.help_lists:
|
||||
return self.get_help_section_name()
|
||||
self.help_section = section
|
||||
self.tutorial_list_index = None
|
||||
return self.get_help_section_name()
|
||||
|
||||
def next_help_section(self):
|
||||
section_index = self.HELP_SECTIONS.index(self.help_section)
|
||||
section_index = (section_index + 1) % len(self.HELP_SECTIONS)
|
||||
return self.select_help_section(self.HELP_SECTIONS[section_index])
|
||||
|
||||
def prev_help_section(self):
|
||||
section_index = self.HELP_SECTIONS.index(self.help_section)
|
||||
section_index = (section_index - 1) % len(self.HELP_SECTIONS)
|
||||
return self.select_help_section(self.HELP_SECTIONS[section_index])
|
||||
|
||||
def get_help_for_current_index(self):
|
||||
if self.tutorialListIndex is None:
|
||||
return ""
|
||||
return self.helpDict[self.tutorialListIndex]
|
||||
entries = self.help_lists[self.help_section]
|
||||
if self.tutorial_list_index is None or not entries:
|
||||
return self.get_help_section_name()
|
||||
return entries[self.tutorial_list_index]
|
||||
|
||||
def next_index(self):
|
||||
if self.tutorialListIndex is None:
|
||||
entries = self.help_lists[self.help_section]
|
||||
if not entries:
|
||||
self.tutorial_list_index = None
|
||||
return
|
||||
self.tutorialListIndex += 1
|
||||
if self.tutorialListIndex >= len(self.helpDict):
|
||||
self.tutorialListIndex = 0
|
||||
if self.tutorial_list_index is None:
|
||||
self.tutorial_list_index = 0
|
||||
return
|
||||
self.tutorial_list_index = (self.tutorial_list_index + 1) % len(
|
||||
entries
|
||||
)
|
||||
|
||||
def prev_index(self):
|
||||
if self.tutorialListIndex is None:
|
||||
entries = self.help_lists[self.help_section]
|
||||
if not entries:
|
||||
self.tutorial_list_index = None
|
||||
return
|
||||
self.tutorialListIndex -= 1
|
||||
if self.tutorialListIndex < 0:
|
||||
self.tutorialListIndex = len(self.helpDict) - 1
|
||||
if self.tutorial_list_index is None:
|
||||
self.tutorial_list_index = len(entries) - 1
|
||||
return
|
||||
self.tutorial_list_index = (self.tutorial_list_index - 1) % len(
|
||||
entries
|
||||
)
|
||||
|
||||
@@ -54,6 +54,9 @@ class InputDriver:
|
||||
return True
|
||||
return True
|
||||
|
||||
def set_help_capture(self, enabled):
|
||||
return True
|
||||
|
||||
def force_ungrab(self):
|
||||
"""Emergency method to release grabbed devices in case of failure"""
|
||||
if not self._initialized:
|
||||
|
||||
@@ -277,6 +277,18 @@ class InputManager:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_help_capture(self, enabled):
|
||||
try:
|
||||
return self.env["runtime"]["InputDriver"].set_help_capture(
|
||||
enabled
|
||||
)
|
||||
except Exception as error:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"InputManager could not change help capture: " + str(error),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
return not enabled
|
||||
|
||||
def handle_plug_input_device(self, event_data):
|
||||
for deviceEntry in event_data:
|
||||
self.update_input_devices(deviceEntry["device"])
|
||||
@@ -576,6 +588,9 @@ class InputManager:
|
||||
self.env["bindings"][
|
||||
str([1, ["KEY_F1", "KEY_FENRIR"]])
|
||||
] = "TOGGLE_TUTORIAL_MODE"
|
||||
self.env["rawBindings"][
|
||||
str([1, ["KEY_F1", "KEY_FENRIR"]])
|
||||
] = [1, ["KEY_F1", "KEY_FENRIR"]]
|
||||
|
||||
def is_valid_key(self, key):
|
||||
return key in inputData.key_names
|
||||
|
||||
@@ -61,20 +61,31 @@ class driver(inputDriver):
|
||||
"Escape": "KEY_ESC",
|
||||
"space": "KEY_SPACE",
|
||||
"minus": "KEY_MINUS",
|
||||
"-": "KEY_MINUS",
|
||||
"underscore": "KEY_MINUS",
|
||||
"_": "KEY_MINUS",
|
||||
"equal": "KEY_EQUAL",
|
||||
"=": "KEY_EQUAL",
|
||||
"plus": "KEY_EQUAL",
|
||||
"+": "KEY_EQUAL",
|
||||
"bracketleft": "KEY_LEFTBRACE",
|
||||
"[": "KEY_LEFTBRACE",
|
||||
"bracketright": "KEY_RIGHTBRACE",
|
||||
"]": "KEY_RIGHTBRACE",
|
||||
"backslash": "KEY_BACKSLASH",
|
||||
"\\": "KEY_BACKSLASH",
|
||||
"semicolon": "KEY_SEMICOLON",
|
||||
";": "KEY_SEMICOLON",
|
||||
"apostrophe": "KEY_APOSTROPHE",
|
||||
"'": "KEY_APOSTROPHE",
|
||||
"grave": "KEY_GRAVE",
|
||||
"`": "KEY_GRAVE",
|
||||
"comma": "KEY_COMMA",
|
||||
",": "KEY_COMMA",
|
||||
"period": "KEY_DOT",
|
||||
".": "KEY_DOT",
|
||||
"slash": "KEY_SLASH",
|
||||
"/": "KEY_SLASH",
|
||||
"Shift_L": "KEY_LEFTSHIFT",
|
||||
"Shift_R": "KEY_RIGHTSHIFT",
|
||||
"Control_L": "KEY_LEFTCTRL",
|
||||
@@ -173,6 +184,7 @@ class driver(inputDriver):
|
||||
self.command_key_active = False
|
||||
self.chord_modifiers = set()
|
||||
self.chord_keys = set()
|
||||
self.help_keyboard_grabbed = False
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
@@ -214,6 +226,7 @@ class driver(inputDriver):
|
||||
self._initialized = True
|
||||
|
||||
def shutdown(self):
|
||||
self.set_help_capture(False)
|
||||
self.ungrab_all_devices()
|
||||
try:
|
||||
if self.display:
|
||||
@@ -861,6 +874,54 @@ class driver(inputDriver):
|
||||
pass
|
||||
return True
|
||||
|
||||
def set_help_capture(self, enabled):
|
||||
if not self._initialized or not self.display or not self.window:
|
||||
return not enabled
|
||||
if enabled:
|
||||
if self.help_keyboard_grabbed:
|
||||
return True
|
||||
try:
|
||||
grab_status = self.window.grab_keyboard(
|
||||
False,
|
||||
X.GrabModeAsync,
|
||||
X.GrabModeAsync,
|
||||
X.CurrentTime,
|
||||
)
|
||||
self.display.flush()
|
||||
self.help_keyboard_grabbed = grab_status == X.GrabSuccess
|
||||
if not self.help_keyboard_grabbed:
|
||||
self.write_debug(
|
||||
"x11Driver help keyboard grab failed with status "
|
||||
+ str(grab_status),
|
||||
debug.DebugLevel.WARNING,
|
||||
)
|
||||
return self.help_keyboard_grabbed
|
||||
except Exception as error:
|
||||
self.help_keyboard_grabbed = False
|
||||
self.write_debug(
|
||||
"x11Driver help keyboard grab failed: " + str(error),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
return False
|
||||
if self.help_keyboard_grabbed:
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
self.display.ungrab_keyboard(X.CurrentTime)
|
||||
self.display.sync()
|
||||
break
|
||||
except Exception as error:
|
||||
self.write_debug(
|
||||
"x11Driver help keyboard ungrab attempt "
|
||||
+ str(attempt)
|
||||
+ " failed: "
|
||||
+ str(error),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
else:
|
||||
return False
|
||||
self.help_keyboard_grabbed = False
|
||||
return True
|
||||
|
||||
def remove_all_devices(self):
|
||||
self.ungrab_all_devices()
|
||||
|
||||
|
||||
@@ -101,6 +101,98 @@ def test_bare_key_taps_advance_shortcut_repeat(monkeypatch):
|
||||
assert manager.env["input"]["shortcut_repeat"] == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_toggle_runs_while_tutorial_mode_is_active():
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
command_manager = Mock()
|
||||
command_manager.command_exists.return_value = False
|
||||
manager.environment = {
|
||||
"runtime": {
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"CommandManager": command_manager,
|
||||
"ReadAllManager": Mock(is_active=Mock(return_value=False)),
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
|
||||
}
|
||||
}
|
||||
|
||||
manager.handle_execute_command({"data": "TOGGLE_TUTORIAL_MODE"})
|
||||
|
||||
command_manager.run_command.assert_called_once_with(
|
||||
"TOGGLE_TUTORIAL_MODE", "commands"
|
||||
)
|
||||
command_manager.execute_command.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_mode_swallows_input_on_ignored_screen():
|
||||
manager, input_manager = create_handle_input_manager(
|
||||
no_key_pressed=False
|
||||
)
|
||||
manager.environment["runtime"]["HelpManager"].is_tutorial_mode.return_value = (
|
||||
True
|
||||
)
|
||||
manager.environment["runtime"]["ScreenManager"].is_ignored_screen.return_value = (
|
||||
True
|
||||
)
|
||||
|
||||
manager.handle_input(
|
||||
{"data": {"event_name": "KEY_A", "event_state": 1}}
|
||||
)
|
||||
|
||||
input_manager.clear_event_buffer.assert_called()
|
||||
input_manager.write_event_buffer.assert_not_called()
|
||||
input_manager.key_echo.assert_called_once_with(
|
||||
{"event_name": "KEY_A", "event_state": 1}
|
||||
)
|
||||
manager.detect_shortcut_command.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tutorial_mode_promotes_non_fenrir_chord_to_command():
|
||||
manager = FenrirManager.__new__(FenrirManager)
|
||||
manager.modifierInput = False
|
||||
manager.modifier_prefix_input = False
|
||||
manager.singleKeyCommand = False
|
||||
manager.command = ""
|
||||
input_manager = Mock(
|
||||
is_key_press=Mock(return_value=False),
|
||||
curr_input_has_command_modifier=Mock(return_value=False),
|
||||
curr_input_is_modifier_prefix=Mock(return_value=False),
|
||||
no_key_pressed=Mock(return_value=False),
|
||||
get_curr_shortcut=Mock(
|
||||
return_value=str([1, ["KEY_CTRL", "KEY_S"]])
|
||||
),
|
||||
get_command_for_shortcut=Mock(return_value="SPELL_CHECK"),
|
||||
)
|
||||
event_manager = Mock()
|
||||
manager.environment = {
|
||||
"input": {
|
||||
"key_forward": 0,
|
||||
"prev_input": ["KEY_CTRL"],
|
||||
"curr_input": ["KEY_CTRL", "KEY_S"],
|
||||
},
|
||||
"runtime": {
|
||||
"InputManager": input_manager,
|
||||
"EventManager": event_manager,
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
|
||||
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
|
||||
},
|
||||
}
|
||||
|
||||
manager.detect_shortcut_command()
|
||||
|
||||
event_manager.put_to_event_queue.assert_called_once_with(
|
||||
FenrirEventType.execute_command, "SPELL_CHECK"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_bare_key_taps_resolve_progressive_shortcuts(monkeypatch):
|
||||
manager = create_input_manager()
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import ast
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.core.commandManager import CommandManager
|
||||
from fenrirscreenreader.core.helpManager import HelpManager
|
||||
|
||||
|
||||
def create_help_manager(capture_succeeds=True):
|
||||
manager = HelpManager()
|
||||
bindings = {
|
||||
str([1, ["KEY_F1", "KEY_FENRIR"]]): "TOGGLE_TUTORIAL_MODE",
|
||||
str([1, ["KEY_FENRIR", "KEY_S"]]): "SPELL_CHECK",
|
||||
str([1, ["KEY_FENRIR", "KEY_W"]]): "READ_WORD",
|
||||
str([2, ["KEY_FENRIR", "KEY_W"]]): "READ_WORD",
|
||||
str([1, ["KEY_SCRIPT", "KEY_U"]]): (
|
||||
"MUSICPLAYER__-__KEY_U"
|
||||
),
|
||||
}
|
||||
raw_bindings = {
|
||||
shortcut: ast.literal_eval(shortcut) for shortcut in bindings
|
||||
}
|
||||
command_manager = Mock()
|
||||
descriptions = {
|
||||
"SPELL_CHECK": "spell check",
|
||||
"READ_WORD": "read current word",
|
||||
"UNBOUND_ACTION": "an action without a shortcut",
|
||||
"MUSICPLAYER__-__KEY_U": "music player plugin",
|
||||
}
|
||||
command_manager.get_command_description.side_effect = (
|
||||
lambda command, section="commands": descriptions.get(command, "")
|
||||
)
|
||||
input_manager = Mock()
|
||||
input_manager.set_help_capture.return_value = capture_succeeds
|
||||
internal_command = Mock()
|
||||
internal_command.help_visible = False
|
||||
plugin_command = Mock()
|
||||
plugin_command.help_category = "plugins"
|
||||
manager.initialize(
|
||||
{
|
||||
"general": {"tutorialMode": False},
|
||||
"bindings": bindings.copy(),
|
||||
"rawBindings": {
|
||||
shortcut: [raw[0], raw[1].copy()]
|
||||
for shortcut, raw in raw_bindings.items()
|
||||
},
|
||||
"commands": {
|
||||
"commands": {
|
||||
"READ_WORD": Mock(),
|
||||
"SPELL_CHECK": Mock(),
|
||||
"TOGGLE_TUTORIAL_MODE": Mock(),
|
||||
"UNBOUND_ACTION": Mock(),
|
||||
"00_INIT_COMMANDS": internal_command,
|
||||
"MUSICPLAYER__-__KEY_U": plugin_command,
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"CommandManager": command_manager,
|
||||
"DebugManager": Mock(),
|
||||
"InputDriver": Mock(refresh_grabs=Mock()),
|
||||
"InputManager": input_manager,
|
||||
"VmenuManager": Mock(get_active=Mock(return_value=False)),
|
||||
},
|
||||
}
|
||||
)
|
||||
return manager, bindings, raw_bindings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_lists_bound_actions_once_with_all_shortcuts():
|
||||
manager, _bindings, raw_bindings = create_help_manager()
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
active_text = " ".join(manager.help_lists["active"])
|
||||
assert active_text.count("read word") == 1
|
||||
assert "fenrir, w" in active_text
|
||||
assert "2 times fenrir, w" in active_text
|
||||
assert "unbound action" not in active_text
|
||||
assert "musicplayer" not in active_text
|
||||
assert "00 init commands" not in active_text
|
||||
assert all(
|
||||
"00 init commands" not in entry
|
||||
for entry in manager.help_lists["unbound"]
|
||||
)
|
||||
assert manager.help_lists["unbound"] == [
|
||||
"unbound action, Shortcuts unbound, Description an action without a shortcut"
|
||||
]
|
||||
assert manager.help_lists["plugins"] == [
|
||||
"musicplayer, Shortcuts script, u, Description music player plugin"
|
||||
]
|
||||
assert raw_bindings[str([1, ["KEY_FENRIR", "KEY_S"]])] == [
|
||||
1,
|
||||
["KEY_FENRIR", "KEY_S"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_navigation_defaults_to_active_and_starts_before_first_item():
|
||||
manager, _bindings, _raw_bindings = create_help_manager()
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert manager.get_help_section_name() == "Active actions"
|
||||
assert manager.get_help_for_current_index() == "Active actions"
|
||||
|
||||
manager.next_index()
|
||||
assert manager.get_help_for_current_index() == manager.help_lists["active"][0]
|
||||
|
||||
assert manager.select_help_section("unbound") == "Unbound actions"
|
||||
assert manager.get_help_for_current_index() == "Unbound actions"
|
||||
manager.prev_index()
|
||||
assert manager.get_help_for_current_index() == manager.help_lists["unbound"][-1]
|
||||
|
||||
assert manager.next_help_section() == "Plugins"
|
||||
assert manager.next_help_section() == "Active actions"
|
||||
assert manager.prev_help_section() == "Plugins"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_installs_and_restores_modal_bindings_exactly():
|
||||
manager, bindings, raw_bindings = create_help_manager()
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert (
|
||||
manager.env["bindings"][str([1, ["KEY_LEFT"]])]
|
||||
== "PREV_HELP_SECTION"
|
||||
)
|
||||
assert (
|
||||
manager.env["bindings"][str([1, ["KEY_RIGHT"]])]
|
||||
== "NEXT_HELP_SECTION"
|
||||
)
|
||||
assert manager.env["bindings"][str([1, ["KEY_ESC"]])] == "TOGGLE_TUTORIAL_MODE"
|
||||
assert manager.env["rawBindings"][str([1, ["KEY_RIGHT"]])] == [
|
||||
1,
|
||||
["KEY_RIGHT"],
|
||||
]
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.assert_called_once_with(
|
||||
True
|
||||
)
|
||||
|
||||
manager.set_tutorial_mode(False)
|
||||
|
||||
assert manager.env["bindings"] == bindings
|
||||
assert manager.env["rawBindings"] == raw_bindings
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.assert_called_with(
|
||||
False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_records_degraded_capture_without_refusing_mode():
|
||||
manager, _bindings, _raw_bindings = create_help_manager(
|
||||
capture_succeeds=False
|
||||
)
|
||||
|
||||
manager.set_tutorial_mode(True)
|
||||
|
||||
assert manager.is_tutorial_mode() is True
|
||||
assert manager.is_capture_degraded() is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_help_does_not_claim_exit_when_capture_release_fails():
|
||||
manager, bindings, raw_bindings = create_help_manager()
|
||||
manager.set_tutorial_mode(True)
|
||||
manager.env["runtime"]["InputManager"].set_help_capture.return_value = False
|
||||
|
||||
assert manager.set_tutorial_mode(False) is False
|
||||
|
||||
assert manager.is_tutorial_mode() is True
|
||||
assert manager.env["bindings"] != bindings
|
||||
assert manager.env["rawBindings"] != raw_bindings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_command_description_is_presented_on_ignored_screen_in_help():
|
||||
command_manager = CommandManager()
|
||||
output_manager = Mock()
|
||||
command_manager.env = {
|
||||
"runtime": {
|
||||
"ScreenManager": Mock(
|
||||
is_ignored_screen=Mock(return_value=True)
|
||||
),
|
||||
"HelpManager": Mock(
|
||||
is_tutorial_mode=Mock(return_value=True)
|
||||
),
|
||||
"DebugManager": Mock(),
|
||||
"OutputManager": output_manager,
|
||||
}
|
||||
}
|
||||
command_manager.command_exists = Mock(return_value=True)
|
||||
command_manager.get_command_description = Mock(
|
||||
return_value="spell check"
|
||||
)
|
||||
|
||||
command_manager.execute_command("SPELL_CHECK", "commands")
|
||||
|
||||
output_manager.present_text.assert_called_once_with(
|
||||
"spell check", interrupt=False
|
||||
)
|
||||
@@ -126,6 +126,14 @@ def test_progressive_review_bindings(layout, character_keys, word_keys):
|
||||
assert bindings[str([2, word_keys])] == "REVIEW_CURR_WORD_SPELL"
|
||||
assert bindings[str([3, word_keys])] == "REVIEW_CURR_WORD_PHONETIC"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("layout", ["desktop", "laptop"])
|
||||
def test_layouts_do_not_bind_removed_keyboard_layout_cycle(layout):
|
||||
bindings = load_bindings(layout)
|
||||
|
||||
assert "CYCLE_KEYBOARD_LAYOUT" not in bindings.values()
|
||||
|
||||
removed_commands = {
|
||||
"REVIEW_PREV_CHAR_PHONETIC",
|
||||
"REVIEW_NEXT_CHAR_PHONETIC",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
COMMAND_PATH = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src"
|
||||
/ "fenrirscreenreader"
|
||||
/ "commands"
|
||||
/ "commands"
|
||||
/ "toggle_vmenu_mode.py"
|
||||
)
|
||||
|
||||
|
||||
def load_command():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"fenrir_toggle_vmenu_mode", COMMAND_PATH
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.command()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_get_description_does_not_toggle_vmenu_mode():
|
||||
vmenu_manager = Mock()
|
||||
command = load_command()
|
||||
command.initialize({"runtime": {"VmenuManager": vmenu_manager}})
|
||||
|
||||
assert command.get_description() == "Entering or Leaving v menu mode."
|
||||
vmenu_manager.toggle_vmenu_mode.assert_not_called()
|
||||
@@ -189,6 +189,29 @@ def test_x11_should_emit_unbound_mapped_keys_for_speech_interrupt():
|
||||
assert x11.should_emit_key("KEY_ENTER") is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
("keysym_name", "key_name"),
|
||||
[
|
||||
("-", "KEY_MINUS"),
|
||||
("=", "KEY_EQUAL"),
|
||||
("[", "KEY_LEFTBRACE"),
|
||||
("]", "KEY_RIGHTBRACE"),
|
||||
("\\", "KEY_BACKSLASH"),
|
||||
(";", "KEY_SEMICOLON"),
|
||||
("'", "KEY_APOSTROPHE"),
|
||||
("`", "KEY_GRAVE"),
|
||||
(",", "KEY_COMMA"),
|
||||
(".", "KEY_DOT"),
|
||||
("/", "KEY_SLASH"),
|
||||
],
|
||||
)
|
||||
def test_x11_maps_literal_punctuation_keysyms(keysym_name, key_name):
|
||||
x11 = X11Driver()
|
||||
|
||||
assert x11.keysym_name_to_key_name(keysym_name) == key_name
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
|
||||
x11 = X11Driver()
|
||||
@@ -372,6 +395,58 @@ def test_x11_write_event_buffer_does_not_replay_key_release():
|
||||
assert x11.env["input"]["event_buffer"] == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_grabs_and_releases_keyboard():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.window = Mock()
|
||||
x11.window.grab_keyboard.return_value = X.GrabSuccess
|
||||
x11.display = Mock()
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(True) is True
|
||||
x11.window.grab_keyboard.assert_called_once_with(
|
||||
False,
|
||||
X.GrabModeAsync,
|
||||
X.GrabModeAsync,
|
||||
X.CurrentTime,
|
||||
)
|
||||
assert x11.help_keyboard_grabbed is True
|
||||
|
||||
assert x11.set_help_capture(False) is True
|
||||
x11.display.ungrab_keyboard.assert_called_once_with(X.CurrentTime)
|
||||
x11.display.sync.assert_called_once_with()
|
||||
assert x11.help_keyboard_grabbed is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_reports_failed_grab():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.window = Mock()
|
||||
x11.window.grab_keyboard.return_value = X.AlreadyGrabbed
|
||||
x11.display = Mock()
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(True) is False
|
||||
assert x11.help_keyboard_grabbed is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_help_capture_retries_failed_keyboard_release():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.help_keyboard_grabbed = True
|
||||
x11.window = Mock()
|
||||
x11.display = Mock()
|
||||
x11.display.sync.side_effect = RuntimeError("ungrab failed")
|
||||
x11.env = {"runtime": {"DebugManager": Mock()}}
|
||||
|
||||
assert x11.set_help_capture(False) is False
|
||||
assert x11.display.ungrab_keyboard.call_count == 3
|
||||
assert x11.help_keyboard_grabbed is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_map_event_keeps_x_event_time_for_replay():
|
||||
x11 = X11Driver()
|
||||
|
||||
+53
-5
@@ -882,12 +882,24 @@ msgstr ""
|
||||
msgid "speech enabled"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:18
|
||||
msgid "Exiting tutorial mode. To enter tutorial mode again press Fenrir+f1"
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:23
|
||||
msgid "enter or leave tutorial mode"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:22
|
||||
msgid "Entering tutorial mode. In this mode commands are described but not executed. You can move through the list of commands with the up and down arrow keys. To Exit tutorial mode press Fenrir+f1."
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:28
|
||||
msgid "Entering tutorial mode. In this mode commands are described but not executed. Use up and down to browse actions and left and right to switch between Active actions, Unbound actions, and Plugins. Press Space to repeat the current item. To exit tutorial mode press Fenrir+F1 or Escape. Active actions."
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:32
|
||||
msgid "Unable to exit tutorial mode because exclusive keyboard capture could not be released. Press Fenrir+F1 or Escape to try again."
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:38
|
||||
msgid "Warning: full keyboard capture is unavailable. Unbound keys may reach the active application."
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_tutorial_mode.py:46
|
||||
msgid "Exiting tutorial mode. To enter tutorial mode again press Fenrir+F1"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\commands\toggle_vmenu_mode.py:18
|
||||
@@ -906,6 +918,10 @@ msgstr ""
|
||||
msgid "get current help message"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\help\next_help_section.py:21
|
||||
msgid "show next help category"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\help\next_help.py:17
|
||||
msgid "get next help message"
|
||||
msgstr ""
|
||||
@@ -914,6 +930,10 @@ msgstr ""
|
||||
msgid "get prev help message"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\commands\help\prev_help_section.py:21
|
||||
msgid "show previous help category"
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
#: ../src/fenrirscreenreader\commands\onCursorChange\65000-present_line_if_cursor_change_vertical.py:46
|
||||
msgid "indented "
|
||||
@@ -1076,10 +1096,38 @@ msgstr ""
|
||||
msgid "Quit Fenrir"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:77
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:132
|
||||
msgid "{} times "
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:161
|
||||
msgid "toggles the tutorial mode"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:168
|
||||
msgid "no description available"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:175
|
||||
msgid "unbound"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:176
|
||||
msgid "{command_name}, Shortcuts {command_shortcuts}, Description {command_description}"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:203
|
||||
msgid "Plugins"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:205
|
||||
msgid "Unbound actions"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\helpManager.py:206
|
||||
msgid "Active actions"
|
||||
msgstr ""
|
||||
|
||||
#: ../src/fenrirscreenreader\core\outputManager.py:297
|
||||
msgid "speech temporary disabled"
|
||||
msgstr ""
|
||||
|
||||
Reference in New Issue
Block a user