From ff9763f98456f7de7dc99b78d177eee8b091e897 Mon Sep 17 00:00:00 2001 From: David Bailes Date: Tue, 18 Oct 2016 15:50:42 +0100 Subject: [PATCH] Add two commands to move the cursor/selection to next/prev label The two commands are "selection to next label" and "selection to previous label". They have default shortcuts alt+right and alt+left. A label track does not have to be the focus. If there is a single label track in the project, that it used. If there is more than one label track, then the first label track, if any, starting at the focused track is used. If the commands are used during playback of the project, playback continues from the new cursor/selection. The commands provide feedback to screen readers: the name of the label, and position in the form of "i of n". --- src/LabelTrack.cpp | 55 ++++++++++++- src/LabelTrack.h | 5 ++ src/Menus.cpp | 71 +++++++++++++++++ src/Menus.h | 4 + src/TrackPanel.cpp | 6 ++ src/TrackPanel.h | 1 + src/TrackPanelAx.cpp | 181 +++++++++++++++++++++++++++---------------- src/TrackPanelAx.h | 6 ++ 8 files changed, 261 insertions(+), 68 deletions(-) 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__