Skip to content

Commit 46c45a3

Browse files
committed
Add bare bones, AI generated accessibility button
Co-authored-by: Copilot
1 parent d148bc1 commit 46c45a3

2 files changed

Lines changed: 307 additions & 52 deletions

File tree

src/connectdlg.cpp

Lines changed: 280 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,22 @@ static QString mapVersionStr ( const QString& versionStr )
7474
// Subclass of QTreeWidgetItem that allows LVC_VERSION to sort by the UserRole data value
7575
CMappedTreeWidgetItem::CMappedTreeWidgetItem ( QTreeWidget* owner ) : QTreeWidgetItem ( owner ), owner ( owner ) {}
7676

77-
bool CMappedTreeWidgetItem::operator< ( const QTreeWidgetItem& other ) const
77+
bool CMappedTreeWidgetItem::operator<( const QTreeWidgetItem& other ) const
7878
{
7979
if ( !owner )
80-
return QTreeWidgetItem::operator< ( other );
80+
return QTreeWidgetItem::operator<( other );
8181

8282
int column = owner->sortColumn();
8383

8484
// we only need this override for comparing server versions
8585
if ( column != CConnectDlg::LVC_VERSION )
86-
return QTreeWidgetItem::operator< ( other );
86+
return QTreeWidgetItem::operator<( other );
8787

8888
QVariant lhs = data ( column, Qt::UserRole );
8989
QVariant rhs = other.data ( column, Qt::UserRole );
9090

9191
if ( !lhs.isValid() || !rhs.isValid() )
92-
return QTreeWidgetItem::operator< ( other );
92+
return QTreeWidgetItem::operator<( other );
9393

9494
return lhs.toString() < rhs.toString();
9595
}
@@ -215,6 +215,90 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR
215215
// per default the root shall not be decorated (to save space)
216216
lvwServers->setRootIsDecorated ( false );
217217

