audacia/src/TrackPanelAx.cpp

802 lines
19 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
TrackPanelAx.cpp
Leland Lucius
and lots of other contributors
******************************************************************//*!
\class TrackPanelAx
\brief Helper to TrackPanel to give accessibility.
*//*******************************************************************/
#include "TrackPanelAx.h"
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
#include <wx/setup.h> // for wxUSE_* macros
#ifndef WX_PRECOMP
// Include your minimal set of headers here, or wx.h
#include <wx/wx.h>
#endif
#include <wx/intl.h>
#include "Project.h"
#include "Track.h"
wxDEFINE_EVENT(EVT_TRACK_FOCUS_CHANGE, wxCommandEvent);
TrackPanelAx::TrackPanelAx( AudacityProject &project )
:
#if wxUSE_ACCESSIBILITY
WindowAccessible( nullptr ) // window pointer must be set after construction
,
#endif
mProject{ project }
{
mTrackName = true;
mMessageCount = 0;
mNumFocusedTrack = 0;
}
TrackPanelAx::~TrackPanelAx()
{
}
TrackList &TrackPanelAx::GetTracks()
{
return TrackList::Get( mProject );
}
// Returns currently focused track
// if that track no longer exists, if there is a track at
// the same position, use that, else if there is a first
// track, use that.
std::shared_ptr<Track> TrackPanelAx::GetFocus()
{
auto focusedTrack = mFocusedTrack.lock();
if( !focusedTrack ) {
if (mNumFocusedTrack >=1) {
// This prevents the focus from being unnecessarily set to track 1
// when effects are applied. (Applying an effect can change
// the pointers of the selected tracks.)
focusedTrack = FindTrack(mNumFocusedTrack);
}
if (!focusedTrack) {
focusedTrack =
Track::SharedPointer( *GetTracks().Any().first );
// only call SetFocus if the focus has changed to avoid
// unnecessary focus events
if (focusedTrack)
focusedTrack = SetFocus();
}
}
if( !TrackNum( focusedTrack ) )
{
mFocusedTrack.reset();
return {};
}
return( focusedTrack );
}
// Changes focus to a specified track
std::shared_ptr<Track> TrackPanelAx::SetFocus( std::shared_ptr<Track> track )
{
mTrackName = true;
#if wxUSE_ACCESSIBILITY
auto focusedTrack = mFocusedTrack.lock();
if( focusedTrack && !focusedTrack->GetSelected() )
{
NotifyEvent( wxACC_EVENT_OBJECT_SELECTIONREMOVE,
GetWindow(),
wxOBJID_CLIENT,
TrackNum( focusedTrack ) );
}
#endif
if( !track )
track = Track::SharedPointer( *GetTracks().Any().begin() );
if ( mFocusedTrack.lock() != track ) {
mFocusedTrack = track;
mProject.QueueEvent( safenew wxCommandEvent{ EVT_TRACK_FOCUS_CHANGE } );
}
mNumFocusedTrack = TrackNum(track);
#if wxUSE_ACCESSIBILITY
if( track )
{
if (GetWindow() == wxWindow::FindFocus())
{
NotifyEvent( wxACC_EVENT_OBJECT_FOCUS,
GetWindow(),
wxOBJID_CLIENT,
mNumFocusedTrack );
}
if( track->GetSelected() )
{
NotifyEvent( wxACC_EVENT_OBJECT_SELECTION,
GetWindow(),
wxOBJID_CLIENT,
mNumFocusedTrack );
}
}
else
{
NotifyEvent(wxACC_EVENT_OBJECT_FOCUS,
GetWindow(),
wxOBJID_CLIENT,
wxACC_SELF);
}
#endif
return track;
}
// Returns TRUE if passed track has the focus
bool TrackPanelAx::IsFocused( const Track *track )
{
auto focusedTrack = mFocusedTrack.lock();
if( !focusedTrack )
focusedTrack = SetFocus();
// Remap track pointer if there are outstanding pending updates
auto origTrack =
GetTracks().FindById( track->GetId() );
if (origTrack)
track = origTrack;
return focusedTrack
? TrackList::Channels(focusedTrack.get()).contains(track)
: !track;
}
int TrackPanelAx::TrackNum( const std::shared_ptr<Track> &target )
{
// Find 1-based position of the target in the visible tracks, or 0 if not
// found
int ndx = 0;
for ( auto t : GetTracks().Leaders() )
{
ndx++;
if( t == target.get() )
{
return ndx;
}
}
return 0;
}
std::shared_ptr<Track> TrackPanelAx::FindTrack( int num )
{
int ndx = 0;
for ( auto t : GetTracks().Leaders() )
{
ndx++;
if( ndx == num )
return t->SharedPointer();
}
return {};
}
void TrackPanelAx::Updated()
{
#if wxUSE_ACCESSIBILITY
auto t = GetFocus();
mTrackName = true;
// The object_focus event is only needed by Window-Eyes
// and can be removed when we cease to support this screen reader.
NotifyEvent(wxACC_EVENT_OBJECT_FOCUS,
GetWindow(),
wxOBJID_CLIENT,
TrackNum(t));
NotifyEvent(wxACC_EVENT_OBJECT_NAMECHANGE,
GetWindow(),
wxOBJID_CLIENT,
TrackNum(t));
#endif
}
void TrackPanelAx::MessageForScreenReader(const TranslatableString& message)
{
#if wxUSE_ACCESSIBILITY
if (GetWindow() == wxWindow::FindFocus())
{
auto t = GetFocus();
int childId = t ? TrackNum(t) : 0;
mMessage = message.Translation();
// append \a alternatively, 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_NAMECHANGE,
GetWindow(),
wxOBJID_CLIENT,
childId);
}
#endif
}
#if wxUSE_ACCESSIBILITY
// Retrieves the address of an IDispatch interface for the specified child.
// All objects must support this property.
wxAccStatus TrackPanelAx::GetChild( int childId, wxAccessible** child )
{
if( childId == wxACC_SELF )
{
*child = this;
}
else
{
*child = NULL;
}
return wxACC_OK;
}
// Gets the number of children.
wxAccStatus TrackPanelAx::GetChildCount( int* childCount )
{
*childCount = GetTracks().Leaders().size();
return wxACC_OK;
}
// Gets the default action for this object (0) or > 0 (the action for a child).
// Return wxACC_OK even if there is no action. actionName is the action, or the empty
// string if there is no action.
// The retrieved string describes the action that is performed on an object,
// not what the object does as a result. For example, a toolbar button that prints
// a document has a default action of "Press" rather than "Prints the current document."
wxAccStatus TrackPanelAx::GetDefaultAction( int WXUNUSED(childId), wxString *actionName )
{
actionName->clear();
return wxACC_OK;
}
// Returns the description for this object or a child.
wxAccStatus TrackPanelAx::GetDescription( int WXUNUSED(childId), wxString *description )
{
description->clear();
return wxACC_OK;
}
// Returns help text for this object or a child, similar to tooltip text.
wxAccStatus TrackPanelAx::GetHelpText( int WXUNUSED(childId), wxString *helpText )
{
helpText->clear();
return wxACC_OK;
}
// Returns the keyboard shortcut for this object or child.
// Return e.g. ALT+K
wxAccStatus TrackPanelAx::GetKeyboardShortcut( int WXUNUSED(childId), wxString *shortcut )
{
shortcut->clear();
return wxACC_OK;
}
// Returns the rectangle for this object (id = 0) or a child element (id > 0).
// rect is in screen coordinates.
wxAccStatus TrackPanelAx::GetLocation( wxRect& rect, int elementId )
{
wxRect client;
if( elementId == wxACC_SELF )
{
rect = GetWindow()->GetRect();
}
else
{
auto t = FindTrack( elementId );
if( t == NULL )
{
return wxACC_FAIL;
}
rect = mFinder ? mFinder( *t ) : wxRect{};
// Inflate the screen reader's rectangle so it overpaints Audacity's own
// yellow focus rectangle.
#ifdef __WXMAC__
const int dx = 2;
#else
const int dx = 1;
#endif
rect.Inflate(dx, dx);
}
rect.SetPosition( GetWindow()->GetParent()->ClientToScreen( rect.GetPosition() ) );
return wxACC_OK;
}
// Gets the name of the specified object.
wxAccStatus TrackPanelAx::GetName( int childId, wxString* name )
{
#if defined(__WXMSW__) || defined(__WXMAC__)
if (mTrackName)
{
if( childId == wxACC_SELF )
{
*name = _( "TrackView" );
}
else
{
auto t = FindTrack( childId );
if( t == NULL )
{
return wxACC_FAIL;
}
else
{
*name = t->GetName();
if( *name == t->GetDefaultName() )
{
/* i18n-hint: The %d is replaced by the number of the track.*/
name->Printf(_("Track %d"), TrackNum( t ) );
}
t->TypeSwitch(
[&](const LabelTrack *) {
/* i18n-hint: This is for screen reader software and indicates that
this is a Label track.*/
name->Append( wxT(" ") + wxString(_("Label Track")));
},
[&](const TimeTrack *) {
/* i18n-hint: This is for screen reader software and indicates that
this is a Time track.*/
name->Append( wxT(" ") + wxString(_("Time Track")));
}
#ifdef USE_MIDI
,
[&](const NoteTrack *) {
/* i18n-hint: This is for screen reader software and indicates that
this is a Note track.*/
name->Append( wxT(" ") + wxString(_("Note Track")));
}
#endif
);
// LLL: Remove these during "refactor"
auto pt = dynamic_cast<PlayableTrack *>(t.get());
if( pt && pt->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
this track is muted. (The mute button is on.)*/
name->Append( wxT(" ") + wxString(_( " Muted" )) );
}
if( pt && pt->GetSolo() )
{
/* i18n-hint: This is for screen reader software and indicates that
this track is soloed. (The Solo button is on.)*/
name->Append( wxT(" ") + wxString(_( " Soloed" )) );
}
if( t->GetSelected() )
{
/* i18n-hint: This is for screen reader software and indicates that
this track is selected.*/
name->Append( wxT(" ") + wxString(_( " Selected" )) );
}
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 Locked" )) );
}
}
}
}
else
{
*name = mMessage;
}
return wxACC_OK;
#endif
#if defined(__WXMAC__)
return wxACC_NOT_IMPLEMENTED;
#endif
}
// Returns a role constant.
wxAccStatus TrackPanelAx::GetRole( int childId, wxAccRole* role )
{
#if defined(__WXMSW__)
if (mTrackName)
{
if( childId == wxACC_SELF )
{
*role = wxROLE_SYSTEM_TABLE;
}
else
{
*role = wxROLE_SYSTEM_ROW;
}
}
else
{
*role = wxROLE_NONE;
}
#endif
#if defined(__WXMAC__)
if( childId == wxACC_SELF )
{
*role = wxROLE_SYSTEM_PANE;
}
else
{
*role = wxROLE_SYSTEM_STATICTEXT;
}
#endif
return wxACC_OK;
}
// Gets a variant representing the selected children
// of this object.
// Acceptable values:
// - a null variant (IsNull() returns TRUE)
// - a list variant (GetType() == wxT("list"))
// - an integer representing the selected child element,
// or 0 if this object is selected (GetType() == wxT("long"))
// - a "void*" pointer to a wxAccessible child object
wxAccStatus TrackPanelAx::GetSelections( wxVariant * WXUNUSED(selections) )
{
return wxACC_NOT_IMPLEMENTED;
}
// Returns a state constant.
wxAccStatus TrackPanelAx::GetState( int childId, long* state )
{
#if defined(__WXMSW__)
if( childId > 0 )
{
auto t = FindTrack( childId );
*state = wxACC_STATE_SYSTEM_FOCUSABLE | wxACC_STATE_SYSTEM_SELECTABLE;
if (t)
{
if( t == mFocusedTrack.lock() )
{
*state |= wxACC_STATE_SYSTEM_FOCUSED;
}
if( t->GetSelected() && mTrackName )
{
*state |= wxACC_STATE_SYSTEM_SELECTED;
}
}
}
else // childId == wxACC_SELF
{
*state = wxACC_STATE_SYSTEM_FOCUSABLE + wxACC_STATE_SYSTEM_FOCUSED;
}
#endif
#if defined(__WXMAC__)
*state = wxACC_STATE_SYSTEM_FOCUSABLE | wxACC_STATE_SYSTEM_SELECTABLE;
if( childId > 0 )
{
auto t = FindTrack( childId );
if (t)
{
if( t == mFocusedTrack.lock() )
{
*state |= wxACC_STATE_SYSTEM_FOCUSED;
}
if( t->GetSelected() )
{
*state |= wxACC_STATE_SYSTEM_SELECTED;
}
}
}
#endif
return wxACC_OK;
}
// Returns a localized string representing the value for the object
// or child.
#if defined(__WXMAC__)
wxAccStatus TrackPanelAx::GetValue( int childId, wxString* strValue )
#else
wxAccStatus TrackPanelAx::GetValue( int WXUNUSED(childId), wxString* WXUNUSED(strValue) )
#endif
{
#if defined(__WXMSW__)
return wxACC_NOT_IMPLEMENTED;
#endif
#if defined(__WXMAC__)
if( childId == wxACC_SELF )
{
*strValue = _( "TrackView" );
}
else
{
auto t = FindTrack( childId );
if( t == NULL )
{
return wxACC_FAIL;
}
else
{
*strValue = t->GetName();
if( *strValue == t->GetDefaultName() )
{
strValue->Printf(_("Track %d"), TrackNum( t ) );
}
// LLL: Remove these during "refactor"
auto pt = dynamic_cast<PlayableTrack *>(t.get());
if( pt && pt->GetMute() )
{
strValue->Append( _( " Mute On" ) );
}
if( pt && pt->GetSolo() )
{
strValue->Append( _( " Solo On" ) );
}
if( t->GetSelected() )
{
strValue->Append( _( " Select On" ) );
}
}
}
return wxACC_OK;
#endif
}
// Gets the window with the keyboard focus.
// If childId is 0 and child is NULL, no object in
// this subhierarchy has the focus.
// If this object has the focus, child should be 'this'.
wxAccStatus TrackPanelAx::GetFocus( int *childId, wxAccessible **child )
{
#if defined(__WXMSW__)
if (GetWindow() == wxWindow::FindFocus())
{
auto focusedTrack = mFocusedTrack.lock();
if (focusedTrack)
{
*childId = TrackNum(focusedTrack);
}
else
{
*child = this;
}
}
return wxACC_OK;
#endif
#if defined(__WXMAC__)
if( GetWindow() == wxWindow::FindFocus() )
{
auto focusedTrack = mFocusedTrack.lock();
if( focusedTrack )
{
*childId = TrackNum( focusedTrack );
}
else
{
*childId = wxACC_SELF;
}
return wxACC_OK;
}
return wxACC_NOT_IMPLEMENTED;
#endif
}
// Navigates from fromId to toId/toObject
wxAccStatus TrackPanelAx::Navigate(wxNavDir navDir, int fromId, int* toId, wxAccessible** toObject)
{
int childCount;
GetChildCount( &childCount );
if (fromId > childCount)
return wxACC_FAIL;
switch (navDir) {
case wxNAVDIR_FIRSTCHILD:
if (fromId == wxACC_SELF && childCount > 0 )
*toId = 1;
else
return wxACC_FALSE;
break;
case wxNAVDIR_LASTCHILD:
if (fromId == wxACC_SELF && childCount > 0 )
*toId = childCount;
else
return wxACC_FALSE;
break;
case wxNAVDIR_NEXT:
case wxNAVDIR_DOWN:
if (fromId != wxACC_SELF) {
*toId = fromId + 1;
if (*toId > childCount)
return wxACC_FALSE;
}
else
return wxACC_NOT_IMPLEMENTED;
break;
case wxNAVDIR_PREVIOUS:
case wxNAVDIR_UP:
if (fromId != wxACC_SELF) {
*toId = fromId - 1;
if (*toId < 1)
return wxACC_FALSE;
}
else
return wxACC_NOT_IMPLEMENTED;
break;
case wxNAVDIR_LEFT:
case wxNAVDIR_RIGHT:
if (fromId != wxACC_SELF)
return wxACC_FALSE;
else
return wxACC_NOT_IMPLEMENTED;
break;
}
*toObject = nullptr;
return wxACC_OK;
}
// Modify focus or selection
wxAccStatus TrackPanelAx::Select(int childId, wxAccSelectionFlags selectFlags)
{
// Only support change of focus
if (selectFlags != wxACC_SEL_TAKEFOCUS)
return wxACC_NOT_IMPLEMENTED;
if (childId != wxACC_SELF) {
int childCount;
GetChildCount( &childCount );
if (childId > childCount)
return wxACC_FAIL;
Track* t = FindTrack(childId).get();
if (t) {
SetFocus( t->SharedPointer() );
t->EnsureVisible();
}
}
else
return wxACC_NOT_IMPLEMENTED;
return wxACC_OK;
}
#endif // wxUSE_ACCESSIBILITY
static const AudacityProject::AttachedObjects::RegisteredFactory key{
[]( AudacityProject &parent ){
return std::make_shared< TrackFocus >( parent );
}
};
TrackFocus &TrackFocus::Get( AudacityProject &project )
{
return project.AttachedObjects::Get< TrackFocus >( key );
}
const TrackFocus &TrackFocus::Get( const AudacityProject &project )
{
return Get( const_cast< AudacityProject & >( project ) );
}
TrackFocus::TrackFocus( AudacityProject &project )
: mProject{ project }
{
}
TrackFocus::~TrackFocus()
{
}
Track *TrackFocus::Get()
{
if (mAx)
return mAx->GetFocus().get();
return nullptr;
}
void TrackFocus::Set( Track *pTrack )
{
if (mAx) {
pTrack = *TrackList::Get( mProject ).FindLeader( pTrack );
mAx->SetFocus( Track::SharedPointer( pTrack ) );
}
}
bool TrackFocus::IsFocused( const Track *pTrack )
{
if (mAx)
return mAx->IsFocused( pTrack );
return false;
}
void TrackFocus::SetAccessible(
wxWindow &owner,
std::unique_ptr< TrackPanelAx > pAx
)
{
#if wxUSE_ACCESSIBILITY
// wxWidgets owns the accessible object
owner.SetAccessible(mAx = pAx.release());
#else
// wxWidgets does not own the object, but we need to retain it
mAx = std::move(pAx);
#endif
}
void TrackFocus::MessageForScreenReader(const TranslatableString& message)
{
if (mAx)
mAx->MessageForScreenReader( message );
}
void TrackFocus::UpdateAccessibility()
{
if (mAx)
mAx->Updated();
}