Compare commits
2
Commits
b5a5b86dda
...
c0e4f7d292
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0e4f7d292 | ||
|
|
15d527c619 |
No files matched your search
@@ -22,3 +22,19 @@ location follows the new user location, the old `/usr` location follows the new
|
||||
system locations, and the old name on `PATH` follows the new name.
|
||||
Restart Cthulhu after changing the selected helper. Keep the launcher, its
|
||||
`.exe.so` sidecar, and the native hooks together in the installed directory.
|
||||
|
||||
Wine typing feedback in standard Edit controls follows the current key echo
|
||||
settings, including the master switch, character echo, and alphabetic, numeric,
|
||||
punctuation, space and diacritical categories. Tabs and line breaks follow
|
||||
key echo's action-key setting. Applying preferences takes effect without
|
||||
restarting the helper. Navigation, selection and deletion feedback remain
|
||||
available when typing echo is disabled. Word and sentence typing echo are not
|
||||
currently supplied by the Wine helper.
|
||||
|
||||
For a live check, type repeated letters, numbers, punctuation and spaces in
|
||||
Wine Notepad, change the corresponding echo settings and apply them, then
|
||||
continue typing. Disable key echo and verify that typing stays quiet while
|
||||
arrows, selection and Backspace still report editing feedback. Re-enable it
|
||||
and check capital/lowercase voices and password exclusion. Automated tests
|
||||
cover queued speech and Braille; audible speech and a physical Braille display
|
||||
still require live verification.
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/remote"
|
||||
"git.stormux.org/storm/cthulhu/internal/wine"
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -95,10 +96,17 @@ func (presenter remoteSpeech) presentWineEditor(event remote.AccessibleEvent) bo
|
||||
// character identity for navigation and Backspace as well as typing.
|
||||
kinds := strings.Fields(event.States)
|
||||
inserted := slices.Contains(kinds, "kind=8")
|
||||
settings := config.DefaultSettings()
|
||||
if presenter.live != nil {
|
||||
settings = presenter.live.Load()
|
||||
}
|
||||
if inserted && !wineTypingEchoEnabled(event.Name, settings) {
|
||||
return false
|
||||
}
|
||||
character := inserted || utf8.RuneCountInString(event.Name) == 1
|
||||
custom := false
|
||||
if character && presenter.live != nil && (inserted || slices.Contains(kinds, "kind=7")) {
|
||||
custom = customEcho(presenter.live.Load(), "character")
|
||||
custom = customEcho(settings, "character")
|
||||
}
|
||||
return presenter.presentRequest(speechRequest{
|
||||
Text: event.Name, Role: "system", Cause: "wine-accessible-event", Append: true,
|
||||
|
||||
@@ -7,7 +7,11 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
"git.stormux.org/storm/cthulhu/internal/remote"
|
||||
)
|
||||
|
||||
@@ -40,6 +44,24 @@ func newWineEventPresenter(present func(string) bool) *wineEventPresenter {
|
||||
return &wineEventPresenter{present: present}
|
||||
}
|
||||
|
||||
// wineTypingEchoEnabled applies the native insertion policy to the helper's
|
||||
// character labels. Navigation and deletion do not pass through this filter.
|
||||
func wineTypingEchoEnabled(text string, settings config.Settings) bool {
|
||||
switch text {
|
||||
case "tab", "new line":
|
||||
return settings.EnableKeyEcho && settings.EnableActionKeys
|
||||
case "space":
|
||||
text = " "
|
||||
}
|
||||
// The helper sends one accepted character per insertion. Do not treat an
|
||||
// unexpected label as a multi-character paste, which has a different policy.
|
||||
if utf8.RuneCountInString(text) != 1 {
|
||||
return false
|
||||
}
|
||||
event := atspi.Event{Type: atspi.TextInsertedEvent, Payload: text}
|
||||
return presentation.EventTextWithOptions(event, presentationSettingsFromConfig(settings, false)) != ""
|
||||
}
|
||||
|
||||
func (owner *wineEventPresenter) Present(event remote.AccessibleEvent) bool {
|
||||
return owner.present_event(event, nil)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/remote"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
@@ -32,7 +33,10 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
|
||||
t.Skip("set CTHULHU_NATIVE_DBUS_TEST=1 inside a private session bus")
|
||||
}
|
||||
output := &wineTestOutput{speech: make(chan speechRequest, 32), braille: make(chan string, 32)}
|
||||
handler := remoteSpeech{speech: output, braille: output}
|
||||
settings := config.DefaultSettings()
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
handler := remoteSpeech{speech: output, braille: output, live: live}
|
||||
owner := newWineEventPresenter(func(message string) bool {
|
||||
return handler.presentMessage(message, "wine-accessible-event")
|
||||
})
|
||||
@@ -71,6 +75,19 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
|
||||
for _, text := range []string{"A", "a", "É", "é"} {
|
||||
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "start=1 end=1 kind=8"}, true)
|
||||
}
|
||||
settings.EnableKeyEcho = false
|
||||
live.Store(settings)
|
||||
for _, text := range []string{"A", "space", "tab", "new line"} {
|
||||
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "kind=8"}, false)
|
||||
}
|
||||
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "e", States: "kind=7"}, true)
|
||||
settings.EnableKeyEcho = true
|
||||
settings.EnableAlphabeticKeys = false
|
||||
live.Store(settings)
|
||||
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, false)
|
||||
settings.EnableAlphabeticKeys = true
|
||||
live.Store(settings)
|
||||
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, true)
|
||||
send(remote.AccessibleEvent{SourceID: "check", EventType: "focus", Name: "Option", Role: "check box", States: "not checked"}, true)
|
||||
owner.flush()
|
||||
send(remote.AccessibleEvent{SourceID: "check", EventType: "name", Name: "Renamed", Role: "check box", States: "not checked"}, true)
|
||||
@@ -87,7 +104,7 @@ func TestNativeSessionBusWineEvents(t *testing.T) {
|
||||
send(remote.AccessibleEvent{SourceID: "no", EventType: "focus", Name: "No", Role: "client"}, true)
|
||||
owner.flush()
|
||||
owner.Close()
|
||||
for _, want := range []string{"edit, hello", "l", "l", "e selected", "e deleted", "A", "a", "É", "é", "Option, check box, not checked", "Renamed, check box, not checked", "Renamed, check box, checked, unavailable", "Yes", "No"} {
|
||||
for _, want := range []string{"edit, hello", "l", "l", "e selected", "e deleted", "A", "a", "É", "é", "e", "A", "Option, check box, not checked", "Renamed, check box, not checked", "Renamed, check box, checked, unavailable", "Yes", "No"} {
|
||||
select {
|
||||
case request := <-output.speech:
|
||||
if request.Text != want || !request.Append {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/remote"
|
||||
)
|
||||
|
||||
func TestWineTypingEchoUsesLiveSettings(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
speech := &remoteSpeechQueue{}
|
||||
braille := &remoteBrailleQueue{running: true}
|
||||
presenter := remoteSpeech{speech: speech, braille: braille, live: live}
|
||||
owner := newWineEventPresenter(func(message string) bool {
|
||||
return presenter.presentMessage(message, "wine-accessible-event")
|
||||
})
|
||||
defer owner.Close()
|
||||
presenter.wine = owner
|
||||
presenter.PresentAccessibleEvent(remote.AccessibleEvent{SourceID: "edit", EventType: "focus", Role: "edit"})
|
||||
owner.flush()
|
||||
check := func(text, kind string, want bool) {
|
||||
t.Helper()
|
||||
beforeSpeech, beforeBraille := len(speech.requests), len(braille.text)
|
||||
accepted := presenter.PresentAccessibleEvent(remote.AccessibleEvent{
|
||||
SourceID: "edit", EventType: "text", Name: text, States: "start=1 end=1 kind=" + kind,
|
||||
})
|
||||
if accepted != want {
|
||||
t.Errorf("%q kind=%s accepted=%v, want %v", text, kind, accepted, want)
|
||||
}
|
||||
if !want {
|
||||
if len(speech.requests) != beforeSpeech || len(braille.text) != beforeBraille {
|
||||
t.Errorf("disabled typing %q reached output", text)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(speech.requests) != beforeSpeech+1 || speech.requests[beforeSpeech].Text != text ||
|
||||
len(braille.text) != beforeBraille+1 || braille.text[beforeBraille] != text {
|
||||
t.Errorf("enabled action %q missing from speech or braille", text)
|
||||
}
|
||||
}
|
||||
for _, text := range []string{"A", "a", "a", "É", "𐐀", "7", "!", "😀", "space", "tab", "new line"} {
|
||||
check(text, "8", true)
|
||||
}
|
||||
settings.EnableKeyEcho = false
|
||||
settings.EnableEchoByCharacter = true
|
||||
live.Store(settings)
|
||||
for _, text := range []string{"a", "7", "!", "space", "tab", "new line"} {
|
||||
check(text, "8", false)
|
||||
}
|
||||
// Reading, deletion and selection remain available with key echo disabled.
|
||||
for _, action := range []struct{ text, kind string }{
|
||||
{"a", "5"}, {"a", "7"}, {"line of text", "1"}, {"a selected", "2"}, {"a deleted", "4"},
|
||||
} {
|
||||
check(action.text, action.kind, true)
|
||||
}
|
||||
settings.EnableKeyEcho = true
|
||||
settings.EnableEchoByCharacter = false
|
||||
settings.EnableAlphabeticKeys = false
|
||||
settings.EnableNumericKeys = false
|
||||
settings.EnablePunctuationKeys = false
|
||||
settings.EnableSpace = false
|
||||
settings.EnableActionKeys = false
|
||||
live.Store(settings)
|
||||
for _, text := range []string{"A", "é", "𐐀", "7", "!", "😀", "space", "tab", "new line", "́"} {
|
||||
check(text, "8", false)
|
||||
}
|
||||
settings.EnableEchoByCharacter = true
|
||||
live.Store(settings)
|
||||
for _, text := range []string{"A", "é", "7", "!", "😀", "space", "́"} {
|
||||
check(text, "8", true)
|
||||
}
|
||||
for _, text := range []string{"tab", "new line"} {
|
||||
check(text, "8", false)
|
||||
}
|
||||
settings.EnableEchoByCharacter = false
|
||||
settings.EnableDiacriticalKeys = true
|
||||
settings.EnableActionKeys = true
|
||||
live.Store(settings)
|
||||
for _, text := range []string{"́", "tab", "new line"} {
|
||||
check(text, "8", true)
|
||||
}
|
||||
settings = config.DefaultSettings()
|
||||
live.Store(settings)
|
||||
check("a", "8", true)
|
||||
}
|
||||
|
||||
func TestWineTypingEchoFiltersCategoriesIndependently(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, blocked, allowed string
|
||||
disable func(*config.Settings)
|
||||
}{
|
||||
{"alphabetic", "É", "7", func(s *config.Settings) { s.EnableAlphabeticKeys = false }},
|
||||
{"numeric", "7", "a", func(s *config.Settings) { s.EnableNumericKeys = false }},
|
||||
{"punctuation", "!", "a", func(s *config.Settings) { s.EnablePunctuationKeys = false }},
|
||||
{"symbol", "😀", "a", func(s *config.Settings) { s.EnablePunctuationKeys = false }},
|
||||
{"space", "space", "a", func(s *config.Settings) { s.EnableSpace = false }},
|
||||
{"action", "tab", "space", func(s *config.Settings) { s.EnableActionKeys = false }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
tc.disable(&settings)
|
||||
if wineTypingEchoEnabled(tc.blocked, settings) {
|
||||
t.Errorf("disabled category echoed %q", tc.blocked)
|
||||
}
|
||||
if !wineTypingEchoEnabled(tc.allowed, settings) {
|
||||
t.Errorf("unrelated category suppressed %q", tc.allowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user