audacia/src/menus/NavigationMenus.cpp

624 lines
17 KiB
C++

#include "../Audacity.h"
#include "../CommonCommandFlags.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../ProjectHistory.h"
#include "../ProjectWindow.h"
#include "../Track.h"
#include "../SelectionState.h"
#include "../TrackPanel.h"
#include "../TrackPanelAx.h"
#include "../commands/CommandContext.h"
#include "../commands/CommandManager.h"
#include "../toolbars/ToolManager.h"
#include "../widgets/AButton.h"
#include "../widgets/ASlider.h"
#include "../widgets/Meter.h"
// private helper classes and functions
namespace {
void NextOrPrevFrame(AudacityProject &project, bool forward)
{
// Focus won't take in a dock unless at least one descendant window
// accepts focus. Tell controls to take focus for the duration of this
// function, only. Outside of this, they won't steal the focus when
// clicked.
auto temp1 = AButton::TemporarilyAllowFocus();
auto temp2 = ASlider::TemporarilyAllowFocus();
auto temp3 = MeterPanel::TemporarilyAllowFocus();
auto &toolManager = ToolManager::Get( project );
auto botDock = toolManager.GetBotDock();
// Define the set of windows we rotate among.
static const unsigned rotationSize = 3u;
wxWindow *const begin [rotationSize] = {
ProjectWindow::Get( project ).GetTopPanel(),
&TrackPanel::Get( project ),
botDock,
};
const auto end = begin + rotationSize;
// helper functions
auto IndexOf = [&](wxWindow *pWindow) {
return std::find(begin, end, pWindow) - begin;
};
auto FindAncestor = [&]() {
wxWindow *pWindow = wxWindow::FindFocus();
unsigned index = rotationSize;
while ( pWindow &&
(rotationSize == (index = IndexOf(pWindow) ) ) )
pWindow = pWindow->GetParent();
return index;
};
const auto idx = FindAncestor();
if (idx == rotationSize)
return;
auto idx2 = idx;
auto increment = (forward ? 1 : rotationSize - 1);
while( idx != (idx2 = (idx2 + increment) % rotationSize) ) {
wxWindow *toFocus = begin[idx2];
bool bIsAnEmptyDock=false;
if( idx2 != 1 )
bIsAnEmptyDock = ((idx2==0) ? toolManager.GetTopDock() : botDock)->
GetChildren().GetCount() < 1;
// Skip docks that are empty (Bug 1564).
if( !bIsAnEmptyDock ){
toFocus->SetFocus();
if ( FindAncestor() == idx2 )
// The focus took!
break;
}
}
}
/// \todo Merge related methods, OnPrevTrack and OnNextTrack.
void DoPrevTrack(
AudacityProject &project, bool shift, bool circularTrackNavigation )
{
auto &projectHistory = ProjectHistory::Get( project );
auto &trackFocus = TrackFocus::Get( project );
auto &tracks = TrackList::Get( project );
auto &selectionState = SelectionState::Get( project );
auto t = trackFocus.Get();
if( t == NULL ) // if there isn't one, focus on last
{
t = *tracks.Any().rbegin();
trackFocus.Set( t );
if (t)
t->EnsureVisible( true );
return;
}
Track* p = NULL;
bool tSelected = false;
bool pSelected = false;
if( shift )
{
p = * -- tracks.FindLeader( t ); // Get previous track
if( p == NULL ) // On first track
{
// JKC: wxBell() is probably for accessibility, so a blind
// user knows they were at the top track.
wxBell();
if( circularTrackNavigation )
p = *tracks.Any().rbegin();
else
{
t->EnsureVisible();
return;
}
}
tSelected = t->GetSelected();
if (p)
pSelected = p->GetSelected();
if( tSelected && pSelected )
{
selectionState.SelectTrack
( *t, false, false );
trackFocus.Set( p ); // move focus to next track up
if (p)
p->EnsureVisible( true );
return;
}
if( tSelected && !pSelected )
{
selectionState.SelectTrack
( *p, true, false );
trackFocus.Set( p ); // move focus to next track up
if (p)
p->EnsureVisible( true );
return;
}
if( !tSelected && pSelected )
{
selectionState.SelectTrack
( *p, false, false );
trackFocus.Set( p ); // move focus to next track up
if (p)
p->EnsureVisible( true );
return;
}
if( !tSelected && !pSelected )
{
selectionState.SelectTrack
( *t, true, false );
trackFocus.Set( p ); // move focus to next track up
if (p)
p->EnsureVisible( true );
return;
}
}
else
{
p = * -- tracks.FindLeader( t ); // Get previous track
if( p == NULL ) // On first track so stay there?
{
wxBell();
if( circularTrackNavigation )
{
auto range = tracks.Leaders();
p = * range.rbegin(); // null if range is empty
trackFocus.Set( p ); // Wrap to the last track
if (p)
p->EnsureVisible( true );
return;
}
else
{
t->EnsureVisible();
return;
}
}
else
{
trackFocus.Set( p ); // move focus to next track up
p->EnsureVisible( true );
return;
}
}
}
/// The following method moves to the next track,
/// selecting and unselecting depending if you are on the start of a
/// block or not.
void DoNextTrack(
AudacityProject &project, bool shift, bool circularTrackNavigation )
{
auto &projectHistory = ProjectHistory::Get( project );
auto &trackFocus = TrackFocus::Get( project );
auto &tracks = TrackList::Get( project );
auto &selectionState = SelectionState::Get( project );
auto t = trackFocus.Get(); // Get currently focused track
if( t == NULL ) // if there isn't one, focus on first
{
t = *tracks.Any().begin();
trackFocus.Set( t );
if (t)
t->EnsureVisible( true );
return;
}
if( shift )
{
auto n = * ++ tracks.FindLeader( t ); // Get next track
if( n == NULL ) // On last track so stay there
{
wxBell();
if( circularTrackNavigation )
n = *tracks.Any().begin();
else
{
t->EnsureVisible();
return;
}
}
auto tSelected = t->GetSelected();
auto nSelected = n->GetSelected();
if( tSelected && nSelected )
{
selectionState.SelectTrack
( *t, false, false );
trackFocus.Set( n ); // move focus to next track down
if (n)
n->EnsureVisible( true );
return;
}
if( tSelected && !nSelected )
{
selectionState.SelectTrack
( *n, true, false );
trackFocus.Set( n ); // move focus to next track down
if (n)
n->EnsureVisible( true );
return;
}
if( !tSelected && nSelected )
{
selectionState.SelectTrack
( *n, false, false );
trackFocus.Set( n ); // move focus to next track down
if (n)
n->EnsureVisible( true );
return;
}
if( !tSelected && !nSelected )
{
selectionState.SelectTrack
( *t, true, false );
trackFocus.Set( n ); // move focus to next track down
if (n)
n->EnsureVisible( true );
return;
}
}
else
{
auto n = * ++ tracks.FindLeader( t ); // Get next track
if( n == NULL ) // On last track so stay there
{
wxBell();
if( circularTrackNavigation )
{
n = *tracks.Any().begin();
trackFocus.Set( n ); // Wrap to the first track
if (n)
n->EnsureVisible( true );
return;
}
else
{
t->EnsureVisible();
return;
}
}
else
{
trackFocus.Set( n ); // move focus to next track down
n->EnsureVisible( true );
return;
}
}
}
}
/// Namespace for functions for project navigation menu (part of Extra menu)
namespace NavigationActions {
// exported helper functions
// none
// Menu handler functions
struct Handler
: CommandHandlerObject // MUST be the first base class!
, ClientData::Base
, PrefsListener
{
void OnPrevWindow(const CommandContext &context)
{
auto &project = context.project;
auto &window = GetProjectFrame( project );
auto isEnabled = window.IsEnabled();
wxWindow *w = wxGetTopLevelParent(wxWindow::FindFocus());
const auto & list = window.GetChildren();
auto iter = list.rbegin(), end = list.rend();
// If the project window has the current focus, start the search with the
// last child
if (w == &window)
{
}
// Otherwise start the search with the current window's previous sibling
else
{
while (iter != end && *iter != w)
++iter;
if (iter != end)
++iter;
}
// Search for the previous toplevel window
for (; iter != end; ++iter)
{
// If it's a toplevel and is visible (we have come hidden windows), then
// we're done
w = *iter;
if (w->IsTopLevel() && w->IsShown() && isEnabled)
{
break;
}
}
// Ran out of siblings, so make the current project active
if ((iter == end) && isEnabled)
{
w = &window;
}
// And make sure it's on top (only for floating windows...project window will
// not raise)
// (Really only works on Windows)
w->Raise();
#if defined(__WXMAC__) || defined(__WXGTK__)
// bug 868
// Simulate a TAB key press before continuing, else the cycle of
// navigation among top level windows stops because the keystrokes don't
// go to the CommandManager.
if (dynamic_cast<wxDialog*>(w)) {
w->SetFocus();
}
#endif
}
void OnNextWindow(const CommandContext &context)
{
auto &project = context.project;
auto &window = GetProjectFrame( project );
auto isEnabled = window.IsEnabled();
wxWindow *w = wxGetTopLevelParent(wxWindow::FindFocus());
const auto & list = window.GetChildren();
auto iter = list.begin(), end = list.end();
// If the project window has the current focus, start the search with the
// first child
if (w == &window)
{
}
// Otherwise start the search with the current window's next sibling
else
{
// Find the window in this projects children. If the window with the
// focus isn't a child of this project (like when a dialog is created
// without specifying a parent), then we'll get back NULL here.
while (iter != end && *iter != w)
++iter;
if (iter != end)
++iter;
}
// Search for the next toplevel window
for (; iter != end; ++iter)
{
// If it's a toplevel, visible (we have hidden windows) and is enabled,
// then we're done. The IsEnabled() prevents us from moving away from
// a modal dialog because all other toplevel windows will be disabled.
w = *iter;
if (w->IsTopLevel() && w->IsShown() && w->IsEnabled())
{
break;
}
}
// Ran out of siblings, so make the current project active
if ((iter == end) && isEnabled)
{
w = &window;
}
// And make sure it's on top (only for floating windows...project window will
// not raise)
// (Really only works on Windows)
w->Raise();
#if defined(__WXMAC__) || defined(__WXGTK__)
// bug 868
// Simulate a TAB key press before continuing, else the cycle of
// navigation among top level windows stops because the keystrokes don't
// go to the CommandManager.
if (dynamic_cast<wxDialog*>(w)) {
w->SetFocus();
}
#endif
}
void OnPrevFrame(const CommandContext &context)
{
auto &project = context.project;
NextOrPrevFrame(project, false);
}
void OnNextFrame(const CommandContext &context)
{
auto &project = context.project;
NextOrPrevFrame(project, true);
}
// Handler state:
bool mCircularTrackNavigation{};
void OnCursorUp(const CommandContext &context)
{
auto &project = context.project;
DoPrevTrack( project, false, mCircularTrackNavigation );
}
void OnCursorDown(const CommandContext &context)
{
auto &project = context.project;
DoNextTrack( project, false, mCircularTrackNavigation );
}
void OnFirstTrack(const CommandContext &context)
{
auto &project = context.project;
auto &trackFocus = TrackFocus::Get( project );
auto &tracks = TrackList::Get( project );
auto t = trackFocus.Get();
if (!t)
return;
auto f = *tracks.Any().begin();
if (t != f)
trackFocus.Set(f);
if (f)
f->EnsureVisible( t != f );
}
void OnLastTrack(const CommandContext &context)
{
auto &project = context.project;
auto &trackFocus = TrackFocus::Get( project );
auto &tracks = TrackList::Get( project );
Track *t = trackFocus.Get();
if (!t)
return;
auto l = *tracks.Any().rbegin();
if (t != l)
trackFocus.Set(l);
if (l)
l->EnsureVisible( t != l );
}
void OnShiftUp(const CommandContext &context)
{
auto &project = context.project;
DoPrevTrack( project, true, mCircularTrackNavigation );
}
void OnShiftDown(const CommandContext &context)
{
auto &project = context.project;
DoNextTrack( project, true, mCircularTrackNavigation );
}
void OnToggle(const CommandContext &context)
{
auto &project = context.project;
auto &trackFocus = TrackFocus::Get( project );
auto &selectionState = SelectionState::Get( project );
Track *t;
t = trackFocus.Get(); // Get currently focused track
if (!t)
return;
selectionState.SelectTrack
( *t, !t->GetSelected(), true );
t->EnsureVisible( true );
trackFocus.UpdateAccessibility();
return;
}
void UpdatePrefs() override
{
mCircularTrackNavigation =
gPrefs->ReadBool(wxT("/GUI/CircularTrackNavigation"), false);
}
Handler()
{
UpdatePrefs();
}
Handler( const Handler & ) PROHIBITED;
Handler &operator=( const Handler & ) PROHIBITED;
}; // struct Handler
} // namespace
// Handler is stateful. Needs a factory registered with
// AudacityProject.
static const AudacityProject::AttachedObjects::RegisteredFactory key{
[](AudacityProject&) {
return std::make_unique< NavigationActions::Handler >(); } };
static CommandHandlerObject &findCommandHandler(AudacityProject &project) {
return project.AttachedObjects::Get< NavigationActions::Handler >( key );
};
// Menu definitions
#define FN(X) (& NavigationActions::Handler :: X)
namespace {
using namespace MenuTable;
BaseItemSharedPtr ExtraGlobalCommands()
{
// Ceci n'est pas un menu
using Options = CommandManager::Options;
static BaseItemSharedPtr items{
( FinderScope{ findCommandHandler },
Items( wxT("Navigation"),
Command( wxT("PrevWindow"), XXO("Move Backward Through Active Windows"),
FN(OnPrevWindow), AlwaysEnabledFlag,
Options{ wxT("Alt+Shift+F6") }.IsGlobal() ),
Command( wxT("NextWindow"), XXO("Move Forward Through Active Windows"),
FN(OnNextWindow), AlwaysEnabledFlag,
Options{ wxT("Alt+F6") }.IsGlobal() )
) ) };
return items;
}
AttachedItem sAttachment2{
wxT("Optional/Extra/Part2"),
Shared( ExtraGlobalCommands() )
};
BaseItemSharedPtr ExtraFocusMenu()
{
static const auto FocusedTracksFlags = TracksExistFlag() | TrackPanelHasFocus();
static BaseItemSharedPtr menu{
( FinderScope{ findCommandHandler },
Menu( wxT("Focus"), XO("F&ocus"),
Command( wxT("PrevFrame"),
XXO("Move &Backward from Toolbars to Tracks"), FN(OnPrevFrame),
AlwaysEnabledFlag, wxT("Ctrl+Shift+F6") ),
Command( wxT("NextFrame"),
XXO("Move F&orward from Toolbars to Tracks"), FN(OnNextFrame),
AlwaysEnabledFlag, wxT("Ctrl+F6") ),
Command( wxT("PrevTrack"), XXO("Move Focus to &Previous Track"),
FN(OnCursorUp), FocusedTracksFlags, wxT("Up") ),
Command( wxT("NextTrack"), XXO("Move Focus to &Next Track"),
FN(OnCursorDown), FocusedTracksFlags, wxT("Down") ),
Command( wxT("FirstTrack"), XXO("Move Focus to &First Track"),
FN(OnFirstTrack), FocusedTracksFlags, wxT("Ctrl+Home") ),
Command( wxT("LastTrack"), XXO("Move Focus to &Last Track"),
FN(OnLastTrack), FocusedTracksFlags, wxT("Ctrl+End") ),
Command( wxT("ShiftUp"), XXO("Move Focus to P&revious and Select"),
FN(OnShiftUp), FocusedTracksFlags, wxT("Shift+Up") ),
Command( wxT("ShiftDown"), XXO("Move Focus to N&ext and Select"),
FN(OnShiftDown), FocusedTracksFlags, wxT("Shift+Down") ),
Command( wxT("Toggle"), XXO("&Toggle Focused Track"), FN(OnToggle),
FocusedTracksFlags, wxT("Return") ),
Command( wxT("ToggleAlt"), XXO("Toggle Focuse&d Track"), FN(OnToggle),
FocusedTracksFlags, wxT("NUMPAD_ENTER") )
) ) };
return menu;
}
AttachedItem sAttachment3{
wxT("Optional/Extra/Part2"),
Shared( ExtraFocusMenu() )
};
}
#undef FN