218+
#ifdef USE_ACCESSIBLE_SERVER_LIST
219+
// Create simplified accessible navigation panel for screen readers
220+
// Design: One info label + 2 navigation buttons (Previous/Next) + Toggle button
221+
222+
wAccessibleNavPanel = new QWidget ( this );
223+
wAccessibleNavPanel->setObjectName ( "wAccessibleNavPanel" );
224+
225+
QVBoxLayout* accessibleMainLayout = new QVBoxLayout ( wAccessibleNavPanel );
226+
accessibleMainLayout->setContentsMargins ( 0, 5, 0, 5 );
227+
228+
// Create read-only, focusable label showing current server information
229+
// Using QLabel with focus policy so screenreaders can read it
230+
lblAccessibleServerInfo = new QLabel ( tr ( "No server selected" ), wAccessibleNavPanel );
231+
lblAccessibleServerInfo->setObjectName ( "lblAccessibleServerInfo" );
232+
lblAccessibleServerInfo->setWordWrap ( true );
233+
lblAccessibleServerInfo->setFrameStyle ( QFrame::Panel | QFrame::Sunken );
234+
lblAccessibleServerInfo->setTextInteractionFlags ( Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard );
235+
lblAccessibleServerInfo->setMinimumHeight ( 50 );
236+
lblAccessibleServerInfo->setFocusPolicy ( Qt::StrongFocus ); // Make it focusable for screen readers
237+
lblAccessibleServerInfo->setAccessibleName ( tr ( "Current server information" ) );
238+
lblAccessibleServerInfo->setAccessibleDescription ( tr ( "Shows details of the currently selected server. Use Previous/Next buttons or Alt+Up/Down to navigate." ) );
239+
accessibleMainLayout->addWidget ( lblAccessibleServerInfo );
240+
241+
// Create horizontal layout for navigation buttons (Previous, Next)
242+
QHBoxLayout* navLayout = new QHBoxLayout();
243+
244+
butAccessiblePrevious = new QPushButton ( u8"\u2190 " + tr ( "Previous Server" ), wAccessibleNavPanel );
245+
butAccessiblePrevious->setObjectName ( "butAccessiblePrevious" );
246+
butAccessiblePrevious->setAccessibleName ( tr ( "Go to previous server" ) );
247+
butAccessiblePrevious->setAccessibleDescription ( tr ( "Navigate to the previous server in the list" ) );
248+
butAccessiblePrevious->setShortcut ( QKeySequence ( Qt::ALT | Qt::Key_Up ) );
249+
butAccessiblePrevious->setToolTip ( tr ( "Previous server (Alt+Up)" ) );
250+
navLayout->addWidget ( butAccessiblePrevious, 1 );
251+
252+
butAccessibleNext = new QPushButton ( tr ( "Next Server" ) + u8" \u2192", wAccessibleNavPanel );
253+
butAccessibleNext->setObjectName ( "butAccessibleNext" );
254+
butAccessibleNext->setAccessibleName ( tr ( "Go to next server" ) );
255+
butAccessibleNext->setAccessibleDescription ( tr ( "Navigate to the next server in the list" ) );
256+
butAccessibleNext->setShortcut ( QKeySequence ( Qt::ALT | Qt::Key_Down ) );
257+
butAccessibleNext->setToolTip ( tr ( "Next server (Alt+Down)" ) );
258+
navLayout->addWidget ( butAccessibleNext, 1 );
259+
260+
accessibleMainLayout->addLayout ( navLayout );
261+
262+
// Create toggle button
263+
butToggleAccessible = new QPushButton ( u8"\u25BC " + tr ( "Hide Accessible Controls" ), this );
264+
butToggleAccessible->setObjectName ( "butToggleAccessible" );
265+
butToggleAccessible->setAccessibleName ( tr ( "Toggle accessible controls" ) );
266+
butToggleAccessible->setAccessibleDescription ( tr ( "Show or hide the accessible navigation panel for screen readers" ) );
267+
butToggleAccessible->setCheckable ( true );
268+
butToggleAccessible->setChecked ( true );
269+
butToggleAccessible->setToolTip ( tr ( "Toggle accessible controls" ) );
270+
271+
// Insert the accessible panel and toggle button into the layout right after the tree widget
272+
QVBoxLayout* mainLayout = qobject_cast<QVBoxLayout*> ( layout() );
273+
if ( mainLayout )
274+
{
275+
// Find the tree widget in the layout
276+
bool inserted = false;
277+
for ( int i = 0; i < mainLayout->count(); ++i )
278+
{
279+
QLayoutItem* item = mainLayout->itemAt ( i );
280+
if ( item && item->widget() == lvwServers )
281+
{
282+
mainLayout->insertWidget ( i + 1, butToggleAccessible );
283+
mainLayout->insertWidget ( i + 2, wAccessibleNavPanel );
284+
inserted = true;
285+
break;
286+
}
287+
}
288+
if ( !inserted )
289+
{
290+
qWarning ( "Accessible navigation panel could not be inserted: tree widget not found in layout." );
291+
}
292+
}
293+
else
294+
{
295+
qWarning ( "Accessible navigation panel could not be inserted: main layout cast failed." );
296+
}
297+
298+
// Initially show the accessible panel
299+
wAccessibleNavPanel->setVisible ( true );
300+
#endif
301+
218302
// make sure the connect button has the focus
219303
butConnect->setFocus();
220304

@@ -269,6 +353,13 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR
269353
QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing );
270354

271355
QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList );
356+
357+
#ifdef USE_ACCESSIBLE_SERVER_LIST
358+
// accessible navigation panel
359+
QObject::connect ( butAccessiblePrevious, &QPushButton::clicked, this, &CConnectDlg::OnAccessiblePreviousClicked );
360+
QObject::connect ( butAccessibleNext, &QPushButton::clicked, this, &CConnectDlg::OnAccessibleNextClicked );
361+
QObject::connect ( butToggleAccessible, &QPushButton::clicked, this, &CConnectDlg::OnToggleAccessibleClicked );
362+
#endif
272363
}
273364

274365
void CConnectDlg::showEvent ( QShowEvent* )
@@ -634,74 +725,214 @@ void CConnectDlg::OnServerAddrEditTextChanged ( const QString& )
634725
lvwServers->clearSelection();
635726
}
636727

