Avoid repeating Mumble server labels across columns
Collapse identical full-row labels only for Mumble while preserving mixed column data and repeated values from other providers. Add Qt native coverage and document live saved-server listening confirmation.
This commit is contained in:
1 parent
64440fbb78
commit
3dbd7f2554
8 files changed
+162
-1
No files matched your search
@@ -14,6 +14,12 @@ occupy history slots. Identical messages remain separate entries. Log messages
|
|||||||
can speak in the background without taking focus. Mumble's message entry and
|
can speak in the background without taking focus. Mumble's message entry and
|
||||||
other text controls do not supply history.
|
other text controls do not supply history.
|
||||||
|
|
||||||
|
Mumble's server list can expose the same accessible label in every column,
|
||||||
|
including Ping and Users. Full-row reading announces that label once when all
|
||||||
|
columns repeat it. Rows containing distinct information retain their complete
|
||||||
|
column text, including equal ping and user-count values. Other applications'
|
||||||
|
tables retain repeated values.
|
||||||
|
|
||||||
The legacy controls for separate conversation histories, room-name prefixes,
|
The legacy controls for separate conversation histories, room-name prefixes,
|
||||||
typing announcements, and message verbosity are not yet ported.
|
typing announcements, and message verbosity are not yet ported.
|
||||||
|
|
||||||
@@ -148,6 +154,17 @@ Actual Pidgin listening, physical shortcuts, and Braille acceptance remain pendi
|
|||||||
|
|
||||||
### Mumble live acceptance
|
### Mumble live acceptance
|
||||||
|
|
||||||
|
For server-list row reading, open Server Connect and move between two saved
|
||||||
|
servers with Up/Down. Each server label should occur once per row announcement.
|
||||||
|
Try a public listing too: any distinct ping or user-count text exposed by Mumble
|
||||||
|
should remain present. Check channel/user navigation after connecting and a
|
||||||
|
multi-column table in another application; equal values in separate columns
|
||||||
|
must remain available there.
|
||||||
|
|
||||||
|
Live listening confirmed that the repeated labels in two saved-server listings
|
||||||
|
are spoken once after this fix. Public listings and physical Braille remain
|
||||||
|
unverified for this change.
|
||||||
|
|
||||||
1. Quit the current reader with **Cthulhu+Q**. Press **Ctrl+Escape**, then press
|
1. Quit the current reader with **Cthulhu+Q**. Press **Ctrl+Escape**, then press
|
||||||
**Shift+G**. These are separate key presses. The launch script rebuilds the
|
**Shift+G**. These are separate key presses. The launch script rebuilds the
|
||||||
reader and Braille helper, starts debug logging, and opens a test terminal.
|
reader and Braille helper, starts debug logging, and opens a test terminal.
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ func (adapter *Adapter) enrichNativeWindowEvent(event *C.AtspiEvent, target *C.A
|
|||||||
snapshot.TablePositionKnown = rowIndex >= 0 && columnIndex >= 0
|
snapshot.TablePositionKnown = rowIndex >= 0 && columnIndex >= 0
|
||||||
snapshot.TableRowText = sanitizeSnapshotField(C.GoString(rowText))
|
snapshot.TableRowText = sanitizeSnapshotField(C.GoString(rowText))
|
||||||
snapshot.GUITable = guiTable != 0
|
snapshot.GUITable = guiTable != 0
|
||||||
|
snapshot.TableRowText = mumbleTableRowText(snapshot)
|
||||||
C.g_free(C.gpointer(tableName))
|
C.g_free(C.gpointer(tableName))
|
||||||
C.g_free(C.gpointer(tablePath))
|
C.g_free(C.gpointer(tablePath))
|
||||||
C.g_free(C.gpointer(rowText))
|
C.g_free(C.gpointer(rowText))
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ import (
|
|||||||
|
|
||||||
var mumbleTimestamp = regexp.MustCompile(`^\[\d{2}:\d{2}:\d{2}\]\s*`)
|
var mumbleTimestamp = regexp.MustCompile(`^\[\d{2}:\d{2}:\d{2}\]\s*`)
|
||||||
|
|
||||||
|
// Mumble supplies the same accessible label for every server-list column,
|
||||||
|
// including Ping and Users. Collapse only a whole row of identical labels;
|
||||||
|
// ordinary equal-valued columns and rows containing other data retain them.
|
||||||
|
func mumbleTableRowText(snapshot Snapshot) string {
|
||||||
|
if snapshot.ApplicationProcess == "mumble" && snapshot.GUITable &&
|
||||||
|
snapshot.Role == "table cell" && snapshot.Name != "" &&
|
||||||
|
snapshot.TableColumnCount > 1 && snapshot.TableColumnCount <= 32 &&
|
||||||
|
snapshot.TableRowText == snapshot.Name+strings.Repeat(", "+snapshot.Name, snapshot.TableColumnCount-1) {
|
||||||
|
return snapshot.Name
|
||||||
|
}
|
||||||
|
return snapshot.TableRowText
|
||||||
|
}
|
||||||
|
|
||||||
// Match legacy Mumble's inserted-message cleanup. Qt can insert the timestamp
|
// Match legacy Mumble's inserted-message cleanup. Qt can insert the timestamp
|
||||||
// separately, and uses paragraph separators and noncharacters in its log.
|
// separately, and uses paragraph separators and noncharacters in its log.
|
||||||
func mumbleMessageText(text string) string {
|
func mumbleMessageText(text string) string {
|
||||||
|
|||||||
@@ -86,3 +86,73 @@ func TestNativeMumbleTranscriptCapture(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNativeMumbleTableRows(t *testing.T) {
|
||||||
|
if os.Getenv("CTHULHU_NATIVE_MUMBLE") != "1" {
|
||||||
|
t.Skip("requires private Qt/Xorg/AT-SPI session")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
adapter := New()
|
||||||
|
if err := adapter.Start(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer adapter.Stop(context.Background())
|
||||||
|
for _, process := range []string{"mumble", "table-test"} {
|
||||||
|
t.Run(process, func(t *testing.T) {
|
||||||
|
command := exec.CommandContext(ctx, "python3", "../../tests/qt_mumble_table.py", process)
|
||||||
|
output, err := command.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
if err := command.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { command.Process.Kill(); command.Wait() }()
|
||||||
|
line, err := bufio.NewReader(output).ReadString('\n')
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if err != nil || len(fields) != 2 || fields[0] != "ready" {
|
||||||
|
t.Fatalf("fixture readiness: %q, %v", line, err)
|
||||||
|
}
|
||||||
|
if _, err := adapter.CaptureDesktop(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if out, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("focus: %s, %v", out, err)
|
||||||
|
}
|
||||||
|
for rowIndex, label := range []string{"Server Alpha", "Server Beta", "Server Gamma, 界"} {
|
||||||
|
key := "Down"
|
||||||
|
if rowIndex == 0 {
|
||||||
|
key = "Home"
|
||||||
|
}
|
||||||
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", key).CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("select row: %s, %v", out, err)
|
||||||
|
}
|
||||||
|
selected:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatalf("row %d (%s) missing: %v", rowIndex, label, ctx.Err())
|
||||||
|
case event := <-adapter.Events():
|
||||||
|
cell := event.Source
|
||||||
|
if cell.ApplicationProcess != process || cell.Name != label || cell.TableColumnCount != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
want := strings.Join([]string{label, label, label}, ", ")
|
||||||
|
if process == "mumble" {
|
||||||
|
want = label
|
||||||
|
}
|
||||||
|
if rowIndex == 1 {
|
||||||
|
want = label + ", 12, 12"
|
||||||
|
}
|
||||||
|
if cell.TableRowText != want || !cell.GUITable || cell.TableRowIndex != rowIndex {
|
||||||
|
t.Fatalf("row %d context = %+v, want %q", rowIndex, cell, want)
|
||||||
|
}
|
||||||
|
break selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,31 @@ package atspi
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestMumbleTableRowText(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, process, label, row, want string
|
||||||
|
columns int
|
||||||
|
gui bool
|
||||||
|
}{
|
||||||
|
{"server label", "mumble", "Server Example", "Server Example, Server Example, Server Example", "Server Example", 3, true},
|
||||||
|
{"commas and Unicode", "mumble", "Server Example, 界", "Server Example, 界, Server Example, 界, Server Example, 界", "Server Example, 界", 3, true},
|
||||||
|
{"useful equal values", "mumble", "Server Example", "Server Example, 12, 12", "Server Example, 12, 12", 3, true},
|
||||||
|
{"mixed labels", "mumble", "Server Example", "Server Example, 12, Server Example", "Server Example, 12, Server Example", 3, true},
|
||||||
|
{"unrelated provider", "other", "Same", "Same, Same, Same", "Same, Same, Same", 3, true},
|
||||||
|
{"document table", "mumble", "Same", "Same, Same, Same", "Same, Same, Same", 3, false},
|
||||||
|
{"single column", "mumble", "Server Example", "Server Example", "Server Example", 1, true},
|
||||||
|
{"empty cells", "mumble", "", "", "", 3, true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
snapshot := Snapshot{ApplicationProcess: test.process, Role: "table cell", Name: test.label,
|
||||||
|
TableRowText: test.row, TableColumnCount: test.columns, GUITable: test.gui}
|
||||||
|
if got := mumbleTableRowText(snapshot); got != test.want {
|
||||||
|
t.Fatalf("row = %q, want %q", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMumbleMessageText(t *testing.T) {
|
func TestMumbleMessageText(t *testing.T) {
|
||||||
for _, test := range []struct{ input, want string }{
|
for _, test := range []struct{ input, want string }{
|
||||||
{"[12:34:56]", ""},
|
{"[12:34:56]", ""},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import "strings"
|
|||||||
|
|
||||||
const Name = "Cthulhu"
|
const Name = "Cthulhu"
|
||||||
|
|
||||||
const Version = "2026.11.06"
|
const Version = "2026.11.07"
|
||||||
const Channel = "testing"
|
const Channel = "testing"
|
||||||
|
|
||||||
var Build = Version
|
var Build = Version
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ environment = dict(os.environ, GOCACHE="/tmp/cthulhu-go-cache", GOPROXY="off",
|
|||||||
QT_LINUX_ACCESSIBILITY_ALWAYS_ON="1", NO_AT_BRIDGE="0")
|
QT_LINUX_ACCESSIBILITY_ALWAYS_ON="1", NO_AT_BRIDGE="0")
|
||||||
checks = [
|
checks = [
|
||||||
("./internal/atspi", "TestNativeMumbleTranscriptCapture"),
|
("./internal/atspi", "TestNativeMumbleTranscriptCapture"),
|
||||||
|
("./internal/atspi", "TestNativeMumbleTableRows"),
|
||||||
("./internal/atspi", "TestNativePidginTranscriptCapture"),
|
("./internal/atspi", "TestNativePidginTranscriptCapture"),
|
||||||
("./internal/app", "TestNativeMessagingCopyPreservesClipboardText"),
|
("./internal/app", "TestNativeMessagingCopyPreservesClipboardText"),
|
||||||
("./internal/platform/xorg", "TestNativeLearnModeCapturesKeysAndRestoresCommands"),
|
("./internal/platform/xorg", "TestNativeLearnModeCapturesKeysAndRestoresCommands"),
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2026 Storm Dragon
|
||||||
|
# Authors: Storm Dragon, Angus Kola
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
"""Private Qt table reproducing Mumble's repeated accessible column labels."""
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PySide6 import QtCore, QtWidgets
|
||||||
|
|
||||||
|
if ctypes.CDLL(None).prctl(15, sys.argv[1].encode(), 0, 0, 0) != 0:
|
||||||
|
raise RuntimeError("Cannot set fixture process identity")
|
||||||
|
|
||||||
|
application = QtWidgets.QApplication([])
|
||||||
|
window = QtWidgets.QMainWindow()
|
||||||
|
window.setWindowTitle("Server table fixture")
|
||||||
|
table = QtWidgets.QTreeWidget()
|
||||||
|
table.setAccessibleName("Server list")
|
||||||
|
table.setHeaderLabels(["Servername", "Ping", "Users"])
|
||||||
|
for labels in [
|
||||||
|
["Server Alpha"] * 3,
|
||||||
|
["Server Beta", "12", "12"],
|
||||||
|
["Server Gamma, 界"] * 3,
|
||||||
|
]:
|
||||||
|
item = QtWidgets.QTreeWidgetItem([labels[0], "12", "12"])
|
||||||
|
for columnIndex, label in enumerate(labels):
|
||||||
|
item.setData(columnIndex, QtCore.Qt.ItemDataRole.AccessibleTextRole, label)
|
||||||
|
table.addTopLevelItem(item)
|
||||||
|
window.setCentralWidget(table)
|
||||||
|
window.resize(500, 250)
|
||||||
|
window.show()
|
||||||
|
table.setFocus()
|
||||||
|
QtCore.QTimer.singleShot(0, lambda: print("ready", int(window.winId()), flush=True))
|
||||||
|
application.exec()
|
||||||
Reference in new issue
Block a user