1
2
3 #include "../CommonCommandFlags.h"
4 #include "Prefs.h"
5 #include "Project.h"
6 #include "../ProjectHistory.h"
7 #include "../ProjectWindow.h"
8 #include "../ProjectWindows.h"
9 #include "../Track.h"
10 #include "../SelectionState.h"
11 #include "../TrackPanel.h"
12 #include "../TrackPanelAx.h"
13 #include "../commands/CommandContext.h"
14 #include "../commands/CommandManager.h"
15 #include "../toolbars/ToolManager.h"
16 #include "../widgets/AButton.h"
17 #include "../widgets/ASlider.h"
18 #include "../widgets/MeterPanel.h"
19
20 // private helper classes and functions
21 namespace {
22
NextOrPrevFrame(AudacityProject & project,bool forward)23 void NextOrPrevFrame(AudacityProject &project, bool forward)
24 {
25 // Focus won't take in a dock unless at least one descendant window
26 // accepts focus. Tell controls to take focus for the duration of this
27 // function, only. Outside of this, they won't steal the focus when
28 // clicked.
29 auto temp1 = AButton::TemporarilyAllowFocus();
30 auto temp2 = ASlider::TemporarilyAllowFocus();
31 auto temp3 = MeterPanel::TemporarilyAllowFocus();
32
33 auto &toolManager = ToolManager::Get( project );
34 auto botDock = toolManager.GetBotDock();
35
36
37 // Define the set of windows we rotate among.
38 static const unsigned rotationSize = 3u;
39
40 wxWindow *const begin [rotationSize] = {
41 ProjectWindow::Get( project ).GetTopPanel(),
42 &TrackPanel::Get( project ),
43 botDock,
44 };
45
46 const auto end = begin + rotationSize;
47
48 // helper functions
49 auto IndexOf = [&](wxWindow *pWindow) {
50 return std::find(begin, end, pWindow) - begin;
51 };
52
53 auto FindAncestor = [&]() {
54 wxWindow *pWindow = wxWindow::FindFocus();
55 unsigned index = rotationSize;
56 while ( pWindow &&
57 (rotationSize == (index = IndexOf(pWindow) ) ) )
58 pWindow = pWindow->GetParent();
59 return index;
60 };
61
62 const auto idx = FindAncestor();
63 if (idx == rotationSize)
64 return;
65
66 auto idx2 = idx;
67 auto increment = (forward ? 1 : rotationSize - 1);
68
69 while( idx != (idx2 = (idx2 + increment) % rotationSize) ) {
70 wxWindow *toFocus = begin[idx2];
71 bool bIsAnEmptyDock=false;
72 if( idx2 != 1 )
73 bIsAnEmptyDock = ((idx2==0) ? toolManager.GetTopDock() : botDock)->
74 GetChildren().GetCount() < 1;
75
76 // Skip docks that are empty (Bug 1564).
77 if( !bIsAnEmptyDock ){
78 toFocus->SetFocus();
79 if ( FindAncestor() == idx2 )
80 // The focus took!
81 break;
82 }
83 }
84 }
85
86 /// \todo Merge related methods, OnPrevTrack and OnNextTrack.
DoPrevTrack(AudacityProject & project,bool shift,bool circularTrackNavigation)87 void DoPrevTrack(
88 AudacityProject &project, bool shift, bool circularTrackNavigation )
89 {
90 auto &projectHistory = ProjectHistory::Get( project );
91 auto &trackFocus = TrackFocus::Get( project );
92 auto &tracks = TrackList::Get( project );
93 auto &selectionState = SelectionState::Get( project );
94
95 auto t = trackFocus.Get();
96 if( t == NULL ) // if there isn't one, focus on last
97 {
98 t = *tracks.Any().rbegin();
99 trackFocus.Set( t );
100 if (t)
101 t->EnsureVisible( true );
102 return;
103 }
104
105 Track* p = NULL;
106 bool tSelected = false;
107 bool pSelected = false;
108 if( shift )
109 {
110 p = * -- tracks.FindLeader( t ); // Get previous track
111 if( p == NULL ) // On first track
112 {
113 // JKC: wxBell() is probably for accessibility, so a blind
114 // user knows they were at the top track.
115 wxBell();
116 if( circularTrackNavigation )
117 p = *tracks.Any().rbegin();
118 else
119 {
120 t->EnsureVisible();
121 return;
122 }
123 }
124 tSelected = t->GetSelected();
125 if (p)
126 pSelected = p->GetSelected();
127 if( tSelected && pSelected )
128 {
129 selectionState.SelectTrack
130 ( *t, false, false );
131 trackFocus.Set( p ); // move focus to next track up
132 if (p)
133 p->EnsureVisible( true );
134 return;
135 }
136 if( tSelected && !pSelected )
137 {
138 selectionState.SelectTrack
139 ( *p, true, false );
140 trackFocus.Set( p ); // move focus to next track up
141 if (p)
142 p->EnsureVisible( true );
143 return;
144 }
145 if( !tSelected && pSelected )
146 {
147 selectionState.SelectTrack
148 ( *p, false, false );
149 trackFocus.Set( p ); // move focus to next track up
150 if (p)
151 p->EnsureVisible( true );
152 return;
153 }
154 if( !tSelected && !pSelected )
155 {
156 selectionState.SelectTrack
157 ( *t, true, false );
158 trackFocus.Set( p ); // move focus to next track up
159 if (p)
160 p->EnsureVisible( true );
161 return;
162 }
163 }
164 else
165 {
166 p = * -- tracks.FindLeader( t ); // Get previous track
167 if( p == NULL ) // On first track so stay there?
168 {
169 wxBell();
170 if( circularTrackNavigation )
171 {
172 auto range = tracks.Leaders();
173 p = * range.rbegin(); // null if range is empty
174 trackFocus.Set( p ); // Wrap to the last track
175 if (p)
176 p->EnsureVisible( true );
177 return;
178 }
179 else
180 {
181 t->EnsureVisible();
182 return;
183 }
184 }
185 else
186 {
187 trackFocus.Set( p ); // move focus to next track up
188 p->EnsureVisible( true );
189 return;
190 }
191 }
192 }
193
194 /// The following method moves to the next track,
195 /// selecting and unselecting depending if you are on the start of a
196 /// block or not.
DoNextTrack(AudacityProject & project,bool shift,bool circularTrackNavigation)197 void DoNextTrack(
198 AudacityProject &project, bool shift, bool circularTrackNavigation )
199 {
200 auto &projectHistory = ProjectHistory::Get( project );
201 auto &trackFocus = TrackFocus::Get( project );
202 auto &tracks = TrackList::Get( project );
203 auto &selectionState = SelectionState::Get( project );
204
205 auto t = trackFocus.Get(); // Get currently focused track
206 if( t == NULL ) // if there isn't one, focus on first
207 {
208 t = *tracks.Any().begin();
209 trackFocus.Set( t );
210 if (t)
211 t->EnsureVisible( true );
212 return;
213 }
214
215 if( shift )
216 {
217 auto n = * ++ tracks.FindLeader( t ); // Get next track
218 if( n == NULL ) // On last track so stay there
219 {
220 wxBell();
221 if( circularTrackNavigation )
222 n = *tracks.Any().begin();
223 else
224 {
225 t->EnsureVisible();
226 return;
227 }
228 }
229 auto tSelected = t->GetSelected();
230 auto nSelected = n->GetSelected();
231 if( tSelected && nSelected )
232 {
233 selectionState.SelectTrack
234 ( *t, false, false );
235 trackFocus.Set( n ); // move focus to next track down
236 if (n)
237 n->EnsureVisible( true );
238 return;
239 }
240 if( tSelected && !nSelected )
241 {
242 selectionState.SelectTrack
243 ( *n, true, false );
244 trackFocus.Set( n ); // move focus to next track down
245 if (n)
246 n->EnsureVisible( true );
247 return;
248 }
249 if( !tSelected && nSelected )
250 {
251 selectionState.SelectTrack
252 ( *n, false, false );
253 trackFocus.Set( n ); // move focus to next track down
254 if (n)
255 n->EnsureVisible( true );
256 return;
257 }
258 if( !tSelected && !nSelected )
259 {
260 selectionState.SelectTrack
261 ( *t, true, false );
262 trackFocus.Set( n ); // move focus to next track down
263 if (n)
264 n->EnsureVisible( true );
265 return;
266 }
267 }
268 else
269 {
270 auto n = * ++ tracks.FindLeader( t ); // Get next track
271 if( n == NULL ) // On last track so stay there
272 {
273 wxBell();
274 if( circularTrackNavigation )
275 {
276 n = *tracks.Any().begin();
277 trackFocus.Set( n ); // Wrap to the first track
278 if (n)
279 n->EnsureVisible( true );
280 return;
281 }
282 else
283 {
284 t->EnsureVisible();
285 return;
286 }
287 }
288 else
289 {
290 trackFocus.Set( n ); // move focus to next track down
291 n->EnsureVisible( true );
292 return;
293 }
294 }
295 }
296
297 }
298
299 /// Namespace for functions for project navigation menu (part of Extra menu)
300 namespace NavigationActions {
301
302 // exported helper functions
303 // none
304
305 // Menu handler functions
306
307 struct Handler
308 : CommandHandlerObject // MUST be the first base class!
309 , ClientData::Base
310 , PrefsListener
311 {
312
OnPrevWindowNavigationActions::Handler313 void OnPrevWindow(const CommandContext &context)
314 {
315 auto &project = context.project;
316 auto &window = GetProjectFrame( project );
317 auto isEnabled = window.IsEnabled();
318
319 wxWindow *w = wxGetTopLevelParent(wxWindow::FindFocus());
320 const auto & list = window.GetChildren();
321 auto iter = list.rbegin(), end = list.rend();
322
323 // If the project window has the current focus, start the search with the
324 // last child
325 if (w == &window)
326 {
327 }
328 // Otherwise start the search with the current window's previous sibling
329 else
330 {
331 while (iter != end && *iter != w)
332 ++iter;
333 if (iter != end)
334 ++iter;
335 }
336
337 // Search for the previous toplevel window
338 for (; iter != end; ++iter)
339 {
340 // If it's a toplevel and is visible (we have come hidden windows), then
341 // we're done
342 w = *iter;
343 if (w->IsTopLevel() && w->IsShown() && isEnabled)
344 {
345 break;
346 }
347 }
348
349 // Ran out of siblings, so make the current project active
350 if ((iter == end) && isEnabled)
351 {
352 w = &window;
353 }
354
355 // And make sure it's on top (only for floating windows...project window will
356 // not raise)
357 // (Really only works on Windows)
358 w->Raise();
359
360
361 #if defined(__WXMAC__) || defined(__WXGTK__)
362 // bug 868
363 // Simulate a TAB key press before continuing, else the cycle of
364 // navigation among top level windows stops because the keystrokes don't
365 // go to the CommandManager.
366 if (dynamic_cast<wxDialog*>(w)) {
367 w->SetFocus();
368 }
369 #endif
370 }
371
OnNextWindowNavigationActions::Handler372 void OnNextWindow(const CommandContext &context)
373 {
374 auto &project = context.project;
375 auto &window = GetProjectFrame( project );
376 auto isEnabled = window.IsEnabled();
377
378 wxWindow *w = wxGetTopLevelParent(wxWindow::FindFocus());
379 const auto & list = window.GetChildren();
380 auto iter = list.begin(), end = list.end();
381
382 // If the project window has the current focus, start the search with the
383 // first child
384 if (w == &window)
385 {
386 }
387 // Otherwise start the search with the current window's next sibling
388 else
389 {
390 // Find the window in this projects children. If the window with the
391 // focus isn't a child of this project (like when a dialog is created
392 // without specifying a parent), then we'll get back NULL here.
393 while (iter != end && *iter != w)
394 ++iter;
395 if (iter != end)
396 ++iter;
397 }
398
399 // Search for the next toplevel window
400 for (; iter != end; ++iter)
401 {
402 // If it's a toplevel, visible (we have hidden windows) and is enabled,
403 // then we're done. The IsEnabled() prevents us from moving away from
404 // a modal dialog because all other toplevel windows will be disabled.
405 w = *iter;
406 if (w->IsTopLevel() && w->IsShown() && w->IsEnabled())
407 {
408 break;
409 }
410 }
411
412 // Ran out of siblings, so make the current project active
413 if ((iter == end) && isEnabled)
414 {
415 w = &window;
416 }
417
418 // And make sure it's on top (only for floating windows...project window will
419 // not raise)
420 // (Really only works on Windows)
421 w->Raise();
422
423
424 #if defined(__WXMAC__) || defined(__WXGTK__)
425 // bug 868
426 // Simulate a TAB key press before continuing, else the cycle of
427 // navigation among top level windows stops because the keystrokes don't
428 // go to the CommandManager.
429 if (dynamic_cast<wxDialog*>(w)) {
430 w->SetFocus();
431 }
432 #endif
433 }
434
OnPrevFrameNavigationActions::Handler435 void OnPrevFrame(const CommandContext &context)
436 {
437 auto &project = context.project;
438 NextOrPrevFrame(project, false);
439 }
440
OnNextFrameNavigationActions::Handler441 void OnNextFrame(const CommandContext &context)
442 {
443 auto &project = context.project;
444 NextOrPrevFrame(project, true);
445 }
446
447 // Handler state:
448 bool mCircularTrackNavigation{};
449
OnCursorUpNavigationActions::Handler450 void OnCursorUp(const CommandContext &context)
451 {
452 auto &project = context.project;
453 DoPrevTrack( project, false, mCircularTrackNavigation );
454 }
455
OnCursorDownNavigationActions::Handler456 void OnCursorDown(const CommandContext &context)
457 {
458 auto &project = context.project;
459 DoNextTrack( project, false, mCircularTrackNavigation );
460 }
461
OnFirstTrackNavigationActions::Handler462 void OnFirstTrack(const CommandContext &context)
463 {
464 auto &project = context.project;
465 auto &trackFocus = TrackFocus::Get( project );
466 auto &tracks = TrackList::Get( project );
467
468 auto t = trackFocus.Get();
469 if (!t)
470 return;
471
472 auto f = *tracks.Any().begin();
473 if (t != f)
474 trackFocus.Set(f);
475 if (f)
476 f->EnsureVisible( t != f );
477 }
478
OnLastTrackNavigationActions::Handler479 void OnLastTrack(const CommandContext &context)
480 {
481 auto &project = context.project;
482 auto &trackFocus = TrackFocus::Get( project );
483 auto &tracks = TrackList::Get( project );
484
485 Track *t = trackFocus.Get();
486 if (!t)
487 return;
488
489 auto l = *tracks.Any().rbegin();
490 if (t != l)
491 trackFocus.Set(l);
492 if (l)
493 l->EnsureVisible( t != l );
494 }
495
OnShiftUpNavigationActions::Handler496 void OnShiftUp(const CommandContext &context)
497 {
498 auto &project = context.project;
499 DoPrevTrack( project, true, mCircularTrackNavigation );
500 }
501
OnShiftDownNavigationActions::Handler502 void OnShiftDown(const CommandContext &context)
503 {
504 auto &project = context.project;
505 DoNextTrack( project, true, mCircularTrackNavigation );
506 }
507
OnToggleNavigationActions::Handler508 void OnToggle(const CommandContext &context)
509 {
510 auto &project = context.project;
511 auto &trackFocus = TrackFocus::Get( project );
512 auto &selectionState = SelectionState::Get( project );
513
514 Track *t;
515
516 t = trackFocus.Get(); // Get currently focused track
517 if (!t)
518 return;
519
520 selectionState.SelectTrack
521 ( *t, !t->GetSelected(), true );
522 t->EnsureVisible( true );
523
524 trackFocus.UpdateAccessibility();
525
526 return;
527 }
528
UpdatePrefsNavigationActions::Handler529 void UpdatePrefs() override
530 {
531 mCircularTrackNavigation =
532 gPrefs->ReadBool(wxT("/GUI/CircularTrackNavigation"), false);
533 }
HandlerNavigationActions::Handler534 Handler()
535 {
536 UpdatePrefs();
537 }
538 Handler( const Handler & ) PROHIBITED;
539 Handler &operator=( const Handler & ) PROHIBITED;
540
541 }; // struct Handler
542
543 } // namespace
544
545 // Handler is stateful. Needs a factory registered with
546 // AudacityProject.
547 static const AudacityProject::AttachedObjects::RegisteredFactory key{
__anondfd3e3490402() 548 [](AudacityProject&) {
549 return std::make_unique< NavigationActions::Handler >(); } };
550
findCommandHandler(AudacityProject & project)551 static CommandHandlerObject &findCommandHandler(AudacityProject &project) {
552 return project.AttachedObjects::Get< NavigationActions::Handler >( key );
553 };
554
555 // Menu definitions
556
557 #define FN(X) (& NavigationActions::Handler :: X)
558
559 namespace {
560 using namespace MenuTable;
ExtraGlobalCommands()561 BaseItemSharedPtr ExtraGlobalCommands()
562 {
563 // Ceci n'est pas un menu
564 using Options = CommandManager::Options;
565
566 static BaseItemSharedPtr items{
567 ( FinderScope{ findCommandHandler },
568 Items( wxT("Navigation"),
569 Command( wxT("PrevWindow"), XXO("Move Backward Through Active Windows"),
570 FN(OnPrevWindow), AlwaysEnabledFlag,
571 Options{ wxT("Alt+Shift+F6") }.IsGlobal() ),
572 Command( wxT("NextWindow"), XXO("Move Forward Through Active Windows"),
573 FN(OnNextWindow), AlwaysEnabledFlag,
574 Options{ wxT("Alt+F6") }.IsGlobal() )
575 ) ) };
576 return items;
577 }
578
579 AttachedItem sAttachment2{
580 wxT("Optional/Extra/Part2"),
581 Shared( ExtraGlobalCommands() )
582 };
583
ExtraFocusMenu()584 BaseItemSharedPtr ExtraFocusMenu()
585 {
586 static const auto FocusedTracksFlags = TracksExistFlag() | TrackPanelHasFocus();
587
588 static BaseItemSharedPtr menu{
589 ( FinderScope{ findCommandHandler },
590 Menu( wxT("Focus"), XXO("Foc&us"),
591 Command( wxT("PrevFrame"),
592 XXO("Move &Backward from Toolbars to Tracks"), FN(OnPrevFrame),
593 AlwaysEnabledFlag, wxT("Ctrl+Shift+F6") ),
594 Command( wxT("NextFrame"),
595 XXO("Move F&orward from Toolbars to Tracks"), FN(OnNextFrame),
596 AlwaysEnabledFlag, wxT("Ctrl+F6") ),
597 Command( wxT("PrevTrack"), XXO("Move Focus to &Previous Track"),
598 FN(OnCursorUp), FocusedTracksFlags, wxT("Up") ),
599 Command( wxT("NextTrack"), XXO("Move Focus to &Next Track"),
600 FN(OnCursorDown), FocusedTracksFlags, wxT("Down") ),
601 Command( wxT("FirstTrack"), XXO("Move Focus to &First Track"),
602 FN(OnFirstTrack), FocusedTracksFlags, wxT("Ctrl+Home") ),
603 Command( wxT("LastTrack"), XXO("Move Focus to &Last Track"),
604 FN(OnLastTrack), FocusedTracksFlags, wxT("Ctrl+End") ),
605 Command( wxT("ShiftUp"), XXO("Move Focus to P&revious and Select"),
606 FN(OnShiftUp), FocusedTracksFlags, wxT("Shift+Up") ),
607 Command( wxT("ShiftDown"), XXO("Move Focus to N&ext and Select"),
608 FN(OnShiftDown), FocusedTracksFlags, wxT("Shift+Down") ),
609 Command( wxT("Toggle"), XXO("&Toggle Focused Track"), FN(OnToggle),
610 FocusedTracksFlags, wxT("Return") ),
611 Command( wxT("ToggleAlt"), XXO("Toggle Focuse&d Track"), FN(OnToggle),
612 FocusedTracksFlags, wxT("NUMPAD_ENTER") )
613 ) ) };
614 return menu;
615 }
616
617 AttachedItem sAttachment3{
618 wxT("Optional/Extra/Part2"),
619 Shared( ExtraFocusMenu() )
620 };
621
622 }
623
624 #undef FN
625