637-
QString CConnectDlg::BuildAccessibleTextForItem ( const QTreeWidgetItem* pItem ) const
728+
void CConnectDlg::OnServerListItemSelectionChanged()
729+
{
730+
#ifdef USE_ACCESSIBLE_SERVER_LIST
731+
UpdateAccessibleServerInfo();
732+
#endif
733+
}
734+
735+
#ifdef USE_ACCESSIBLE_SERVER_LIST
736+
void CConnectDlg::UpdateAccessibleServerInfo()
638737
{
639-
// Return early if item is null to avoid crashes
640-
if ( !pItem )
738+
QList<QTreeWidgetItem*> selectedItems = lvwServers->selectedItems();
739+
740+
if ( !selectedItems.isEmpty() && selectedItems.first()->parent() == nullptr )
641741
{
642-
return QString();
742+
// We have a server item selected (not a musician child item)
743+
QTreeWidgetItem* pItem = selectedItems.first();
744+
745+
// Extract server information
746+
QString serverName = pItem->text ( LVC_NAME );
747+
QString pingTime = pItem->text ( LVC_PING );
748+
QString musicians = pItem->text ( LVC_CLIENTS );
749+
QString location = pItem->text ( LVC_LOCATION );
750+
QString version = pItem->text ( LVC_VERSION );
751+
752+
// Build comprehensive text for the label
753+
QString labelText = tr ( "<b>Server:</b> %1" ).arg ( serverName );
754+
if ( !pingTime.isEmpty() )
755+
{
756+
labelText += tr ( "<br><b>Ping:</b> %1" ).arg ( pingTime );
757+
}
758+
if ( !musicians.isEmpty() )
759+
{
760+
labelText += tr ( " &nbsp; <b>Musicians:</b> %1" ).arg ( musicians );
761+
}
762+
if ( !location.isEmpty() )
763+
{
764+
labelText += tr ( "<br><b>Location:</b> %1" ).arg ( location );
765+
}
766+
if ( !version.isEmpty() )
767+
{
768+
labelText += tr ( " &nbsp; <b>Version:</b> %1" ).arg ( version );
769+
}
770+
771+
// Build text for screen readers (without HTML)
772+
QString accessibleText = tr ( "Server: %1" ).arg ( serverName );
773+
if ( !pingTime.isEmpty() )
774+
{
775+
accessibleText += tr ( ", Ping: %1" ).arg ( pingTime );
776+
}
777+
if ( !musicians.isEmpty() )
778+
{
779+
accessibleText += tr ( ", Musicians: %1" ).arg ( musicians );
780+
}
781+
if ( !location.isEmpty() )
782+
{
783+
accessibleText += tr ( ", Location: %1" ).arg ( location );
784+
}
785+
if ( !version.isEmpty() )
786+
{
787+
accessibleText += tr ( ", Version: %1" ).arg ( version );
788+
}
789+
790+
// Update label
791+
lblAccessibleServerInfo->setText ( labelText );
792+
lblAccessibleServerInfo->setAccessibleName ( tr ( "Selected server: %1" ).arg ( serverName ) );
793+
lblAccessibleServerInfo->setAccessibleDescription ( accessibleText );
794+
795+
// Update navigation buttons to show previous/next server names
796+
int currentIndex = lvwServers->indexOfTopLevelItem ( pItem );
797+
798+
// Update "Previous" button with previous server name
799+
if ( currentIndex > 0 )
800+
{
801+
QTreeWidgetItem* prevItem = lvwServers->topLevelItem ( currentIndex - 1 );
802+
QString prevName = prevItem->text ( LVC_NAME );
803+
butAccessiblePrevious->setText ( u8"\u2190 " + prevName );
804+
butAccessiblePrevious->setAccessibleName ( tr ( "Previous server: %1" ).arg ( prevName ) );
805+
butAccessiblePrevious->setAccessibleDescription ( tr ( "Go to previous server: %1" ).arg ( prevName ) );
806+
butAccessiblePrevious->setEnabled ( true );
807+
}
808+
else
809+
{
810+
butAccessiblePrevious->setText ( u8"\u2190 " + tr ( "(first)" ) );
811+
butAccessiblePrevious->setAccessibleName ( tr ( "No previous server - at first server" ) );
812+
butAccessiblePrevious->setAccessibleDescription ( tr ( "Cannot go back, already at first server" ) );
813+
butAccessiblePrevious->setEnabled ( false );
814+
}
815+
816+
// Update "Next" button with next server name
817+
if ( currentIndex < lvwServers->topLevelItemCount() - 1 )
818+
{
819+
QTreeWidgetItem* nextItem = lvwServers->topLevelItem ( currentIndex + 1 );
820+
QString nextName = nextItem->text ( LVC_NAME );
821+
butAccessibleNext->setText ( nextName + u8" \u2192" );
822+
butAccessibleNext->setAccessibleName ( tr ( "Next server: %1" ).arg ( nextName ) );
823+
butAccessibleNext->setAccessibleDescription ( tr ( "Go to next server: %1" ).arg ( nextName ) );
824+
butAccessibleNext->setEnabled ( true );
825+
}
826+
else
827+
{
828+
butAccessibleNext->setText ( tr ( "(last)" ) + u8" \u2192" );
829+
butAccessibleNext->setAccessibleName ( tr ( "No next server - at last server" ) );
830+
butAccessibleNext->setAccessibleDescription ( tr ( "Cannot go forward, already at last server" ) );
831+
butAccessibleNext->setEnabled ( false );
832+
}
833+
834+
// Force VoiceOver to announce the change
835+
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lblAccessibleServerInfo, accessibleText ) );
643836
}
644-
645-
// Check if this is a child item (musician) or parent item (server).
646-
// Child items have a parent and represent individual musicians connected to a server.
647-
// Parent items represent servers and contain multiple columns with server information.
648-
if ( pItem->parent() != nullptr )
837+
else
649838
{
650-
// This is a musician/child item - announce the musician name
651-
return tr ( "Musician: %1" ).arg ( pItem->text ( LVC_NAME ) );
839+
// No server selected or musician child selected
840+
lblAccessibleServerInfo->setText ( tr ( "<i>No server selected or in musician selection</i>" ) );
841+
lblAccessibleServerInfo->setAccessibleName ( tr ( "No server selected" ) );
842+
lblAccessibleServerInfo->setAccessibleDescription ( tr ( "No server selected. Use Previous/Next buttons or Alt+Up/Down to navigate servers." ) );
843+
844+
// Reset navigation buttons
845+
butAccessiblePrevious->setText ( u8"\u2190 " + tr ( "Previous" ) );
846+
butAccessiblePrevious->setAccessibleName ( tr ( "Navigate to previous server" ) );
847+
butAccessiblePrevious->setEnabled ( lvwServers->topLevelItemCount() > 0 );
848+
849+
butAccessibleNext->setText ( tr ( "Next" ) + u8" \u2192" );
850+
butAccessibleNext->setAccessibleName ( tr ( "Navigate to next server" ) );
851+
butAccessibleNext->setEnabled ( lvwServers->topLevelItemCount() > 0 );
852+
652853
}
854+
}
653855

