Expirmental improvements to highlight tracking.
This commit is contained in:
@ -24,6 +24,27 @@ class command:
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# Check if we're in table mode first
|
||||
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
|
||||
if is_table_mode:
|
||||
table_info = self.env["runtime"]["TableManager"].move_to_first_cell()
|
||||
if table_info:
|
||||
output_text = (
|
||||
f"{table_info['cell_content']} {table_info['column_header']}"
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
output_text, interrupt=True, flush=False
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("first cell"), interrupt=False
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("no table data"), interrupt=True, flush=False
|
||||
)
|
||||
return
|
||||
|
||||
# Regular line begin navigation (when not in table mode)
|
||||
cursor_pos = self.env["runtime"][
|
||||
"CursorManager"
|
||||
].get_review_or_text_cursor()
|
||||
|
@ -24,6 +24,27 @@ class command:
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# Check if we're in table mode first
|
||||
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
|
||||
if is_table_mode:
|
||||
table_info = self.env["runtime"]["TableManager"].move_to_last_cell()
|
||||
if table_info:
|
||||
output_text = (
|
||||
f"{table_info['cell_content']} {table_info['column_header']}"
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
output_text, interrupt=True, flush=False
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("last cell"), interrupt=False
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("no table data"), interrupt=True, flush=False
|
||||
)
|
||||
return
|
||||
|
||||
# Regular line end navigation (when not in table mode)
|
||||
cursor_pos = self.env["runtime"][
|
||||
"CursorManager"
|
||||
].get_review_or_text_cursor()
|
||||
|
@ -23,6 +23,29 @@ class command:
|
||||
return _("Move Review to the first character on the line")
|
||||
|
||||
def run(self):
|
||||
# Check if we're in table mode first
|
||||
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
|
||||
if is_table_mode:
|
||||
table_info = self.env["runtime"]["TableManager"].move_to_first_char_in_cell()
|
||||
if table_info:
|
||||
char_utils.present_char_for_review(
|
||||
self.env,
|
||||
table_info['character'],
|
||||
interrupt=True,
|
||||
announce_capital=True,
|
||||
flush=False,
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("first character in cell {0}").format(table_info['column_header']),
|
||||
interrupt=False,
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("no table data"), interrupt=True, flush=False
|
||||
)
|
||||
return
|
||||
|
||||
# Regular line first character navigation (when not in table mode)
|
||||
cursor_pos = self.env["runtime"][
|
||||
"CursorManager"
|
||||
].get_review_or_text_cursor()
|
||||
|
@ -22,6 +22,29 @@ class command:
|
||||
return _("Move Review to the last character on the line")
|
||||
|
||||
def run(self):
|
||||
# Check if we're in table mode first
|
||||
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
|
||||
if is_table_mode:
|
||||
table_info = self.env["runtime"]["TableManager"].move_to_last_char_in_cell()
|
||||
if table_info:
|
||||
char_utils.present_char_for_review(
|
||||
self.env,
|
||||
table_info['character'],
|
||||
interrupt=True,
|
||||
announce_capital=True,
|
||||
flush=False,
|
||||
)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("last character in cell {0}").format(table_info['column_header']),
|
||||
interrupt=False,
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("no table data"), interrupt=True, flush=False
|
||||
)
|
||||
return
|
||||
|
||||
# Regular line last character navigation (when not in table mode)
|
||||
cursor_pos = self.env["runtime"][
|
||||
"CursorManager"
|
||||
].get_review_or_text_cursor()
|
||||
|
@ -322,6 +322,9 @@ class AttributeManager:
|
||||
This is crucial for screen readers to announce when text becomes highlighted,
|
||||
selected, or changes visual emphasis (bold, reverse video, color changes, etc.)
|
||||
|
||||
Enhanced version includes bracket pattern detection for better context in applications
|
||||
like ninjam that use patterns like [X]mute or [ ]mute.
|
||||
|
||||
Returns:
|
||||
tuple: (highlighted_text, cursor_position)
|
||||
- highlighted_text: string of characters that gained highlighting
|
||||
@ -352,6 +355,9 @@ class AttributeManager:
|
||||
if len(text_lines) != len(self.currAttributes):
|
||||
return result, curr_cursor
|
||||
|
||||
# Track highlighted positions for context analysis
|
||||
highlighted_positions = []
|
||||
|
||||
# Compare attributes line by line, character by character
|
||||
for line in range(len(self.prevAttributes)):
|
||||
if self.prevAttributes[line] != self.currAttributes[line]:
|
||||
@ -373,13 +379,316 @@ class AttributeManager:
|
||||
# for navigation
|
||||
if not curr_cursor:
|
||||
curr_cursor = {"x": column, "y": line}
|
||||
# Store position for context analysis
|
||||
highlighted_positions.append((line, column))
|
||||
# Accumulate highlighted characters
|
||||
result += text_lines[line][column]
|
||||
# Add space between lines of highlighted text for speech
|
||||
# clarity
|
||||
result += " "
|
||||
|
||||
# Enhanced bracket pattern detection for better context
|
||||
if highlighted_positions:
|
||||
try:
|
||||
enhanced_result = self._detect_bracket_context(text_lines, highlighted_positions, result)
|
||||
if enhanced_result and enhanced_result != result:
|
||||
# Debug logging for bracket detection
|
||||
try:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"AttributeManager bracket detection: Original='{result}' Enhanced='{enhanced_result}'",
|
||||
debug.DebugLevel.INFO
|
||||
)
|
||||
except Exception:
|
||||
pass # Don't let debug logging break functionality
|
||||
result = enhanced_result
|
||||
except Exception as e:
|
||||
# If bracket detection fails, fall back to original result
|
||||
try:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"AttributeManager bracket detection error: {e}",
|
||||
debug.DebugLevel.ERROR
|
||||
)
|
||||
except Exception:
|
||||
pass # Don't let debug logging break functionality
|
||||
|
||||
return result, curr_cursor
|
||||
|
||||
def _detect_bracket_context(self, text_lines, highlighted_positions, original_result):
|
||||
"""
|
||||
Analyzes highlighted positions to detect bracket patterns and provide better context.
|
||||
|
||||
This method specifically looks for patterns like [X]mute, [ ]mute, [X]xmit, etc.
|
||||
that are common in applications like ninjam where the bracket content indicates
|
||||
state but the meaningful context is the surrounding text.
|
||||
|
||||
Args:
|
||||
text_lines: List of text lines from the screen
|
||||
highlighted_positions: List of (line, column) tuples of highlighted characters
|
||||
original_result: The original highlighted text result
|
||||
|
||||
Returns:
|
||||
str: Enhanced result with context, or None if no bracket pattern detected
|
||||
"""
|
||||
if not highlighted_positions:
|
||||
return None
|
||||
|
||||
# Group positions by line for easier analysis
|
||||
positions_by_line = {}
|
||||
for line, column in highlighted_positions:
|
||||
if line not in positions_by_line:
|
||||
positions_by_line[line] = []
|
||||
positions_by_line[line].append(column)
|
||||
|
||||
enhanced_results = []
|
||||
|
||||
for line_num, columns in positions_by_line.items():
|
||||
if line_num >= len(text_lines):
|
||||
continue
|
||||
|
||||
line_text = text_lines[line_num]
|
||||
columns.sort() # Process columns in order
|
||||
|
||||
# Look for bracket patterns in this line
|
||||
bracket_context = self._analyze_bracket_pattern(line_text, columns)
|
||||
if bracket_context:
|
||||
enhanced_results.append(bracket_context)
|
||||
|
||||
# If we found enhanced context, return it; otherwise return None to use original
|
||||
if enhanced_results:
|
||||
# Remove duplicates while preserving order
|
||||
unique_results = []
|
||||
seen = set()
|
||||
for result in enhanced_results:
|
||||
if result not in seen:
|
||||
unique_results.append(result)
|
||||
seen.add(result)
|
||||
return " ".join(unique_results)
|
||||
return None
|
||||
|
||||
def _analyze_bracket_pattern(self, line_text, highlighted_columns):
|
||||
"""
|
||||
Analyzes a single line for bracket patterns around highlighted positions.
|
||||
|
||||
Looks for patterns like:
|
||||
- master: [X]mute -> "master mute on"
|
||||
- metronome: [ ]mute -> "metronome mute off"
|
||||
- [Left] [X]xmit -> "Left xmit on"
|
||||
|
||||
Args:
|
||||
line_text: The text of the line
|
||||
highlighted_columns: List of column positions that are highlighted
|
||||
|
||||
Returns:
|
||||
str: Context-aware description, or None if no pattern found
|
||||
"""
|
||||
if not line_text or not highlighted_columns:
|
||||
return None
|
||||
|
||||
# Look for bracket patterns around highlighted positions
|
||||
# Process columns in order and only return the first meaningful match
|
||||
for col in highlighted_columns:
|
||||
bracket_info = self._find_bracket_at_position(line_text, col)
|
||||
if bracket_info:
|
||||
bracket_start, bracket_end, bracket_content = bracket_info
|
||||
|
||||
# Get context before and after the bracket
|
||||
context_before = self._get_context_before(line_text, bracket_start)
|
||||
context_after = self._get_context_after(line_text, bracket_end)
|
||||
|
||||
# Build meaningful description
|
||||
description = self._build_bracket_description(
|
||||
context_before, bracket_content, context_after
|
||||
)
|
||||
|
||||
# Only return if we have meaningful context (not just state)
|
||||
if description and (context_before or context_after):
|
||||
return description
|
||||
|
||||
return None
|
||||
|
||||
def _find_bracket_at_position(self, line_text, position):
|
||||
"""
|
||||
Determines if a position is within a bracket pattern like [X] or [ ].
|
||||
|
||||
Args:
|
||||
line_text: The text line
|
||||
position: Column position to check
|
||||
|
||||
Returns:
|
||||
tuple: (bracket_start, bracket_end, bracket_content) or None
|
||||
"""
|
||||
if position >= len(line_text):
|
||||
return None
|
||||
|
||||
# Look for opening bracket before or at position
|
||||
bracket_start = None
|
||||
for i in range(max(0, position - 2), min(len(line_text), position + 3)):
|
||||
if line_text[i] == '[':
|
||||
bracket_start = i
|
||||
break
|
||||
|
||||
if bracket_start is None:
|
||||
return None
|
||||
|
||||
# Look for closing bracket after bracket_start
|
||||
bracket_end = None
|
||||
for i in range(bracket_start + 1, min(len(line_text), bracket_start + 5)):
|
||||
if line_text[i] == ']':
|
||||
bracket_end = i
|
||||
break
|
||||
|
||||
if bracket_end is None:
|
||||
return None
|
||||
|
||||
# Check if our position is within the bracket
|
||||
if bracket_start <= position <= bracket_end:
|
||||
bracket_content = line_text[bracket_start + 1:bracket_end]
|
||||
|
||||
# Filter out brackets that are likely not controls
|
||||
# Skip brackets that contain complex content like "[-10.5dB center]"
|
||||
if len(bracket_content) > 3 and ('dB' in bracket_content or 'Hz' in bracket_content):
|
||||
return None
|
||||
|
||||
return bracket_start, bracket_end, bracket_content
|
||||
|
||||
return None
|
||||
|
||||
def _get_context_before(self, line_text, bracket_start):
|
||||
"""
|
||||
Gets meaningful context words before a bracket.
|
||||
|
||||
Args:
|
||||
line_text: The text line
|
||||
bracket_start: Position of opening bracket
|
||||
|
||||
Returns:
|
||||
str: Context words before bracket, or empty string
|
||||
"""
|
||||
if bracket_start <= 0:
|
||||
return ""
|
||||
|
||||
# Get text before bracket
|
||||
before_text = line_text[:bracket_start].rstrip()
|
||||
|
||||
# Special handling for common ninjam patterns
|
||||
# Look for patterns like "master: " or "metronome: " or bracketed labels
|
||||
|
||||
# Check if we have a colon-separated label immediately before
|
||||
if ':' in before_text:
|
||||
# Get the last colon-separated part
|
||||
parts = before_text.split('|') # Split by pipe first
|
||||
last_part = parts[-1].strip()
|
||||
|
||||
if ':' in last_part:
|
||||
# Extract the label before the colon
|
||||
label_parts = last_part.split(':')
|
||||
if len(label_parts) >= 2:
|
||||
return label_parts[-2].strip()
|
||||
|
||||
# Look for bracketed content that might be a label
|
||||
# Pattern: [Something] [X]target -> "Something"
|
||||
bracket_matches = []
|
||||
i = 0
|
||||
while i < len(before_text):
|
||||
if before_text[i] == '[':
|
||||
start = i
|
||||
i += 1
|
||||
while i < len(before_text) and before_text[i] != ']':
|
||||
i += 1
|
||||
if i < len(before_text): # Found closing bracket
|
||||
content = before_text[start+1:i]
|
||||
if content.strip():
|
||||
bracket_matches.append(content.strip())
|
||||
i += 1
|
||||
|
||||
# If we found bracketed content, use the last one
|
||||
if bracket_matches:
|
||||
return bracket_matches[-1]
|
||||
|
||||
# Fall back to removing bracketed content and getting words
|
||||
cleaned_text = ""
|
||||
bracket_level = 0
|
||||
for char in before_text:
|
||||
if char == '[':
|
||||
bracket_level += 1
|
||||
elif char == ']':
|
||||
bracket_level -= 1
|
||||
elif bracket_level == 0:
|
||||
cleaned_text += char
|
||||
|
||||
# Clean up separators and get meaningful words
|
||||
words = []
|
||||
for word in cleaned_text.replace(':', '').replace('|', '').strip().split():
|
||||
if word and not word.startswith('[') and not word.endswith(']'):
|
||||
words.append(word)
|
||||
|
||||
# Return last word as context
|
||||
if words:
|
||||
return words[-1]
|
||||
|
||||
return ""
|
||||
|
||||
def _get_context_after(self, line_text, bracket_end):
|
||||
"""
|
||||
Gets meaningful context words after a bracket.
|
||||
|
||||
Args:
|
||||
line_text: The text line
|
||||
bracket_end: Position of closing bracket
|
||||
|
||||
Returns:
|
||||
str: Context words after bracket, or empty string
|
||||
"""
|
||||
if bracket_end >= len(line_text) - 1:
|
||||
return ""
|
||||
|
||||
# Get text after bracket and find first meaningful word
|
||||
after_text = line_text[bracket_end + 1:].lstrip()
|
||||
|
||||
# Get first word, removing common separators
|
||||
words = after_text.replace(':', '').replace('|', '').strip().split()
|
||||
|
||||
if words:
|
||||
return words[0]
|
||||
|
||||
return ""
|
||||
|
||||
def _build_bracket_description(self, context_before, bracket_content, context_after):
|
||||
"""
|
||||
Builds a human-readable description from bracket context.
|
||||
|
||||
Args:
|
||||
context_before: Words before the bracket
|
||||
bracket_content: Content inside brackets (X, space, etc.)
|
||||
context_after: Words after the bracket
|
||||
|
||||
Returns:
|
||||
str: Human-readable description
|
||||
"""
|
||||
# Clean up bracket content
|
||||
bracket_content = bracket_content.strip()
|
||||
|
||||
# Determine state based on bracket content
|
||||
if bracket_content == 'X':
|
||||
state = "on"
|
||||
elif bracket_content == '' or bracket_content == ' ':
|
||||
state = "off"
|
||||
else:
|
||||
state = bracket_content # For other patterns
|
||||
|
||||
# Build description prioritizing context_before, then context_after
|
||||
# Add spaces between components for better speech clarity
|
||||
components = []
|
||||
|
||||
if context_before:
|
||||
components.append(context_before)
|
||||
if context_after:
|
||||
components.append(context_after)
|
||||
components.append(state)
|
||||
|
||||
# Join with spaces for better speech flow
|
||||
return " ".join(components)
|
||||
|
||||
def is_useful_for_tracking(
|
||||
self,
|
||||
line,
|
||||
|
@ -382,6 +382,151 @@ class TableManager:
|
||||
# Check if cursor is within the column bounds
|
||||
return column_start <= cursor_x < column_end
|
||||
|
||||
def move_to_first_cell(self):
|
||||
"""Move to first cell in current row"""
|
||||
if not self.env["runtime"]["CursorManager"].is_review_mode():
|
||||
return None
|
||||
|
||||
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
|
||||
if not cursor_pos:
|
||||
return None
|
||||
|
||||
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
|
||||
if not line_text:
|
||||
return None
|
||||
|
||||
columns = self.parse_line_into_columns(line_text)
|
||||
if not columns:
|
||||
return None
|
||||
|
||||
# Set current column to first column
|
||||
self.currentColumn = 0
|
||||
|
||||
# Return info for the first column
|
||||
return self.get_table_cell_info_by_indices(cursor_pos["y"], 0)
|
||||
|
||||
def move_to_last_cell(self):
|
||||
"""Move to last cell in current row"""
|
||||
if not self.env["runtime"]["CursorManager"].is_review_mode():
|
||||
return None
|
||||
|
||||
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
|
||||
if not cursor_pos:
|
||||
return None
|
||||
|
||||
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
|
||||
if not line_text:
|
||||
return None
|
||||
|
||||
columns = self.parse_line_into_columns(line_text)
|
||||
if not columns:
|
||||
return None
|
||||
|
||||
# Set current column to last column
|
||||
self.currentColumn = len(columns) - 1
|
||||
|
||||
# Return info for the last column
|
||||
return self.get_table_cell_info_by_indices(cursor_pos["y"], self.currentColumn)
|
||||
|
||||
def move_to_first_char_in_cell(self):
|
||||
"""Move to first character in current cell"""
|
||||
if not self.env["runtime"]["CursorManager"].is_review_mode():
|
||||
return None
|
||||
|
||||
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
|
||||
if not cursor_pos:
|
||||
return None
|
||||
|
||||
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
|
||||
if not line_text:
|
||||
return None
|
||||
|
||||
columns = self.parse_line_into_columns(line_text)
|
||||
if not columns or self.currentColumn < 0 or self.currentColumn >= len(columns):
|
||||
return None
|
||||
|
||||
# Get column start position
|
||||
column_start = self.get_column_start_position(line_text, self.currentColumn)
|
||||
|
||||
# Find first non-space character in the column
|
||||
column_text = columns[self.currentColumn]
|
||||
first_char_offset = len(column_text) - len(column_text.lstrip())
|
||||
|
||||
# Set cursor position to first character in cell
|
||||
new_x = column_start + first_char_offset
|
||||
self.env["runtime"]["CursorManager"].set_review_cursor_position(new_x, cursor_pos["y"])
|
||||
|
||||
# Get the character at the new position
|
||||
from fenrirscreenreader.utils import char_utils
|
||||
(
|
||||
self.env["screen"]["newCursorReview"]["x"],
|
||||
self.env["screen"]["newCursorReview"]["y"],
|
||||
curr_char,
|
||||
) = char_utils.get_current_char(
|
||||
new_x,
|
||||
cursor_pos["y"],
|
||||
self.env["screen"]["new_content_text"],
|
||||
)
|
||||
|
||||
return {
|
||||
'cell_content': column_text.strip(),
|
||||
'column_header': self.get_column_header(self.currentColumn),
|
||||
'character': curr_char,
|
||||
'position': 'first'
|
||||
}
|
||||
|
||||
def move_to_last_char_in_cell(self):
|
||||
"""Move to last character in current cell"""
|
||||
if not self.env["runtime"]["CursorManager"].is_review_mode():
|
||||
return None
|
||||
|
||||
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
|
||||
if not cursor_pos:
|
||||
return None
|
||||
|
||||
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
|
||||
if not line_text:
|
||||
return None
|
||||
|
||||
columns = self.parse_line_into_columns(line_text)
|
||||
if not columns or self.currentColumn < 0 or self.currentColumn >= len(columns):
|
||||
return None
|
||||
|
||||
# Get column start position
|
||||
column_start = self.get_column_start_position(line_text, self.currentColumn)
|
||||
column_text = columns[self.currentColumn]
|
||||
|
||||
# Find last non-space character in the column
|
||||
trimmed_text = column_text.rstrip()
|
||||
if not trimmed_text:
|
||||
# If empty cell, go to column start
|
||||
new_x = column_start
|
||||
else:
|
||||
# Find the position of the last character
|
||||
new_x = column_start + len(trimmed_text) - 1
|
||||
|
||||
# Set cursor position to last character in cell
|
||||
self.env["runtime"]["CursorManager"].set_review_cursor_position(new_x, cursor_pos["y"])
|
||||
|
||||
# Get the character at the new position
|
||||
from fenrirscreenreader.utils import char_utils
|
||||
(
|
||||
self.env["screen"]["newCursorReview"]["x"],
|
||||
self.env["screen"]["newCursorReview"]["y"],
|
||||
curr_char,
|
||||
) = char_utils.get_current_char(
|
||||
new_x,
|
||||
cursor_pos["y"],
|
||||
self.env["screen"]["new_content_text"],
|
||||
)
|
||||
|
||||
return {
|
||||
'cell_content': column_text.strip(),
|
||||
'column_header': self.get_column_header(self.currentColumn),
|
||||
'character': curr_char,
|
||||
'position': 'last'
|
||||
}
|
||||
|
||||
def reset_table_mode(self):
|
||||
self.set_head_line()
|
||||
|
||||
|
@ -4,6 +4,6 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2025.07.08"
|
||||
version = "2025.07.09"
|
||||
codeName = "testing"
|
||||
code_name = "testing"
|
||||
|
Reference in New Issue
Block a user