204 lines
5.4 KiB
Go
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 ""
|
|
}
|