@@ -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
7575CMappedTreeWidgetItem::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
274365void 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 ( " <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 ( " <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
706937void CConnectDlg::OnCustomDirectoriesChanged ()
707938{
0 commit comments