From d148bc1a2decd34144efccbd9d6c4afe9d55d548 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:37:06 +0100 Subject: [PATCH 1/8] Try to fix QTreeWidget accessibility bug Co-authored-by: GitHub Copilot --- src/connectdlg.cpp | 80 +++++++++++++++++++++++++++++++++++++++++++--- src/connectdlg.h | 6 +++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 1c4404ea2b..bce5133f9e 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -74,22 +74,22 @@ static QString mapVersionStr ( const QString& versionStr ) // Subclass of QTreeWidgetItem that allows LVC_VERSION to sort by the UserRole data value CMappedTreeWidgetItem::CMappedTreeWidgetItem ( QTreeWidget* owner ) : QTreeWidgetItem ( owner ), owner ( owner ) {} -bool CMappedTreeWidgetItem::operator<( const QTreeWidgetItem& other ) const +bool CMappedTreeWidgetItem::operator< ( const QTreeWidgetItem& other ) const { if ( !owner ) - return QTreeWidgetItem::operator<( other ); + return QTreeWidgetItem::operator< ( other ); int column = owner->sortColumn(); // we only need this override for comparing server versions if ( column != CConnectDlg::LVC_VERSION ) - return QTreeWidgetItem::operator<( other ); + return QTreeWidgetItem::operator< ( other ); QVariant lhs = data ( column, Qt::UserRole ); QVariant rhs = other.data ( column, Qt::UserRole ); if ( !lhs.isValid() || !rhs.isValid() ) - return QTreeWidgetItem::operator<( other ); + return QTreeWidgetItem::operator< ( other ); return lhs.toString() < rhs.toString(); } @@ -243,6 +243,9 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // to get default return key behaviour working QObject::connect ( lvwServers, &QTreeWidget::activated, this, &CConnectDlg::OnConnectClicked ); + // connect selection change for accessibility support + QObject::connect ( lvwServers, &QTreeWidget::itemSelectionChanged, this, &CConnectDlg::OnServerListItemSelectionChanged ); + // line edit QObject::connect ( edtFilter, &QLineEdit::textEdited, this, &CConnectDlg::OnFilterTextEdited ); @@ -631,6 +634,75 @@ void CConnectDlg::OnServerAddrEditTextChanged ( const QString& ) lvwServers->clearSelection(); } +QString CConnectDlg::BuildAccessibleTextForItem ( const QTreeWidgetItem* pItem ) const +{ + // Return early if item is null to avoid crashes + if ( !pItem ) + { + return QString(); + } + + // Check if this is a child item (musician) or parent item (server). + // Child items have a parent and represent individual musicians connected to a server. + // Parent items represent servers and contain multiple columns with server information. + if ( pItem->parent() != nullptr ) + { + // This is a musician/child item - announce the musician name + return tr ( "Musician: %1" ).arg ( pItem->text ( LVC_NAME ) ); + } + + // This is a server item - build accessible text with all available information. + // Start with the server name, then append other columns that have data. + QString accessibleText = tr ( "Server: %1" ).arg ( pItem->text ( LVC_NAME ) ); + + // Helper lambda to append column data if it exists + auto appendColumnIfNotEmpty = [&] ( int column, const QString& label ) { + QString value = pItem->text ( column ); + if ( !value.isEmpty() ) + { + accessibleText += tr ( ", %1: %2" ).arg ( label ).arg ( value ); + } + }; + + // Append all server information columns in the order they appear in the UI + appendColumnIfNotEmpty ( LVC_PING, tr ( "Ping" ) ); + appendColumnIfNotEmpty ( LVC_CLIENTS, tr ( "Musicians" ) ); + appendColumnIfNotEmpty ( LVC_LOCATION, tr ( "Location" ) ); + appendColumnIfNotEmpty ( LVC_VERSION, tr ( "Version" ) ); + + return accessibleText; +} + +void CConnectDlg::UpdateAccessibilityForItem ( QTreeWidgetItem* pItem ) +{ + // Update the accessible description and notify the accessibility system + // when items are selected. This improves screen reader support on all platforms, + // including VoiceOver on macOS, NVDA/JAWS on Windows, and Orca on Linux. + if ( !pItem ) + { + return; + } + + QString accessibleText = BuildAccessibleTextForItem ( pItem ); + lvwServers->setAccessibleDescription ( accessibleText ); + QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lvwServers, accessibleText ) ); +} + +void CConnectDlg::OnServerListItemSelectionChanged() +{ + QList selectedItems = lvwServers->selectedItems(); + + if ( !selectedItems.isEmpty() ) + { + UpdateAccessibilityForItem ( selectedItems.first() ); + } + else + { + // Clear accessible description when nothing is selected + lvwServers->setAccessibleDescription ( tr ( "Server list" ) ); + } +} + void CConnectDlg::OnCustomDirectoriesChanged() { diff --git a/src/connectdlg.h b/src/connectdlg.h index 1a3b00abb7..a6244f8db7 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -32,6 +32,7 @@ #include #include #include +#include #include "global.h" #include "util.h" #include "settings.h" @@ -51,7 +52,7 @@ class CMappedTreeWidgetItem : public QTreeWidgetItem public: explicit CMappedTreeWidgetItem ( QTreeWidget* owner = nullptr ); - bool operator<( const QTreeWidgetItem& other ) const override; + bool operator< ( const QTreeWidgetItem& other ) const override; private: QTreeWidget* owner = nullptr; @@ -104,6 +105,8 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase void RequestServerList(); void EmitCLServerListPingMes ( const CHostAddress& haServerAddress, const bool bNeedVersion ); void UpdateDirectoryComboBox(); + QString BuildAccessibleTextForItem ( const QTreeWidgetItem* pItem ) const; + void UpdateAccessibilityForItem ( QTreeWidgetItem* pItem ); CClientSettings* pSettings; @@ -132,6 +135,7 @@ public slots: void OnDeleteServerAddrClicked(); void OnTimerPing(); void OnTimerReRequestServList(); + void OnServerListItemSelectionChanged(); signals: void ReqServerListQuery ( CHostAddress InetAddr ); From 46c45a392d8eb7d797b580f2e250eec85c4b023f Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:09:47 +0100 Subject: [PATCH 2/8] Add bare bones, AI generated accessibility button Co-authored-by: Copilot --- src/connectdlg.cpp | 329 ++++++++++++++++++++++++++++++++++++++------- src/connectdlg.h | 30 ++++- 2 files changed, 307 insertions(+), 52 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index bce5133f9e..db90643567 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -74,22 +74,22 @@ static QString mapVersionStr ( const QString& versionStr ) // Subclass of QTreeWidgetItem that allows LVC_VERSION to sort by the UserRole data value CMappedTreeWidgetItem::CMappedTreeWidgetItem ( QTreeWidget* owner ) : QTreeWidgetItem ( owner ), owner ( owner ) {} -bool CMappedTreeWidgetItem::operator< ( const QTreeWidgetItem& other ) const +bool CMappedTreeWidgetItem::operator<( const QTreeWidgetItem& other ) const { if ( !owner ) - return QTreeWidgetItem::operator< ( other ); + return QTreeWidgetItem::operator<( other ); int column = owner->sortColumn(); // we only need this override for comparing server versions if ( column != CConnectDlg::LVC_VERSION ) - return QTreeWidgetItem::operator< ( other ); + return QTreeWidgetItem::operator<( other ); QVariant lhs = data ( column, Qt::UserRole ); QVariant rhs = other.data ( column, Qt::UserRole ); if ( !lhs.isValid() || !rhs.isValid() ) - return QTreeWidgetItem::operator< ( other ); + return QTreeWidgetItem::operator<( other ); return lhs.toString() < rhs.toString(); } @@ -215,6 +215,90 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // per default the root shall not be decorated (to save space) lvwServers->setRootIsDecorated ( false ); +#ifdef USE_ACCESSIBLE_SERVER_LIST + // Create simplified accessible navigation panel for screen readers + // Design: One info label + 2 navigation buttons (Previous/Next) + Toggle button + + wAccessibleNavPanel = new QWidget ( this ); + wAccessibleNavPanel->setObjectName ( "wAccessibleNavPanel" ); + + QVBoxLayout* accessibleMainLayout = new QVBoxLayout ( wAccessibleNavPanel ); + accessibleMainLayout->setContentsMargins ( 0, 5, 0, 5 ); + + // Create read-only, focusable label showing current server information + // Using QLabel with focus policy so screenreaders can read it + lblAccessibleServerInfo = new QLabel ( tr ( "No server selected" ), wAccessibleNavPanel ); + lblAccessibleServerInfo->setObjectName ( "lblAccessibleServerInfo" ); + lblAccessibleServerInfo->setWordWrap ( true ); + lblAccessibleServerInfo->setFrameStyle ( QFrame::Panel | QFrame::Sunken ); + lblAccessibleServerInfo->setTextInteractionFlags ( Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard ); + lblAccessibleServerInfo->setMinimumHeight ( 50 ); + lblAccessibleServerInfo->setFocusPolicy ( Qt::StrongFocus ); // Make it focusable for screen readers + lblAccessibleServerInfo->setAccessibleName ( tr ( "Current server information" ) ); + lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Shows details of the currently selected server. Use Previous/Next buttons or Alt+Up/Down to navigate." ) ); + accessibleMainLayout->addWidget ( lblAccessibleServerInfo ); + + // Create horizontal layout for navigation buttons (Previous, Next) + QHBoxLayout* navLayout = new QHBoxLayout(); + + butAccessiblePrevious = new QPushButton ( u8"\u2190 " + tr ( "Previous Server" ), wAccessibleNavPanel ); + butAccessiblePrevious->setObjectName ( "butAccessiblePrevious" ); + butAccessiblePrevious->setAccessibleName ( tr ( "Go to previous server" ) ); + butAccessiblePrevious->setAccessibleDescription ( tr ( "Navigate to the previous server in the list" ) ); + butAccessiblePrevious->setShortcut ( QKeySequence ( Qt::ALT | Qt::Key_Up ) ); + butAccessiblePrevious->setToolTip ( tr ( "Previous server (Alt+Up)" ) ); + navLayout->addWidget ( butAccessiblePrevious, 1 ); + + butAccessibleNext = new QPushButton ( tr ( "Next Server" ) + u8" \u2192", wAccessibleNavPanel ); + butAccessibleNext->setObjectName ( "butAccessibleNext" ); + butAccessibleNext->setAccessibleName ( tr ( "Go to next server" ) ); + butAccessibleNext->setAccessibleDescription ( tr ( "Navigate to the next server in the list" ) ); + butAccessibleNext->setShortcut ( QKeySequence ( Qt::ALT | Qt::Key_Down ) ); + butAccessibleNext->setToolTip ( tr ( "Next server (Alt+Down)" ) ); + navLayout->addWidget ( butAccessibleNext, 1 ); + + accessibleMainLayout->addLayout ( navLayout ); + + // Create toggle button + butToggleAccessible = new QPushButton ( u8"\u25BC " + tr ( "Hide Accessible Controls" ), this ); + butToggleAccessible->setObjectName ( "butToggleAccessible" ); + butToggleAccessible->setAccessibleName ( tr ( "Toggle accessible controls" ) ); + butToggleAccessible->setAccessibleDescription ( tr ( "Show or hide the accessible navigation panel for screen readers" ) ); + butToggleAccessible->setCheckable ( true ); + butToggleAccessible->setChecked ( true ); + butToggleAccessible->setToolTip ( tr ( "Toggle accessible controls" ) ); + + // Insert the accessible panel and toggle button into the layout right after the tree widget + QVBoxLayout* mainLayout = qobject_cast ( layout() ); + if ( mainLayout ) + { + // Find the tree widget in the layout + bool inserted = false; + for ( int i = 0; i < mainLayout->count(); ++i ) + { + QLayoutItem* item = mainLayout->itemAt ( i ); + if ( item && item->widget() == lvwServers ) + { + mainLayout->insertWidget ( i + 1, butToggleAccessible ); + mainLayout->insertWidget ( i + 2, wAccessibleNavPanel ); + inserted = true; + break; + } + } + if ( !inserted ) + { + qWarning ( "Accessible navigation panel could not be inserted: tree widget not found in layout." ); + } + } + else + { + qWarning ( "Accessible navigation panel could not be inserted: main layout cast failed." ); + } + + // Initially show the accessible panel + wAccessibleNavPanel->setVisible ( true ); +#endif + // make sure the connect button has the focus butConnect->setFocus(); @@ -269,6 +353,13 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing ); QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList ); + +#ifdef USE_ACCESSIBLE_SERVER_LIST + // accessible navigation panel + QObject::connect ( butAccessiblePrevious, &QPushButton::clicked, this, &CConnectDlg::OnAccessiblePreviousClicked ); + QObject::connect ( butAccessibleNext, &QPushButton::clicked, this, &CConnectDlg::OnAccessibleNextClicked ); + QObject::connect ( butToggleAccessible, &QPushButton::clicked, this, &CConnectDlg::OnToggleAccessibleClicked ); +#endif } void CConnectDlg::showEvent ( QShowEvent* ) @@ -634,74 +725,214 @@ void CConnectDlg::OnServerAddrEditTextChanged ( const QString& ) lvwServers->clearSelection(); } -QString CConnectDlg::BuildAccessibleTextForItem ( const QTreeWidgetItem* pItem ) const +void CConnectDlg::OnServerListItemSelectionChanged() +{ +#ifdef USE_ACCESSIBLE_SERVER_LIST + UpdateAccessibleServerInfo(); +#endif +} + +#ifdef USE_ACCESSIBLE_SERVER_LIST +void CConnectDlg::UpdateAccessibleServerInfo() { - // Return early if item is null to avoid crashes - if ( !pItem ) + QList selectedItems = lvwServers->selectedItems(); + + if ( !selectedItems.isEmpty() && selectedItems.first()->parent() == nullptr ) { - return QString(); + // We have a server item selected (not a musician child item) + QTreeWidgetItem* pItem = selectedItems.first(); + + // Extract server information + QString serverName = pItem->text ( LVC_NAME ); + QString pingTime = pItem->text ( LVC_PING ); + QString musicians = pItem->text ( LVC_CLIENTS ); + QString location = pItem->text ( LVC_LOCATION ); + QString version = pItem->text ( LVC_VERSION ); + + // Build comprehensive text for the label + QString labelText = tr ( "Server: %1" ).arg ( serverName ); + if ( !pingTime.isEmpty() ) + { + labelText += tr ( "
Ping: %1" ).arg ( pingTime ); + } + if ( !musicians.isEmpty() ) + { + labelText += tr ( "   Musicians: %1" ).arg ( musicians ); + } + if ( !location.isEmpty() ) + { + labelText += tr ( "
Location: %1" ).arg ( location ); + } + if ( !version.isEmpty() ) + { + labelText += tr ( "   Version: %1" ).arg ( version ); + } + + // Build text for screen readers (without HTML) + QString accessibleText = tr ( "Server: %1" ).arg ( serverName ); + if ( !pingTime.isEmpty() ) + { + accessibleText += tr ( ", Ping: %1" ).arg ( pingTime ); + } + if ( !musicians.isEmpty() ) + { + accessibleText += tr ( ", Musicians: %1" ).arg ( musicians ); + } + if ( !location.isEmpty() ) + { + accessibleText += tr ( ", Location: %1" ).arg ( location ); + } + if ( !version.isEmpty() ) + { + accessibleText += tr ( ", Version: %1" ).arg ( version ); + } + + // Update label + lblAccessibleServerInfo->setText ( labelText ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) ); + lblAccessibleServerInfo->setAccessibleDescription ( accessibleText ); + + // Update navigation buttons to show previous/next server names + int currentIndex = lvwServers->indexOfTopLevelItem ( pItem ); + + // Update "Previous" button with previous server name + if ( currentIndex > 0 ) + { + QTreeWidgetItem* prevItem = lvwServers->topLevelItem ( currentIndex - 1 ); + QString prevName = prevItem->text ( LVC_NAME ); + butAccessiblePrevious->setText ( u8"\u2190 " + prevName ); + butAccessiblePrevious->setAccessibleName ( tr ( "Previous server: %1" ).arg ( prevName ) ); + butAccessiblePrevious->setAccessibleDescription ( tr ( "Go to previous server: %1" ).arg ( prevName ) ); + butAccessiblePrevious->setEnabled ( true ); + } + else + { + butAccessiblePrevious->setText ( u8"\u2190 " + tr ( "(first)" ) ); + butAccessiblePrevious->setAccessibleName ( tr ( "No previous server - at first server" ) ); + butAccessiblePrevious->setAccessibleDescription ( tr ( "Cannot go back, already at first server" ) ); + butAccessiblePrevious->setEnabled ( false ); + } + + // Update "Next" button with next server name + if ( currentIndex < lvwServers->topLevelItemCount() - 1 ) + { + QTreeWidgetItem* nextItem = lvwServers->topLevelItem ( currentIndex + 1 ); + QString nextName = nextItem->text ( LVC_NAME ); + butAccessibleNext->setText ( nextName + u8" \u2192" ); + butAccessibleNext->setAccessibleName ( tr ( "Next server: %1" ).arg ( nextName ) ); + butAccessibleNext->setAccessibleDescription ( tr ( "Go to next server: %1" ).arg ( nextName ) ); + butAccessibleNext->setEnabled ( true ); + } + else + { + butAccessibleNext->setText ( tr ( "(last)" ) + u8" \u2192" ); + butAccessibleNext->setAccessibleName ( tr ( "No next server - at last server" ) ); + butAccessibleNext->setAccessibleDescription ( tr ( "Cannot go forward, already at last server" ) ); + butAccessibleNext->setEnabled ( false ); + } + + // Force VoiceOver to announce the change + QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, accessibleText ) ); } - - // Check if this is a child item (musician) or parent item (server). - // Child items have a parent and represent individual musicians connected to a server. - // Parent items represent servers and contain multiple columns with server information. - if ( pItem->parent() != nullptr ) + else { - // This is a musician/child item - announce the musician name - return tr ( "Musician: %1" ).arg ( pItem->text ( LVC_NAME ) ); + // No server selected or musician child selected + lblAccessibleServerInfo->setText ( tr ( "No server selected or in musician selection" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected" ) ); + lblAccessibleServerInfo->setAccessibleDescription ( tr ( "No server selected. Use Previous/Next buttons or Alt+Up/Down to navigate servers." ) ); + + // Reset navigation buttons + butAccessiblePrevious->setText ( u8"\u2190 " + tr ( "Previous" ) ); + butAccessiblePrevious->setAccessibleName ( tr ( "Navigate to previous server" ) ); + butAccessiblePrevious->setEnabled ( lvwServers->topLevelItemCount() > 0 ); + + butAccessibleNext->setText ( tr ( "Next" ) + u8" \u2192" ); + butAccessibleNext->setAccessibleName ( tr ( "Navigate to next server" ) ); + butAccessibleNext->setEnabled ( lvwServers->topLevelItemCount() > 0 ); + } +} - // This is a server item - build accessible text with all available information. - // Start with the server name, then append other columns that have data. - QString accessibleText = tr ( "Server: %1" ).arg ( pItem->text ( LVC_NAME ) ); - - // Helper lambda to append column data if it exists - auto appendColumnIfNotEmpty = [&] ( int column, const QString& label ) { - QString value = pItem->text ( column ); - if ( !value.isEmpty() ) +void CConnectDlg::OnAccessiblePreviousClicked() +{ + // Navigate to previous server + QList selectedItems = lvwServers->selectedItems(); + QTreeWidgetItem* currentItem = selectedItems.isEmpty() ? nullptr : selectedItems.first(); + + // Get current item or first item if none selected + if ( currentItem == nullptr ) + { + // Select first item + if ( lvwServers->topLevelItemCount() > 0 ) { - accessibleText += tr ( ", %1: %2" ).arg ( label ).arg ( value ); + lvwServers->setCurrentItem ( lvwServers->topLevelItem ( 0 ) ); } - }; - - // Append all server information columns in the order they appear in the UI - appendColumnIfNotEmpty ( LVC_PING, tr ( "Ping" ) ); - appendColumnIfNotEmpty ( LVC_CLIENTS, tr ( "Musicians" ) ); - appendColumnIfNotEmpty ( LVC_LOCATION, tr ( "Location" ) ); - appendColumnIfNotEmpty ( LVC_VERSION, tr ( "Version" ) ); - - return accessibleText; + return; + } + + // If current item is a musician (child), move to parent + if ( currentItem->parent() != nullptr ) + { + lvwServers->setCurrentItem ( currentItem->parent() ); + return; + } + + // Get previous server item + int currentIndex = lvwServers->indexOfTopLevelItem ( currentItem ); + if ( currentIndex > 0 ) + { + lvwServers->setCurrentItem ( lvwServers->topLevelItem ( currentIndex - 1 ) ); + } } -void CConnectDlg::UpdateAccessibilityForItem ( QTreeWidgetItem* pItem ) +void CConnectDlg::OnAccessibleNextClicked() { - // Update the accessible description and notify the accessibility system - // when items are selected. This improves screen reader support on all platforms, - // including VoiceOver on macOS, NVDA/JAWS on Windows, and Orca on Linux. - if ( !pItem ) + // Navigate to next server + QList selectedItems = lvwServers->selectedItems(); + QTreeWidgetItem* currentItem = selectedItems.isEmpty() ? nullptr : selectedItems.first(); + + // Get current item or first item if none selected + if ( currentItem == nullptr ) { + // Select first item + if ( lvwServers->topLevelItemCount() > 0 ) + { + lvwServers->setCurrentItem ( lvwServers->topLevelItem ( 0 ) ); + } return; } - - QString accessibleText = BuildAccessibleTextForItem ( pItem ); - lvwServers->setAccessibleDescription ( accessibleText ); - QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lvwServers, accessibleText ) ); + + // If current item is a musician (child), find next server + if ( currentItem->parent() != nullptr ) + { + currentItem = currentItem->parent(); + } + + // Find next server item (skip musician children) + int currentIndex = lvwServers->indexOfTopLevelItem ( currentItem ); + if ( currentIndex >= 0 && currentIndex < lvwServers->topLevelItemCount() - 1 ) + { + lvwServers->setCurrentItem ( lvwServers->topLevelItem ( currentIndex + 1 ) ); + } } -void CConnectDlg::OnServerListItemSelectionChanged() +void CConnectDlg::OnToggleAccessibleClicked() { - QList selectedItems = lvwServers->selectedItems(); - - if ( !selectedItems.isEmpty() ) + bool isVisible = wAccessibleNavPanel->isVisible(); + wAccessibleNavPanel->setVisible ( !isVisible ); + + if ( isVisible ) { - UpdateAccessibilityForItem ( selectedItems.first() ); + butToggleAccessible->setText ( u8"\u25B6 " + tr ( "Show Accessible Controls" ) ); + butToggleAccessible->setAccessibleDescription ( tr ( "Show the accessible navigation controls" ) ); } else { - // Clear accessible description when nothing is selected - lvwServers->setAccessibleDescription ( tr ( "Server list" ) ); + butToggleAccessible->setText ( u8"\u25BC " + tr ( "Hide Accessible Controls" ) ); + butToggleAccessible->setAccessibleDescription ( tr ( "Hide the accessible navigation controls" ) ); } } +#endif void CConnectDlg::OnCustomDirectoriesChanged() { diff --git a/src/connectdlg.h b/src/connectdlg.h index a6244f8db7..3e0d483b7b 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -24,6 +24,11 @@ #pragma once +// Enable accessible server list by default (can be disabled with -DDISABLE_ACCESSIBLE_SERVER_LIST) +#ifndef DISABLE_ACCESSIBLE_SERVER_LIST +# define USE_ACCESSIBLE_SERVER_LIST +#endif + #include #include #include @@ -33,6 +38,8 @@ #include #include #include +#include +#include #include "global.h" #include "util.h" #include "settings.h" @@ -52,7 +59,7 @@ class CMappedTreeWidgetItem : public QTreeWidgetItem public: explicit CMappedTreeWidgetItem ( QTreeWidget* owner = nullptr ); - bool operator< ( const QTreeWidgetItem& other ) const override; + bool operator<( const QTreeWidgetItem& other ) const override; private: QTreeWidget* owner = nullptr; @@ -105,11 +112,22 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase void RequestServerList(); void EmitCLServerListPingMes ( const CHostAddress& haServerAddress, const bool bNeedVersion ); void UpdateDirectoryComboBox(); - QString BuildAccessibleTextForItem ( const QTreeWidgetItem* pItem ) const; - void UpdateAccessibilityForItem ( QTreeWidgetItem* pItem ); + +#ifdef USE_ACCESSIBLE_SERVER_LIST + void UpdateAccessibleServerInfo(); +#endif CClientSettings* pSettings; +#ifdef USE_ACCESSIBLE_SERVER_LIST + // Accessible navigation panel for screen readers + QWidget* wAccessibleNavPanel; // Container for accessible navigation + QLabel* lblAccessibleServerInfo; // Label showing current server information (read-only) + QPushButton* butAccessiblePrevious; // Button to navigate to previous server + QPushButton* butAccessibleNext; // Button to navigate to next server + QPushButton* butToggleAccessible; // Button to show/hide accessible panel +#endif + QTimer TimerPing; QTimer TimerReRequestServList; QTimer TimerInitialSort; @@ -137,6 +155,12 @@ public slots: void OnTimerReRequestServList(); void OnServerListItemSelectionChanged(); +#ifdef USE_ACCESSIBLE_SERVER_LIST + void OnAccessiblePreviousClicked(); + void OnAccessibleNextClicked(); + void OnToggleAccessibleClicked(); +#endif + signals: void ReqServerListQuery ( CHostAddress InetAddr ); void CreateCLServerListPingMes ( CHostAddress InetAddr ); From 867e87c3c295a26e3630e72d0bd8a91f51a860db Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:32:41 +0100 Subject: [PATCH 3/8] Clean up probably invalid text --- src/connectdlg.cpp | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index db90643567..dc45a839d9 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -768,29 +768,10 @@ void CConnectDlg::UpdateAccessibleServerInfo() labelText += tr ( "   Version: %1" ).arg ( version ); } - // Build text for screen readers (without HTML) - QString accessibleText = tr ( "Server: %1" ).arg ( serverName ); - if ( !pingTime.isEmpty() ) - { - accessibleText += tr ( ", Ping: %1" ).arg ( pingTime ); - } - if ( !musicians.isEmpty() ) - { - accessibleText += tr ( ", Musicians: %1" ).arg ( musicians ); - } - if ( !location.isEmpty() ) - { - accessibleText += tr ( ", Location: %1" ).arg ( location ); - } - if ( !version.isEmpty() ) - { - accessibleText += tr ( ", Version: %1" ).arg ( version ); - } - // Update label lblAccessibleServerInfo->setText ( labelText ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) ); - lblAccessibleServerInfo->setAccessibleDescription ( accessibleText ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Seelected server information box" ) ); + lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Show stats for selected server" ) ); // Update navigation buttons to show previous/next server names int currentIndex = lvwServers->indexOfTopLevelItem ( pItem ); From 61013758c300abd16f7e71c89a9d3c4c2db2db66 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:37:55 +0100 Subject: [PATCH 4/8] Fix bug --- src/connectdlg.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index dc45a839d9..e00e2c128f 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -740,8 +740,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() if ( !selectedItems.isEmpty() && selectedItems.first()->parent() == nullptr ) { // We have a server item selected (not a musician child item) - QTreeWidgetItem* pItem = selectedItems.first(); - + QTreeWidgetItem* pItem = selectedItems.first(); // Extract server information QString serverName = pItem->text ( LVC_NAME ); QString pingTime = pItem->text ( LVC_PING ); @@ -770,7 +769,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() // Update label lblAccessibleServerInfo->setText ( labelText ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "Seelected server information box" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server information box" ) ); lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Show stats for selected server" ) ); // Update navigation buttons to show previous/next server names @@ -813,7 +812,8 @@ void CConnectDlg::UpdateAccessibleServerInfo() } // Force VoiceOver to announce the change - QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, accessibleText ) ); + // TODO: Find out in how far this call is needed. + QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, tr ( "Selected server information box" ) ) ); } else { From e6d543a036c1ed0e441c1b7691b854ac6a4c1723 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:48:35 +0100 Subject: [PATCH 5/8] Revert "Fix bug" This reverts commit 61013758c300abd16f7e71c89a9d3c4c2db2db66. --- src/connectdlg.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index e00e2c128f..dc45a839d9 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -740,7 +740,8 @@ void CConnectDlg::UpdateAccessibleServerInfo() if ( !selectedItems.isEmpty() && selectedItems.first()->parent() == nullptr ) { // We have a server item selected (not a musician child item) - QTreeWidgetItem* pItem = selectedItems.first(); + QTreeWidgetItem* pItem = selectedItems.first(); + // Extract server information QString serverName = pItem->text ( LVC_NAME ); QString pingTime = pItem->text ( LVC_PING ); @@ -769,7 +770,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() // Update label lblAccessibleServerInfo->setText ( labelText ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server information box" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Seelected server information box" ) ); lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Show stats for selected server" ) ); // Update navigation buttons to show previous/next server names @@ -812,8 +813,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() } // Force VoiceOver to announce the change - // TODO: Find out in how far this call is needed. - QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, tr ( "Selected server information box" ) ) ); + QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, accessibleText ) ); } else { From 1d39c1d0d3a481a8381b59e05c6c40594bdc6667 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:49:01 +0100 Subject: [PATCH 6/8] Revert "Clean up probably invalid text" This reverts commit 867e87c3c295a26e3630e72d0bd8a91f51a860db. --- src/connectdlg.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index dc45a839d9..db90643567 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -768,10 +768,29 @@ void CConnectDlg::UpdateAccessibleServerInfo() labelText += tr ( "   Version: %1" ).arg ( version ); } + // Build text for screen readers (without HTML) + QString accessibleText = tr ( "Server: %1" ).arg ( serverName ); + if ( !pingTime.isEmpty() ) + { + accessibleText += tr ( ", Ping: %1" ).arg ( pingTime ); + } + if ( !musicians.isEmpty() ) + { + accessibleText += tr ( ", Musicians: %1" ).arg ( musicians ); + } + if ( !location.isEmpty() ) + { + accessibleText += tr ( ", Location: %1" ).arg ( location ); + } + if ( !version.isEmpty() ) + { + accessibleText += tr ( ", Version: %1" ).arg ( version ); + } + // Update label lblAccessibleServerInfo->setText ( labelText ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "Seelected server information box" ) ); - lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Show stats for selected server" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) ); + lblAccessibleServerInfo->setAccessibleDescription ( accessibleText ); // Update navigation buttons to show previous/next server names int currentIndex = lvwServers->indexOfTopLevelItem ( pItem ); From 2ba71f7b39b507ddeb0418804a863552fa403f58 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:58:16 +0100 Subject: [PATCH 7/8] Simplify text --- src/connectdlg.cpp | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index db90643567..824b8faca3 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -747,28 +747,9 @@ void CConnectDlg::UpdateAccessibleServerInfo() QString pingTime = pItem->text ( LVC_PING ); QString musicians = pItem->text ( LVC_CLIENTS ); QString location = pItem->text ( LVC_LOCATION ); - QString version = pItem->text ( LVC_VERSION ); + QString version = pItem->text ( LVC_VERSION ); - // Build comprehensive text for the label - QString labelText = tr ( "Server: %1" ).arg ( serverName ); - if ( !pingTime.isEmpty() ) - { - labelText += tr ( "
Ping: %1" ).arg ( pingTime ); - } - if ( !musicians.isEmpty() ) - { - labelText += tr ( "   Musicians: %1" ).arg ( musicians ); - } - if ( !location.isEmpty() ) - { - labelText += tr ( "
Location: %1" ).arg ( location ); - } - if ( !version.isEmpty() ) - { - labelText += tr ( "   Version: %1" ).arg ( version ); - } - - // Build text for screen readers (without HTML) + // Build text for screen readers QString accessibleText = tr ( "Server: %1" ).arg ( serverName ); if ( !pingTime.isEmpty() ) { @@ -788,7 +769,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() } // Update label - lblAccessibleServerInfo->setText ( labelText ); + lblAccessibleServerInfo->setText ( accessibleText ); lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) ); lblAccessibleServerInfo->setAccessibleDescription ( accessibleText ); @@ -838,7 +819,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() { // No server selected or musician child selected lblAccessibleServerInfo->setText ( tr ( "No server selected or in musician selection" ) ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected or in musician selection" ) ); lblAccessibleServerInfo->setAccessibleDescription ( tr ( "No server selected. Use Previous/Next buttons or Alt+Up/Down to navigate servers." ) ); // Reset navigation buttons From 0be5b53dfee8eaa9d0328202f9934bf142b88103 Mon Sep 17 00:00:00 2001 From: ann0see <20726856+ann0see@users.noreply.github.com> Date: Sat, 6 Dec 2025 17:12:31 +0100 Subject: [PATCH 8/8] Move around layout for more intuitive screen reader use --- src/connectdlg.cpp | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index 824b8faca3..4828f1b825 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -225,19 +225,6 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QVBoxLayout* accessibleMainLayout = new QVBoxLayout ( wAccessibleNavPanel ); accessibleMainLayout->setContentsMargins ( 0, 5, 0, 5 ); - // Create read-only, focusable label showing current server information - // Using QLabel with focus policy so screenreaders can read it - lblAccessibleServerInfo = new QLabel ( tr ( "No server selected" ), wAccessibleNavPanel ); - lblAccessibleServerInfo->setObjectName ( "lblAccessibleServerInfo" ); - lblAccessibleServerInfo->setWordWrap ( true ); - lblAccessibleServerInfo->setFrameStyle ( QFrame::Panel | QFrame::Sunken ); - lblAccessibleServerInfo->setTextInteractionFlags ( Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard ); - lblAccessibleServerInfo->setMinimumHeight ( 50 ); - lblAccessibleServerInfo->setFocusPolicy ( Qt::StrongFocus ); // Make it focusable for screen readers - lblAccessibleServerInfo->setAccessibleName ( tr ( "Current server information" ) ); - lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Shows details of the currently selected server. Use Previous/Next buttons or Alt+Up/Down to navigate." ) ); - accessibleMainLayout->addWidget ( lblAccessibleServerInfo ); - // Create horizontal layout for navigation buttons (Previous, Next) QHBoxLayout* navLayout = new QHBoxLayout(); @@ -258,7 +245,20 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR navLayout->addWidget ( butAccessibleNext, 1 ); accessibleMainLayout->addLayout ( navLayout ); - + + // Create read-only, focusable label showing current server information + // Using QLabel with focus policy so screenreaders can read it + lblAccessibleServerInfo = new QLabel ( tr ( "No server selected" ), wAccessibleNavPanel ); + lblAccessibleServerInfo->setObjectName ( "lblAccessibleServerInfo" ); + lblAccessibleServerInfo->setWordWrap ( true ); + lblAccessibleServerInfo->setFrameStyle ( QFrame::Panel | QFrame::Sunken ); + lblAccessibleServerInfo->setTextInteractionFlags ( Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard ); + lblAccessibleServerInfo->setMinimumHeight ( 50 ); + lblAccessibleServerInfo->setFocusPolicy ( Qt::StrongFocus ); // Make it focusable for screen readers + lblAccessibleServerInfo->setAccessibleName ( tr ( "Current server information" ) ); + lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Shows details of the currently selected server. Use Previous/Next buttons or Alt+Up/Down to navigate." ) ); + accessibleMainLayout->addWidget ( lblAccessibleServerInfo ); + // Create toggle button butToggleAccessible = new QPushButton ( u8"\u25BC " + tr ( "Hide Accessible Controls" ), this ); butToggleAccessible->setObjectName ( "butToggleAccessible" ); @@ -770,7 +770,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() // Update label lblAccessibleServerInfo->setText ( accessibleText ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server information" ) ); lblAccessibleServerInfo->setAccessibleDescription ( accessibleText ); // Update navigation buttons to show previous/next server names @@ -816,12 +816,7 @@ void CConnectDlg::UpdateAccessibleServerInfo() QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, accessibleText ) ); } else - { - // No server selected or musician child selected - lblAccessibleServerInfo->setText ( tr ( "No server selected or in musician selection" ) ); - lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected or in musician selection" ) ); - lblAccessibleServerInfo->setAccessibleDescription ( tr ( "No server selected. Use Previous/Next buttons or Alt+Up/Down to navigate servers." ) ); - + { // Reset navigation buttons butAccessiblePrevious->setText ( u8"\u2190 " + tr ( "Previous" ) ); butAccessiblePrevious->setAccessibleName ( tr ( "Navigate to previous server" ) ); @@ -830,6 +825,11 @@ void CConnectDlg::UpdateAccessibleServerInfo() butAccessibleNext->setText ( tr ( "Next" ) + u8" \u2192" ); butAccessibleNext->setAccessibleName ( tr ( "Navigate to next server" ) ); butAccessibleNext->setEnabled ( lvwServers->topLevelItemCount() > 0 ); + + // No server selected or musician child selected + lblAccessibleServerInfo->setText ( tr ( "No server selected or in musician selection" ) ); + lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected or in musician selection" ) ); + lblAccessibleServerInfo->setAccessibleDescription ( tr ( "No server selected. Use Previous/Next buttons or Alt+Up/Down to navigate servers." ) ); } }