Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,431 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/review"
|
||||
)
|
||||
|
||||
type brailleFrameEnqueuer interface{ EnqueueFrame(braille.Frame) }
|
||||
|
||||
// Browse cells map to native object offsets under the page's review epoch.
|
||||
func brailleDocumentFrame(result review.Result, options config.BrailleOptions, configured ...config.TextAttributeSettings) braille.Frame {
|
||||
snapshot := result.DocumentPosition.Snapshot
|
||||
line, assembled := result.DocumentBrailleLine, result.DocumentCaretLine
|
||||
if line == nil && assembled == nil || snapshot.Role == "password text" {
|
||||
frame := brailleFocusFrameWithOptions(snapshot, options, configured...)
|
||||
frame.Routes = nil
|
||||
textLength := utf8.RuneCountInString(snapshot.Name)
|
||||
if textLength == 0 {
|
||||
textLength = utf8.RuneCountInString(snapshot.Text)
|
||||
}
|
||||
return linkedHeadingBraille(frame, result.DocumentPosition.LinkedHeading, options, textLength)
|
||||
}
|
||||
frame := braille.Frame{Cursor: -1, WordWrap: options.WordWrap}
|
||||
appendContent := func(source atspi.Snapshot, raw, separator string, sourceStart int) {
|
||||
frame.Text += separator
|
||||
start := utf8.RuneCountInString(frame.Text)
|
||||
frame.Text += raw
|
||||
length := utf8.RuneCountInString(raw)
|
||||
if source.ID() == snapshot.ID() {
|
||||
frame.Cursor = start + result.DocumentPosition.Offset - sourceStart
|
||||
}
|
||||
if source.ID().Valid() && source.Role != "password text" && length > 0 {
|
||||
frame.Routes = append(frame.Routes, braille.TextRoute{Start: start, End: start + length,
|
||||
SourceStart: sourceStart, TextLength: length, BusName: source.BusName, Path: source.Path, SourceText: raw})
|
||||
}
|
||||
if options.SelectionIndicator != 0 && source.SelectionEnd > source.SelectionStart {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
for offset := max(source.SelectionStart, sourceStart); offset < min(source.SelectionEnd, sourceStart+length); offset++ {
|
||||
frame.Dots[start+offset-sourceStart] |= byte(options.SelectionIndicator)
|
||||
}
|
||||
}
|
||||
isLink := source.Role == "link"
|
||||
for _, ancestor := range source.BrailleContext {
|
||||
isLink = isLink || ancestor.Role == "link"
|
||||
}
|
||||
if options.LinkIndicator != 0 && isLink {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
for offset := start; offset < start+length; offset++ {
|
||||
frame.Dots[offset] |= byte(options.LinkIndicator)
|
||||
}
|
||||
}
|
||||
if len(configured) != 0 {
|
||||
if len(source.TextAttributeRuns) != 0 {
|
||||
for _, run := range source.TextAttributeRuns {
|
||||
markTextAttributeRun(&frame, run, sourceStart, start, length, configured[0])
|
||||
}
|
||||
} else {
|
||||
markTextAttributeRun(&frame, source.TextAttributeRun, sourceStart, start, length, configured[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
if assembled != nil {
|
||||
for index, content := range assembled.Contents {
|
||||
raw := content.RawText
|
||||
if index == len(assembled.Contents)-1 {
|
||||
raw = strings.TrimRight(raw, "\r\n")
|
||||
}
|
||||
if content.Snapshot.Role == "password text" {
|
||||
continue
|
||||
}
|
||||
appendContent(content.Snapshot, raw, content.Separator, content.Start)
|
||||
}
|
||||
} else {
|
||||
snapshot.SelectionStart, snapshot.SelectionEnd = line.SelectionStart, line.SelectionEnd
|
||||
snapshot.TextAttributeRuns = line.TextAttributeRuns
|
||||
appendContent(snapshot, strings.TrimRight(line.Text, "\r\n"), "", line.Start)
|
||||
}
|
||||
textLength := utf8.RuneCountInString(frame.Text)
|
||||
if frame.Cursor < 0 || frame.Cursor > textLength {
|
||||
frame.Cursor = -1
|
||||
} else if frame.Cursor == textLength {
|
||||
frame.Text += " "
|
||||
for index := range frame.Routes {
|
||||
if frame.Routes[index].End == textLength {
|
||||
frame.Routes[index].End++
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.DocumentFocus.Valid() && result.DocumentPosition.Document.Valid() {
|
||||
doc := result.DocumentPosition.Document.ID()
|
||||
frame.Document = &braille.DocumentRoute{Epoch: result.DocumentEpoch, FocusBus: result.DocumentFocus.BusName,
|
||||
FocusPath: result.DocumentFocus.Path, DocumentBus: doc.BusName, DocumentPath: doc.Path}
|
||||
} else {
|
||||
frame.Routes = nil
|
||||
}
|
||||
ancestors := append([]atspi.Snapshot{snapshot}, result.DocumentCaretContext...)
|
||||
for _, ancestor := range ancestors {
|
||||
if ancestor.Role == "heading" {
|
||||
frame.Text = brailleJoin(frame.Text, fmt.Sprintf("h%d", ancestor.HeadingLevel))
|
||||
break
|
||||
}
|
||||
}
|
||||
if options.LinkIndicator != 0 && assembled == nil {
|
||||
for _, ancestor := range ancestors {
|
||||
if ancestor.Role == "link" {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
for index := 0; index < textLength; index++ {
|
||||
frame.Dots[index] |= byte(options.LinkIndicator)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(frame.Dots) != 0 {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
}
|
||||
return linkedHeadingBraille(frame, result.DocumentPosition.LinkedHeading, options, textLength)
|
||||
}
|
||||
|
||||
func linkedHeadingBraille(frame braille.Frame, link *atspi.Snapshot, options config.BrailleOptions, textLength int) braille.Frame {
|
||||
if link == nil {
|
||||
return frame
|
||||
}
|
||||
contentLength := utf8.RuneCountInString(frame.Text)
|
||||
textLength = min(contentLength, max(0, textLength))
|
||||
if options.LinkIndicator != 0 {
|
||||
if len(frame.Dots) < contentLength {
|
||||
frame.Dots = append(frame.Dots, make([]byte, contentLength-len(frame.Dots))...)
|
||||
}
|
||||
for index := 0; index < textLength; index++ {
|
||||
frame.Dots[index] |= byte(options.LinkIndicator)
|
||||
}
|
||||
}
|
||||
role := "lnk"
|
||||
if link.Visited {
|
||||
role = "vlnk"
|
||||
}
|
||||
frame.Text = brailleJoin(frame.Text, role)
|
||||
if len(frame.Dots) != 0 {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-contentLength)...)
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// Formats the captured object and bounded context using Python formatting.py's
|
||||
// braille rules and object_properties.py's indicators.
|
||||
func brailleFocusFrame(s atspi.Snapshot) braille.Frame {
|
||||
return brailleFocusFrameWithOptions(s, config.DefaultSettings().Braille)
|
||||
}
|
||||
|
||||
func brailleFocusFrameWithOptions(s atspi.Snapshot, options config.BrailleOptions, configured ...config.TextAttributeSettings) braille.Frame {
|
||||
return braille_context_frame(braille_object_frame(s, options, configured...), s, options)
|
||||
}
|
||||
|
||||
func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, configured ...config.TextAttributeSettings) braille.Frame {
|
||||
name, role := strings.TrimSpace(s.Name), s.Role
|
||||
if role == "terminal" || brailleTextRole(role) && s.TextCaretKnown {
|
||||
label := name
|
||||
if role == "terminal" || role == "heading" || role == "static" {
|
||||
label = ""
|
||||
}
|
||||
line := strings.TrimRight(s.BrailleLine, "\r\n")
|
||||
if label == line {
|
||||
label = ""
|
||||
}
|
||||
prefix := ""
|
||||
if label != "" {
|
||||
prefix = label + " "
|
||||
}
|
||||
text := prefix + line
|
||||
end := utf8.RuneCountInString(text)
|
||||
cursor := -1
|
||||
relative := s.BrailleCaret - s.BrailleLineStart
|
||||
if s.TextCaretKnown && relative >= 0 && relative <= utf8.RuneCountInString(s.BrailleLine) {
|
||||
cursor = utf8.RuneCountInString(prefix) + min(relative, utf8.RuneCountInString(line))
|
||||
}
|
||||
if role != "terminal" && role != "heading" && !options.DisableEOL {
|
||||
text += " $l"
|
||||
} else if cursor == end {
|
||||
text += " "
|
||||
}
|
||||
frame := braille.Frame{Text: text, Cursor: cursor, WordWrap: options.WordWrap}
|
||||
if options.SelectionIndicator != 0 && s.SelectionEnd > s.SelectionStart {
|
||||
frame.Dots = make([]byte, utf8.RuneCountInString(frame.Text))
|
||||
start := utf8.RuneCountInString(prefix)
|
||||
for source := max(s.SelectionStart, s.BrailleLineStart); source < min(s.SelectionEnd, s.BrailleLineStart+utf8.RuneCountInString(line)); source++ {
|
||||
frame.Dots[start+source-s.BrailleLineStart] = byte(options.SelectionIndicator)
|
||||
}
|
||||
}
|
||||
if options.LinkIndicator != 0 {
|
||||
for _, ancestor := range s.BrailleContext {
|
||||
if ancestor.Role != "link" {
|
||||
continue
|
||||
}
|
||||
if len(frame.Dots) == 0 {
|
||||
frame.Dots = make([]byte, utf8.RuneCountInString(frame.Text))
|
||||
}
|
||||
start := utf8.RuneCountInString(prefix)
|
||||
for index := start; index < start+utf8.RuneCountInString(line); index++ {
|
||||
frame.Dots[index] |= byte(options.LinkIndicator)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(configured) != 0 {
|
||||
if len(s.TextAttributeRuns) != 0 {
|
||||
for _, run := range s.TextAttributeRuns {
|
||||
markTextAttributeRun(&frame, run, s.BrailleLineStart, utf8.RuneCountInString(prefix), utf8.RuneCountInString(line), configured[0])
|
||||
}
|
||||
} else {
|
||||
markTextAttributeRun(&frame, s.TextAttributeRun, s.BrailleLineStart, utf8.RuneCountInString(prefix), utf8.RuneCountInString(line), configured[0])
|
||||
}
|
||||
}
|
||||
// Only actual text and its EOL marker route; labels and state suffixes do not.
|
||||
if s.TextCaretKnown && s.ID().Valid() {
|
||||
frame.Routes = []braille.TextRoute{{Start: utf8.RuneCountInString(prefix), End: utf8.RuneCountInString(text),
|
||||
SourceStart: s.BrailleLineStart, TextLength: utf8.RuneCountInString(line),
|
||||
BusName: s.BusName, Path: s.Path, SourceText: s.BrailleLine}}
|
||||
}
|
||||
if role == "heading" {
|
||||
frame.Text = brailleJoin(frame.Text, fmt.Sprintf("h%d", s.HeadingLevel))
|
||||
} else if role != "terminal" {
|
||||
if s.Invalid && role != "spin button" && role != "document frame" && role != "document web" {
|
||||
frame.Text += " invalid"
|
||||
}
|
||||
if s.Required {
|
||||
frame.Text += " required"
|
||||
}
|
||||
if s.ReadOnly {
|
||||
frame.Text += " rdonly"
|
||||
}
|
||||
}
|
||||
if len(frame.Dots) != 0 && len(frame.Dots) < utf8.RuneCountInString(frame.Text) {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
}
|
||||
return frame
|
||||
}
|
||||
// Protected text is never copied, even if a provider supplies it accidentally.
|
||||
if role == "password text" {
|
||||
return braille.Frame{Text: brailleJoin(name, role), Cursor: 0}
|
||||
}
|
||||
roleName := role
|
||||
switch role {
|
||||
case "invalid", "unknown", "redundant object", "filler", "extended", "link":
|
||||
roleName = ""
|
||||
case "heading":
|
||||
roleName = fmt.Sprintf("h%d", s.HeadingLevel)
|
||||
}
|
||||
if options.Verbosity == 0 && role != "heading" {
|
||||
roleName = ""
|
||||
}
|
||||
if options.AbbreviateRoles && roleName != "" && role != "heading" {
|
||||
roleName = shortBrailleRole(roleName)
|
||||
}
|
||||
expanded := ""
|
||||
if s.Expandable {
|
||||
expanded = "collapsed"
|
||||
if s.Expanded {
|
||||
expanded = "expanded"
|
||||
}
|
||||
}
|
||||
available := ""
|
||||
if s.SensitiveKnown && !s.Sensitive {
|
||||
available = "grayed"
|
||||
}
|
||||
indicator := ""
|
||||
if s.Checked {
|
||||
indicator = "<x>"
|
||||
} else if s.Indeterminate {
|
||||
indicator = "<->"
|
||||
} else {
|
||||
indicator = "< >"
|
||||
}
|
||||
value := brailleValue(s)
|
||||
text := ""
|
||||
cursor := 0
|
||||
switch role {
|
||||
case "check box":
|
||||
text = brailleJoin(indicator, name, roleName)
|
||||
case "check menu item":
|
||||
text = brailleJoin(indicator, name, roleName, available)
|
||||
case "radio button", "radio menu item", "toggle button":
|
||||
indicator = "& y"
|
||||
if s.Checked {
|
||||
indicator = "&=y"
|
||||
}
|
||||
if name == "" {
|
||||
name = s.Description
|
||||
}
|
||||
if role == "toggle button" {
|
||||
text = brailleJoin(indicator, name, expanded, roleName)
|
||||
} else {
|
||||
text = brailleJoin(indicator, name, roleName)
|
||||
if role == "radio menu item" {
|
||||
text = brailleJoin(text, available)
|
||||
}
|
||||
}
|
||||
case "push button", "button":
|
||||
if name == "" {
|
||||
name = s.Description
|
||||
}
|
||||
text = brailleJoin(name, expanded, roleName)
|
||||
case "menu item":
|
||||
text = brailleJoin(name, expanded, available)
|
||||
case "tree item":
|
||||
text = brailleJoin(name, expanded)
|
||||
case "list item":
|
||||
text = brailleJoin(name, expanded, roleName, available)
|
||||
case "menu", "page tab":
|
||||
text = brailleJoin(name, roleName, available)
|
||||
case "combo box":
|
||||
if value == "" {
|
||||
value = s.Description
|
||||
}
|
||||
text = brailleJoin(name, value, roleName)
|
||||
if name != "" && value != "" {
|
||||
cursor = utf8.RuneCountInString(name) + 1
|
||||
}
|
||||
case "dial", "scroll bar", "slider", "spin button":
|
||||
text = brailleJoin(name, value, roleName)
|
||||
if s.Required {
|
||||
text = brailleJoin(text, "required")
|
||||
}
|
||||
if role == "slider" && s.Invalid {
|
||||
text = brailleJoin(text, "invalid")
|
||||
}
|
||||
case "progress bar":
|
||||
text = brailleJoin(name, value, roleName)
|
||||
default:
|
||||
displayed := s.Text
|
||||
if displayed == name {
|
||||
displayed = ""
|
||||
}
|
||||
text = brailleJoin(name, displayed, value, roleName)
|
||||
if s.Required {
|
||||
text = brailleJoin(text, "required")
|
||||
}
|
||||
if s.Invalid {
|
||||
text = brailleJoin(text, "invalid")
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
cursor = -1
|
||||
}
|
||||
frame := braille.Frame{Text: text, Cursor: cursor, WordWrap: options.WordWrap}
|
||||
if role == "link" && options.LinkIndicator != 0 {
|
||||
frame.Dots = make([]byte, utf8.RuneCountInString(text))
|
||||
for index := range frame.Dots {
|
||||
frame.Dots[index] |= byte(options.LinkIndicator)
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
func shortBrailleRole(role string) string {
|
||||
if short, ok := map[string]string{
|
||||
"check box": "chk", "check menu item": "chk", "radio button": "radio",
|
||||
"push button": "btn", "button": "btn", "combo box": "cbo",
|
||||
"list item": "lstitm", "page tab": "pgt", "text": "txt",
|
||||
"link": "lnk", "slider": "sldr", "spin button": "spin",
|
||||
"menu": "mnu", "menu item": "mnuitm", "progress bar": "pgbar",
|
||||
"toggle button": "tglbtn", "password text": "pwd", "label": "lbl",
|
||||
"application": "app", "frame": "frm", "dialog": "dlg", "panel": "pnl",
|
||||
"list": "lst", "table": "tbl", "table cell": "cll",
|
||||
"column header": "colhdr", "row header": "rwhdr",
|
||||
}[role]; ok {
|
||||
return short
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
func brailleTextRole(role string) bool {
|
||||
switch role {
|
||||
case "entry", "text", "static", "paragraph", "section", "label", "page", "document frame", "document web", "heading", "spin button":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func brailleJoin(parts ...string) string {
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func brailleValue(s atspi.Snapshot) string {
|
||||
if s.Role == "progress bar" {
|
||||
if !s.ValueKnown || s.Indeterminate && s.CurrentValue <= 0 {
|
||||
return ""
|
||||
}
|
||||
if s.MaximumValue == s.MinimumValue {
|
||||
if s.CurrentValue == s.MaximumValue && s.CurrentValue >= 1 && s.CurrentValue <= 100 {
|
||||
return fmt.Sprintf("%.0f%%", math.Trunc(s.CurrentValue))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
value := (s.CurrentValue - s.MinimumValue) / (s.MaximumValue - s.MinimumValue) * 100
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%.0f%%", math.Trunc(value))
|
||||
}
|
||||
if s.ValueText != "" {
|
||||
return s.ValueText
|
||||
}
|
||||
if !s.ValueKnown || math.IsNaN(s.CurrentValue) || math.IsInf(s.CurrentValue, 0) {
|
||||
return ""
|
||||
}
|
||||
increment := s.MinimumIncrement
|
||||
if increment < 0 || math.IsNaN(increment) || math.IsInf(increment, 0) {
|
||||
return ""
|
||||
}
|
||||
places := 0
|
||||
if increment > 0 {
|
||||
places = int(math.Ceil(math.Max(0, -math.Log10(increment))))
|
||||
} else if math.Abs(s.CurrentValue) < 1 {
|
||||
places = 1
|
||||
}
|
||||
return fmt.Sprintf("%.*f", places, s.CurrentValue)
|
||||
}
|
||||
Reference in new issue
Block a user