654-
// This is a server item - build accessible text with all available information.
655-
// Start with the server name, then append other columns that have data.
656-
QString accessibleText = tr ( "Server: %1" ).arg ( pItem->text ( LVC_NAME ) );
657-
658-
// Helper lambda to append column data if it exists
659-
auto appendColumnIfNotEmpty = [&] ( int column, const QString& label ) {
660-
QString value = pItem->text ( column );
661-
if ( !value.isEmpty() )
856+
void CConnectDlg::OnAccessiblePreviousClicked()
857+
{
858+
// Navigate to previous server
859+
QList<QTreeWidgetItem*> selectedItems = lvwServers->selectedItems();
860+
QTreeWidgetItem* currentItem = selectedItems.isEmpty() ? nullptr : selectedItems.first();
861+
862+
// Get current item or first item if none selected
863+
if ( currentItem == nullptr )
864+
{
865+
// Select first item
866+
if ( lvwServers->topLevelItemCount() > 0 )
662867
{
663-
accessibleText += tr ( ", %1: %2" ).arg ( label ).arg ( value );
868+
lvwServers->setCurrentItem ( lvwServers->topLevelItem ( 0 ) );
664869
}
665-
};
666-
667-
// Append all server information columns in the order they appear in the UI
668-
appendColumnIfNotEmpty ( LVC_PING, tr ( "Ping" ) );
669-
appendColumnIfNotEmpty ( LVC_CLIENTS, tr ( "Musicians" ) );
670-
appendColumnIfNotEmpty ( LVC_LOCATION, tr ( "Location" ) );
671-
appendColumnIfNotEmpty ( LVC_VERSION, tr ( "Version" ) );
672-
673-
return accessibleText;
870+
return;
871+
}
872+
873+
// If current item is a musician (child), move to parent
874+
if ( currentItem->parent() != nullptr )
875+
{
876+
lvwServers->setCurrentItem ( currentItem->parent() );
877+
return;
878+
}
879+
880+
// Get previous server item
881+
int currentIndex = lvwServers->indexOfTopLevelItem ( currentItem );
882+
if ( currentIndex > 0 )
883+
{
884+
lvwServers->setCurrentItem ( lvwServers->topLevelItem ( currentIndex - 1 ) );
885+
}
674886
}
675887

