Files

204 lines
5.4 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package presentation
import (
"fmt"
"math"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// ComboBoxSelectionText presents the current choice without repeating the
// control's label or role. Native combo snapshots prefer selected-child text.
func ComboBoxSelectionText(snapshot atspi.Snapshot) string {
if value := controlValue(snapshot); value != "" {
return value
}
if snapshot.Text != "" {
return snapshot.Text
}
return snapshot.Name
}
func controlValue(snapshot atspi.Snapshot) string {
switch snapshot.Role {
case "combo box":
if snapshot.ValueText != "" {
return snapshot.ValueText
}
return snapshot.Description
case "spin button":
if snapshot.Text != "" {
return snapshot.Text
}
if snapshot.ValueText != "" {
return snapshot.ValueText
}
if !snapshot.ValueKnown {
return snapshot.Text
}
case "dial", "progress bar", "scroll bar", "slider", "split pane":
default:
return ""
}
if snapshot.ValueText != "" {
return snapshot.ValueText
}
if !snapshot.ValueKnown || math.IsNaN(snapshot.CurrentValue) || math.IsInf(snapshot.CurrentValue, 0) {
return ""
}
if snapshot.Role != "progress bar" {
// Python's textForValue formats the actual value using the provider's
// minimum increment, rather than normalizing controls to percentages.
increment := snapshot.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(snapshot.CurrentValue) < 1 {
places = 1
}
return fmt.Sprintf("%.*f", places, snapshot.CurrentValue)
}
if snapshot.MaximumValue == snapshot.MinimumValue {
return ""
}
percentage := math.Round((snapshot.CurrentValue - snapshot.MinimumValue) /
(snapshot.MaximumValue - snapshot.MinimumValue) * 100)
return fmt.Sprintf("%.0f percent", percentage)
}
func controlStates(snapshot atspi.Snapshot) []string {
var states []string
switch snapshot.ControlRole() {
case "check box", "check menu item":
switch {
case snapshot.Indeterminate:
states = append(states, "partially checked")
case snapshot.Checked:
states = append(states, "checked")
default:
states = append(states, "not checked")
}
case "radio button", "radio menu item":
if snapshot.Checked {
states = append(states, "selected")
} else {
states = append(states, "not selected")
}
case "switch":
if snapshot.Checked {
states = append(states, "on")
} else {
states = append(states, "off")
}
case "toggle button":
if snapshot.Checked {
states = append(states, "pressed")
} else {
states = append(states, "not pressed")
}
}
if snapshot.Expandable {
if snapshot.Expanded {
states = append(states, "expanded")
} else {
states = append(states, "collapsed")
}
}
if snapshot.Selected && presentsSelectedState(snapshot.Role) {
states = append(states, "selected")
}
if snapshot.Multiselectable {
states = append(states, "multi-select")
}
if snapshot.ReadOnly {
states = append(states, "read only")
}
if snapshot.Required {
states = append(states, "required")
}
if snapshot.Invalid {
states = append(states, "invalid entry")
}
if snapshot.SensitiveKnown && !snapshot.Sensitive {
states = append(states, "grayed")
}
return states
}
func presentsSelectedState(role string) bool {
switch role {
case "icon", "list item", "tree item":
return true
default:
return false
}
}
func stateChangeText(snapshot atspi.Snapshot, eventType string) string {
switch eventType {
case "object:state-changed:checked", "object:state-changed:indeterminate", "object:state-changed:pressed":
for _, state := range controlStates(snapshot) {
switch state {
case "partially checked", "checked", "not checked", "selected", "not selected", "on", "off", "pressed", "not pressed":
return state
}
}
case "object:state-changed:expanded":
if snapshot.Expanded {
return "expanded"
}
return "collapsed"
case "object:state-changed:invalid-entry":
if snapshot.Invalid {
return "invalid entry"
}
case "object:state-changed:read-only":
if snapshot.ReadOnly {
return "read only"
}
return "editable"
case "object:state-changed:required":
if snapshot.Required {
return "required"
}
case "object:state-changed:selected":
if snapshot.Role == "table cell" {
if snapshot.Selected {
return "selected"
}
return ""
}
if presentsSelectedState(snapshot.Role) {
if snapshot.Selected {
return "selected"
}
return "not selected"
}
case "object:state-changed:sensitive":
if snapshot.SensitiveKnown && !snapshot.Sensitive {
return "grayed"
}
}
return ""
}