771 lines
27 KiB
Python
771 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Fenrir TTY screen reader
|
|
# By Chrys, Storm Dragon, and contributors.
|
|
|
|
from collections import Counter
|
|
|
|
from fenrirscreenreader.core import debug
|
|
from fenrirscreenreader.core.i18n import _
|
|
|
|
|
|
class AttributeManager:
|
|
def __init__(self):
|
|
self.currAttributes = None
|
|
self.prevAttributes = None
|
|
self.currAttributeDelta = ""
|
|
self.currAttributeCursor = None
|
|
self.prefAttributeCursor = None
|
|
self.init_default_attributes()
|
|
self.prevLastCursorAttribute = None
|
|
self.currLastCursorAttribute = None
|
|
|
|
def initialize(self, environment):
|
|
self.env = environment
|
|
|
|
def shutdown(self):
|
|
pass
|
|
|
|
def set_last_cursor_attribute(self, lastCursorAttribute):
|
|
self.prevLastCursorAttribute = self.currLastCursorAttribute
|
|
self.currLastCursorAttribute = lastCursorAttribute
|
|
|
|
def reset_last_cursor_attribute(self):
|
|
self.prevLastCursorAttribute = None
|
|
self.currLastCursorAttribute = None
|
|
|
|
def is_last_cursor_attribute_change(self):
|
|
if self.prevLastCursorAttribute is None:
|
|
return False
|
|
return self.prevLastCursorAttribute != self.currLastCursorAttribute
|
|
|
|
def get_curr_attribute_cursor(self):
|
|
return self.currAttributeCursor
|
|
|
|
def is_attribute_cursor_active(self):
|
|
return self.currAttributeCursor is not None
|
|
|
|
def is_attribute_change(self):
|
|
if not self.prevAttributes:
|
|
return False
|
|
return self.currAttributes != self.prevAttributes
|
|
|
|
def reset_attribute_all(self):
|
|
self.reset_attribute_delta()
|
|
self.reset_attribute_cursor()
|
|
|
|
def get_attribute_delta(self):
|
|
return self.currAttributeDelta
|
|
|
|
def reset_attribute_delta(self):
|
|
self.currAttributeDelta = ""
|
|
|
|
def set_attribute_delta(self, currAttributeDelta):
|
|
self.currAttributeDelta = currAttributeDelta
|
|
|
|
def reset_attribute_cursor(self):
|
|
self.currAttributeCursor = None
|
|
self.prefAttributeCursor = None
|
|
|
|
def set_attribute_cursor(self, currAttributeCursor):
|
|
self.prefAttributeCursor = self.currAttributeCursor
|
|
self.currAttributeCursor = currAttributeCursor.copy()
|
|
|
|
def reset_attributes(self, currAttributes):
|
|
self.prevAttributes = None
|
|
self.currAttributes = currAttributes
|
|
|
|
def set_attributes(self, currAttributes):
|
|
self.prevAttributes = self.currAttributes
|
|
self.currAttributes = currAttributes.copy()
|
|
|
|
def get_attribute_by_xy(self, x, y):
|
|
if not self.currAttributes:
|
|
return None
|
|
if len(self.currAttributes) < y:
|
|
return None
|
|
if len(self.currAttributes[y]) < x - 1:
|
|
return None
|
|
try:
|
|
return self.currAttributes[y][x]
|
|
except KeyError:
|
|
try:
|
|
return self.defaultAttributes[0]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"AttributeManager get_attribute_by_xy: Error accessing default attributes: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
return None
|
|
|
|
def append_default_attributes(self, attribute):
|
|
if not attribute:
|
|
return
|
|
if len(attribute) != 10:
|
|
return
|
|
self.defaultAttributes.append(attribute)
|
|
|
|
def init_default_attributes(self):
|
|
self.defaultAttributes = [None]
|
|
self.defaultAttributes.append(
|
|
[
|
|
"default", # fg
|
|
"default", # bg
|
|
False, # bold
|
|
False, # italics
|
|
False, # underscore
|
|
False, # strikethrough
|
|
False, # reverse
|
|
False, # blink
|
|
"default", # fontsize
|
|
"default", # fontfamily
|
|
]
|
|
) # end attribute
|
|
|
|
def is_default_attribute(self, attribute):
|
|
return attribute in self.defaultAttributes
|
|
|
|
def has_attributes(self, cursor, update=True):
|
|
if not cursor:
|
|
return False
|
|
cursor_pos = cursor.copy()
|
|
try:
|
|
attribute = self.get_attribute_by_xy(
|
|
cursor_pos["x"], cursor_pos["y"]
|
|
)
|
|
|
|
if update:
|
|
self.set_last_cursor_attribute(attribute)
|
|
if not self.is_last_cursor_attribute_change():
|
|
return False
|
|
|
|
if self.is_default_attribute(attribute):
|
|
return False
|
|
|
|
except Exception as e:
|
|
return False
|
|
return True
|
|
|
|
def format_attributes(self, attribute, attribute_format_string=""):
|
|
# "black",
|
|
# "red",
|
|
# "green",
|
|
# "brown",
|
|
# "blue",
|
|
# "magenta",
|
|
# "cyan",
|
|
# "white",
|
|
# "default" # white.
|
|
# _order_
|
|
# "fg",
|
|
# "bg",
|
|
# "bold",
|
|
# "italics",
|
|
# "underscore",
|
|
# "strikethrough",
|
|
# "reverse",
|
|
# "blink"
|
|
# "fontsieze"
|
|
# "fontfamily"
|
|
if attribute_format_string == "":
|
|
attribute_format_string = self.env["runtime"][
|
|
"SettingsManager"
|
|
].get_setting("general", "attribute_format_string")
|
|
if not attribute_format_string:
|
|
return ""
|
|
if attribute_format_string == "":
|
|
return ""
|
|
if not attribute:
|
|
return ""
|
|
if len(attribute) != 10:
|
|
return ""
|
|
|
|
# 0 FG color (name)
|
|
try:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFGColor", _(attribute[0])
|
|
)
|
|
except Exception as e:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFGColor", ""
|
|
)
|
|
|
|
# 1 BG color (name)
|
|
try:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBGColor", _(attribute[1])
|
|
)
|
|
except Exception as e:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBGColor", ""
|
|
)
|
|
|
|
# 2 bold (True/ False)
|
|
try:
|
|
if attribute[2]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBold", _("bold")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBold", ""
|
|
)
|
|
|
|
# 3 italics (True/ False)
|
|
try:
|
|
if attribute[3]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirItalics", _("italic")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirItalics", ""
|
|
)
|
|
|
|
# 4 underline (True/ False)
|
|
try:
|
|
if attribute[4]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirUnderline", _("underline")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirUnderline", ""
|
|
)
|
|
|
|
# 5 strikethrough (True/ False)
|
|
try:
|
|
if attribute[5]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirStrikethrough", _("strikethrough")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirStrikethrough", ""
|
|
)
|
|
|
|
# 6 reverse (True/ False)
|
|
try:
|
|
if attribute[6]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirReverse", _("reverse")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirReverse", ""
|
|
)
|
|
|
|
# 7 blink (True/ False)
|
|
try:
|
|
if attribute[7]:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBlink", _("blink")
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirBlink", ""
|
|
)
|
|
|
|
# 8 font size (int/ string)
|
|
try:
|
|
try:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFontSize", int(attribute[8])
|
|
)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"AttributeManager formatAttributeToString: Error formatting font size as int: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFontSize", str(attribute[8])
|
|
)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"AttributeManager formatAttributeToString: Error formatting font size as string: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFontSize", _("default")
|
|
)
|
|
|
|
# 9 font family (string)
|
|
try:
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFont", attribute[9]
|
|
)
|
|
except Exception as e:
|
|
pass
|
|
attribute_format_string = attribute_format_string.replace(
|
|
"fenrirFont", _("default")
|
|
)
|
|
|
|
return attribute_format_string
|
|
|
|
def track_highlights(self):
|
|
"""
|
|
Detects text with changed attributes (highlighting) between screen updates.
|
|
|
|
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
|
|
- cursor_position: dict {'x': col, 'y': row} of first highlighted char
|
|
"""
|
|
result = ""
|
|
curr_cursor = None
|
|
|
|
# Early exit conditions - no attribute comparison possible
|
|
if self.prevAttributes is None:
|
|
# First screen load - no previous attributes to compare against
|
|
return result, curr_cursor
|
|
if self.prevAttributes == self.currAttributes:
|
|
# No attribute changes detected
|
|
return result, curr_cursor
|
|
if self.currAttributes is None:
|
|
# Error condition - current attributes missing
|
|
return result, curr_cursor
|
|
if len(self.currAttributes) == 0:
|
|
# Special case for PTY environments with no text content
|
|
return result, curr_cursor
|
|
|
|
# Get current screen text to correlate with attribute changes
|
|
text = self.env["runtime"]["ScreenManager"].get_screen_text()
|
|
text_lines = text.split("\n")
|
|
|
|
# Sanity check: text lines must match attribute array dimensions
|
|
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]:
|
|
# This line has attribute changes - examine each character
|
|
# position
|
|
for column in range(len(self.prevAttributes[line])):
|
|
if (
|
|
self.prevAttributes[line][column]
|
|
== self.currAttributes[line][column]
|
|
):
|
|
# No change at this position
|
|
continue
|
|
# Attribute changed at this position - check if it's worth
|
|
# announcing
|
|
if self.is_useful_for_tracking(
|
|
line, column, self.currAttributes, self.prevAttributes
|
|
):
|
|
# First highlighted character becomes cursor position
|
|
# 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,
|
|
column,
|
|
currAttributes,
|
|
prevAttributes,
|
|
attribute=1,
|
|
mode="zaxe",
|
|
):
|
|
"""
|
|
Determines if an attribute change at a specific position is worth announcing.
|
|
|
|
This prevents announcing every minor attribute change and focuses on meaningful
|
|
highlighting that indicates user selections, focus changes, or important emphasis.
|
|
|
|
Args:
|
|
line, column: Position of the attribute change
|
|
currAttributes, prevAttributes: Current and previous attribute arrays
|
|
attribute: Which attribute to examine (1=background color by default)
|
|
mode: Detection algorithm ('zaxe', 'default', 'barrier')
|
|
|
|
Returns:
|
|
bool: True if this attribute change should be announced to user
|
|
"""
|
|
# Sanity checks for valid position and sufficient screen content
|
|
if len(currAttributes) <= 3:
|
|
return False
|
|
if line < 0:
|
|
return False
|
|
if line > len(currAttributes):
|
|
return False
|
|
|
|
useful = False
|
|
|
|
if mode == "default":
|
|
# Simple mode: announce any non-default attribute
|
|
useful = not self.is_default_attribute(
|
|
currAttributes[line][column]
|
|
)
|
|
|
|
elif (mode == "zaxe") or (mode == ""):
|
|
# Context-aware mode: only announce attributes that stand out from surroundings
|
|
# This prevents announcing entire blocks of highlighted text character by character
|
|
# by checking if the attribute differs from adjacent lines
|
|
|
|
if line == 0:
|
|
# Top line: compare against lines below
|
|
useful = (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line + 1][column][attribute]
|
|
) and (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line + 2][column][attribute]
|
|
)
|
|
elif line >= len(prevAttributes):
|
|
# Bottom line: compare against lines above
|
|
useful = (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line - 1][column][attribute]
|
|
) and (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line - 2][column][attribute]
|
|
)
|
|
else:
|
|
# Middle lines: compare against both directions
|
|
useful = (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line + 1][column][attribute]
|
|
) and (
|
|
currAttributes[line][column][attribute]
|
|
!= currAttributes[line - 1][column][attribute]
|
|
)
|
|
|
|
elif mode == "barrier":
|
|
# Barrier mode: future enhancement for detecting screen
|
|
# boundaries/separators
|
|
useful = True
|
|
|
|
return useful
|