diff --git a/src/LabelTrack.cpp b/src/LabelTrack.cpp index 5051d37c9..74bd04234 100644 --- a/src/LabelTrack.cpp +++ b/src/LabelTrack.cpp @@ -105,7 +105,8 @@ LabelTrack::LabelTrack(const std::shared_ptr &projDirManager): mMouseOverLabelRight(-1), mRestoreFocus(-1), mClipLen(0.0), - mIsAdjustingLabel(false) + mIsAdjustingLabel(false), + miLastLabel(-1) { SetDefaultName(_("Label Track")); SetName(GetDefaultName()); @@ -2849,3 +2850,55 @@ wxString LabelTrack::GetTextOfLabels(double t0, double t1) const return retVal; } + +int LabelTrack::FindNextLabel(const SelectedRegion& currentRegion) +{ + int i = -1; + + if (!mLabels.empty()) { + int len = (int) mLabels.size(); + if (miLastLabel >= 0 && miLastLabel + 1 < len + && currentRegion.t0() == mLabels[miLastLabel].getT0() + && currentRegion.t0() == mLabels[miLastLabel + 1].getT0() ) { + i = miLastLabel + 1; + } + else { + i = 0; + if (currentRegion.t0() < mLabels[len - 1].getT0()) { + while (i < len && + mLabels[i].getT0() <= currentRegion.t0()) { + i++; + } + } + } + } + + miLastLabel = i; + return i; +} + + int LabelTrack::FindPrevLabel(const SelectedRegion& currentRegion) +{ + int i = -1; + + if (!mLabels.empty()) { + int len = (int) mLabels.size(); + if (miLastLabel > 0 && miLastLabel < len + && currentRegion.t0() == mLabels[miLastLabel].getT0() + && currentRegion.t0() == mLabels[miLastLabel - 1].getT0() ) { + i = miLastLabel - 1; + } + else { + i = len - 1; + if (currentRegion.t0() > mLabels[0].getT0()) { + while (i >=0 && + mLabels[i].getT0() >= currentRegion.t0()) { + i--; + } + } + } + } + + miLastLabel = i; + return i; +} diff --git a/src/LabelTrack.h b/src/LabelTrack.h index b33a0bd94..4ab7aa6e3 100644 --- a/src/LabelTrack.h +++ b/src/LabelTrack.h @@ -232,6 +232,9 @@ class AUDACITY_DLL_API LabelTrack final : public Track // Returns tab-separated text of all labels completely within given region wxString GetTextOfLabels(double t0, double t1) const; + int FindNextLabel(const SelectedRegion& currentSelection); + int FindPrevLabel(const SelectedRegion& currentSelection); + public: void SortLabels(); //These two are used by a TrackPanel KLUDGE, which is why they are public. @@ -268,6 +271,8 @@ class AUDACITY_DLL_API LabelTrack final : public Track // Set in copied label tracks double mClipLen; + int miLastLabel; // used by FindNextLabel and FindPrevLabel + void ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const; void ComputeTextPosition(const wxRect & r, int index) const; diff --git a/src/Menus.cpp b/src/Menus.cpp index 9f952076b..070e45aaa 100644 --- a/src/Menus.cpp +++ b/src/Menus.cpp @@ -1251,6 +1251,11 @@ void AudacityProject::CreateMenusAndCommands() c->AddCommand(wxT("PlaySpeedInc"), _("Increase playback speed"), FN(OnPlaySpeedInc)); c->AddCommand(wxT("PlaySpeedDec"), _("Decrease playback speed"), FN(OnPlaySpeedDec)); + c->AddCommand(wxT("SelToNextLabel"), _("Selection to next label"), FN(OnSelToNextLabel), wxT("Alt+Right"), + CaptureNotBusyFlag | TrackPanelHasFocus, CaptureNotBusyFlag | TrackPanelHasFocus); + c->AddCommand(wxT("SelToPrevLabel"), _("Selection to previous label"), FN(OnSelToPrevLabel), wxT("Alt+Left"), + CaptureNotBusyFlag | TrackPanelHasFocus, CaptureNotBusyFlag | TrackPanelHasFocus); + #ifdef __WXMAC__ /* i8n-hint: Shrink all project windows to icons on the Macintosh tooldock */ c->AddCommand(wxT("MacMinimizeAll"), _("Minimize all projects"), @@ -2656,6 +2661,72 @@ void AudacityProject::OnSelToEnd() ModifyState(false); } +void AudacityProject::OnSelToNextLabel() +{ + OnSelToLabel(true); +} + +void AudacityProject::OnSelToPrevLabel() +{ + OnSelToLabel(false); +} + +void AudacityProject::OnSelToLabel(bool next) +{ + // Find the number of label tracks, and ptr to last track found + Track* track = nullptr; + int nLabelTrack = 0; + TrackListOfKindIterator iter(Track::Label, &*mTracks); + for (Track* t = iter.First(); t; t = iter.Next()) { + nLabelTrack++; + track = t; + } + + if (nLabelTrack == 0 ) { + mTrackPanel->MessageForScreenReader(_("no label track")); + } + else if (nLabelTrack > 1) { // find first label track, if any, starting at the focused track + track = mTrackPanel->GetFocusedTrack(); + while (track && track->GetKind() != Track::Label) { + track = mTracks->GetNext(track, true); + if (!track) { + mTrackPanel->MessageForScreenReader(_("no label track at or below focused track")); + } + } + } + + // If there is a single label track, or there is a label track at or below the focused track + if (track) { + LabelTrack* lt = static_cast(track); + int i; + if (next) + i = lt->FindNextLabel(GetSelection()); + else + i = lt->FindPrevLabel(GetSelection()); + + if (i >= 0) { + const LabelStruct* label = lt->GetLabel(i); + if (IsAudioActive()) { + OnPlayStop(); // stop + GetViewInfo().selectedRegion = label->selectedRegion; + RedrawProject(); + OnPlayStop(); // play + } + else { + GetViewInfo().selectedRegion = label->selectedRegion; + RedrawProject(); + } + + wxString message; + message.Printf(wxT("%s %d of %d"), label->title, i + 1, lt->GetNumLabels() ); + mTrackPanel->MessageForScreenReader(message); + } + else { + mTrackPanel->MessageForScreenReader(_("no labels in label track")); + } + } +} + void AudacityProject::OnCursorUp() { mTrackPanel->OnPrevTrack( false ); diff --git a/src/Menus.h b/src/Menus.h index 0d2d65a65..01e287032 100644 --- a/src/Menus.h +++ b/src/Menus.h @@ -170,6 +170,10 @@ void OnSetRightSelection(); void OnSelToStart(); void OnSelToEnd(); +void OnSelToNextLabel(); +void OnSelToPrevLabel(); +void OnSelToLabel(bool next); + void OnZeroCrossing(); void OnLockPlayRegion(); diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index fce732484..a97333119 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -2406,6 +2406,12 @@ void TrackPanel::UpdateAccessibility() mAx->Updated(); } +void TrackPanel::MessageForScreenReader(const wxString& message) +{ + if (mAx) + mAx->MessageForScreenReader(message); +} + #ifdef EXPERIMENTAL_SPECTRAL_EDITING namespace { diff --git a/src/TrackPanel.h b/src/TrackPanel.h index c03b711b1..6c09d0631 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -308,6 +308,7 @@ protected: public: virtual void UpdateAccessibility(); + void MessageForScreenReader(const wxString& message); #ifdef EXPERIMENTAL_SPECTRAL_EDITING public: diff --git a/src/TrackPanelAx.cpp b/src/TrackPanelAx.cpp index ec370c459..145b7f3a3 100644 --- a/src/TrackPanelAx.cpp +++ b/src/TrackPanelAx.cpp @@ -37,6 +37,9 @@ TrackPanelAx::TrackPanelAx( wxWindow *window ) { mTrackPanel = wxDynamicCast( window, TrackPanel ); mFocusedTrack = NULL; + + mTrackName = true; + mMessageCount = 0; } TrackPanelAx::~TrackPanelAx() @@ -62,8 +65,11 @@ Track *TrackPanelAx::GetFocus() // Changes focus to a specified track void TrackPanelAx::SetFocus( Track *track ) { + mTrackName = true; + #if wxUSE_ACCESSIBILITY - if( mFocusedTrack != NULL ) + + if( mFocusedTrack != NULL && !mFocusedTrack->GetSelected() ) { NotifyEvent( wxACC_EVENT_OBJECT_SELECTIONREMOVE, mTrackPanel, @@ -164,6 +170,7 @@ void TrackPanelAx::Updated() { #if wxUSE_ACCESSIBILITY Track *t = GetFocus(); + mTrackName = true; // logically, this should be an OBJECT_NAMECHANGE event, but Window eyes 9.1 // does not read out the name with this event type, hence use OBJECT_FOCUS. @@ -174,6 +181,32 @@ void TrackPanelAx::Updated() #endif } +void TrackPanelAx::MessageForScreenReader(const wxString& message) +{ +#if wxUSE_ACCESSIBILITY + if (mTrackPanel == wxWindow::FindFocus()) + { + Track *t = GetFocus(); + int childId = t ? TrackNum(t) : 0; + + mMessage = message; + + // append \a alernatively, so that the string is never the same as the previous string. + // This ensures that screen readers read it. + if (mMessageCount % 2 == 0) + mMessage.Append('\a'); + mMessageCount++; + + mTrackName = false; + NotifyEvent(wxACC_EVENT_OBJECT_FOCUS, + mTrackPanel, + wxOBJID_CLIENT, + childId); + } + +#endif +} + #if wxUSE_ACCESSIBILITY // Retrieves the address of an IDispatch interface for the specified child. @@ -285,82 +318,89 @@ wxAccStatus TrackPanelAx::GetLocation( wxRect& rect, int elementId ) wxAccStatus TrackPanelAx::GetName( int childId, wxString* name ) { #if defined(__WXMSW__) || defined(__WXMAC__) - if( childId == wxACC_SELF ) + if (mTrackName) { - *name = _( "TrackView" ); - } - else - { - Track *t = FindTrack( childId ); - - if( t == NULL ) + if( childId == wxACC_SELF ) { - return wxACC_FAIL; + *name = _( "TrackView" ); } else { - *name = t->GetName(); - if( *name == t->GetDefaultName() ) - { - /* i18n-hint: The %d is replaced by th enumber of the track.*/ - name->Printf(_("Track %d"), TrackNum( t ) ); - } + Track *t = FindTrack( childId ); - if (t->GetKind() == Track::Label) + if( t == NULL ) { - /* i18n-hint: This is for screen reader software and indicates that - this is a Label track.*/ - name->Append( wxT(" ") + wxString(_("Label Track"))); + return wxACC_FAIL; } - else if (t->GetKind() == Track::Time) + else { - /* i18n-hint: This is for screen reader software and indicates that - this is a Time track.*/ - name->Append( wxT(" ") + wxString(_("Time Track"))); - } - else if (t->GetKind() == Track::Note) - { - /* i18n-hint: This is for screen reader software and indicates that - this is a Note track.*/ - name->Append( wxT(" ") + wxString(_("Note Track"))); - } + *name = t->GetName(); + if( *name == t->GetDefaultName() ) + { + /* i18n-hint: The %d is replaced by th enumber of the track.*/ + name->Printf(_("Track %d"), TrackNum( t ) ); + } - // LLL: Remove these during "refactor" - if( t->GetMute() ) - { - // The following comment also applies to the solo, selected, - // and synclockselected states. - // Many of translations of the strings with a leading space omitted - // the leading space. Therefore a space has been added using wxT(" "). - // Because screen readers won't be affected by multiple spaces, the - // leading spaces have not been removed, so that no NEW translations are needed. - /* i18n-hint: This is for screen reader software and indicates that - on this track mute is on.*/ - name->Append( wxT(" ") + wxString(_( " Mute On" )) ); - } + if (t->GetKind() == Track::Label) + { + /* i18n-hint: This is for screen reader software and indicates that + this is a Label track.*/ + name->Append( wxT(" ") + wxString(_("Label Track"))); + } + else if (t->GetKind() == Track::Time) + { + /* i18n-hint: This is for screen reader software and indicates that + this is a Time track.*/ + name->Append( wxT(" ") + wxString(_("Time Track"))); + } + else if (t->GetKind() == Track::Note) + { + /* i18n-hint: This is for screen reader software and indicates that + this is a Note track.*/ + name->Append( wxT(" ") + wxString(_("Note Track"))); + } - if( t->GetSolo() ) - { - /* i18n-hint: This is for screen reader software and indicates that - on this track solo is on.*/ - name->Append( wxT(" ") + wxString(_( " Solo On" )) ); - } - if( t->GetSelected() ) - { - /* i18n-hint: This is for screen reader software and indicates that - this track is selected.*/ - name->Append( wxT(" ") + wxString(_( " Select On" )) ); - } - if( t->IsSyncLockSelected() ) - { - /* i18n-hint: This is for screen reader software and indicates that - this track is shown with a sync-locked icon.*/ - // The absence of a dash between Sync and Locked is deliberate - - // if present, Jaws reads it as "dash". - name->Append( wxT(" ") + wxString(_( " Sync Lock Selected" )) ); + // LLL: Remove these during "refactor" + if( t->GetMute() ) + { + // The following comment also applies to the solo, selected, + // and synclockselected states. + // Many of translations of the strings with a leading space omitted + // the leading space. Therefore a space has been added using wxT(" "). + // Because screen readers won't be affected by multiple spaces, the + // leading spaces have not been removed, so that no NEW translations are needed. + /* i18n-hint: This is for screen reader software and indicates that + on this track mute is on.*/ + name->Append( wxT(" ") + wxString(_( " Mute On" )) ); + } + + if( t->GetSolo() ) + { + /* i18n-hint: This is for screen reader software and indicates that + on this track solo is on.*/ + name->Append( wxT(" ") + wxString(_( " Solo On" )) ); + } + if( t->GetSelected() ) + { + /* i18n-hint: This is for screen reader software and indicates that + this track is selected.*/ + name->Append( wxT(" ") + wxString(_( " Select On" )) ); + } + if( t->IsSyncLockSelected() ) + { + /* i18n-hint: This is for screen reader software and indicates that + this track is shown with a sync-locked icon.*/ + // The absence of a dash between Sync and Locked is deliberate - + // if present, Jaws reads it as "dash". + name->Append( wxT(" ") + wxString(_( " Sync Lock Selected" )) ); + } } } } + else + { + *name = mMessage; + } return wxACC_OK; #endif @@ -374,13 +414,20 @@ wxAccStatus TrackPanelAx::GetName( int childId, wxString* name ) wxAccStatus TrackPanelAx::GetRole( int childId, wxAccRole* role ) { #if defined(__WXMSW__) - if( childId == wxACC_SELF ) + if (mTrackName) { - *role = wxROLE_SYSTEM_TABLE; + if( childId == wxACC_SELF ) + { + *role = wxROLE_SYSTEM_TABLE; + } + else + { + *role = wxROLE_SYSTEM_ROW; + } } else { - *role = wxROLE_SYSTEM_ROW; + *role = wxROLE_NONE; } #endif @@ -427,7 +474,7 @@ wxAccStatus TrackPanelAx::GetState( int childId, long* state ) *state |= wxACC_STATE_SYSTEM_FOCUSED; } - if( t->GetSelected() ) + if( t->GetSelected() && mTrackName ) { *state |= wxACC_STATE_SYSTEM_SELECTED; } diff --git a/src/TrackPanelAx.h b/src/TrackPanelAx.h index 78c67a57a..691a20e24 100644 --- a/src/TrackPanelAx.h +++ b/src/TrackPanelAx.h @@ -41,6 +41,8 @@ public: // Called to signal changes to a track void Updated(); + void MessageForScreenReader(const wxString& message); + #if wxUSE_ACCESSIBILITY // Retrieves the address of an IDispatch interface for the specified child. // All objects must support this property. @@ -108,6 +110,10 @@ private: TrackPanel *mTrackPanel; Track *mFocusedTrack; + + wxString mMessage; + bool mTrackName; + int mMessageCount; }; #endif // __AUDACITY_TRACK_PANEL_ACCESSIBILITY__