676-
void CConnectDlg::UpdateAccessibilityForItem ( QTreeWidgetItem* pItem )
888+
void CConnectDlg::OnAccessibleNextClicked()
677889
{
678-
// Update the accessible description and notify the accessibility system
679-
// when items are selected. This improves screen reader support on all platforms,
680-
// including VoiceOver on macOS, NVDA/JAWS on Windows, and Orca on Linux.
681-
if ( !pItem )
890+
// Navigate to next server
891+
QList<QTreeWidgetItem*> selectedItems = lvwServers->selectedItems();
892+
QTreeWidgetItem* currentItem = selectedItems.isEmpty() ? nullptr : selectedItems.first();
893+
894+
// Get current item or first item if none selected
895+
if ( currentItem == nullptr )
682896
{
897+
// Select first item
898+
if ( lvwServers->topLevelItemCount() > 0 )
899+
{
900+
lvwServers->setCurrentItem ( lvwServers->topLevelItem ( 0 ) );
901+
}
683902
return;
684903
}
685-
686-
QString accessibleText = BuildAccessibleTextForItem ( pItem );
687-
lvwServers->setAccessibleDescription ( accessibleText );
688-
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( lvwServers, accessibleText ) );
904+
905+
// If current item is a musician (child), find next server
906+
if ( currentItem->parent() != nullptr )
907+
{
908+
currentItem = currentItem->parent();
909+
}
910+
911+
// Find next server item (skip musician children)
912+
int currentIndex = lvwServers->indexOfTopLevelItem ( currentItem );
913+
if ( currentIndex >= 0 && currentIndex < lvwServers->topLevelItemCount() - 1 )
914+
{
915+
lvwServers->setCurrentItem ( lvwServers->topLevelItem ( currentIndex + 1 ) );
916+
}
689917
}
690918

691-
void CConnectDlg::OnServerListItemSelectionChanged()
919+
void CConnectDlg::OnToggleAccessibleClicked()
692920
{
693-
QList<QTreeWidgetItem*> selectedItems = lvwServers->selectedItems();
694-
695-
if ( !selectedItems.isEmpty() )
921+
bool isVisible = wAccessibleNavPanel->isVisible();
922+
wAccessibleNavPanel->setVisible ( !isVisible );
923+
924+
if ( isVisible )
696925
{
697-
UpdateAccessibilityForItem ( selectedItems.first() );
926+
butToggleAccessible->setText ( u8"\u25B6 " + tr ( "Show Accessible Controls" ) );
927+
butToggleAccessible->setAccessibleDescription ( tr ( "Show the accessible navigation controls" ) );
698928
}
699929
else
700930
{
701-
// Clear accessible description when nothing is selected
702-
lvwServers->setAccessibleDescription ( tr ( "Server list" ) );
931+
butToggleAccessible->setText ( u8"\u25BC " + tr ( "Hide Accessible Controls" ) );
932+
butToggleAccessible->setAccessibleDescription ( tr ( "Hide the accessible navigation controls" ) );
703933
}
704934
}
935+
#endif
705936

706937
void CConnectDlg::OnCustomDirectoriesChanged()
707938
{

0 commit comments

Comments
 (0)