Audacity 3.2.0
CellularPanel.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 CellularPanel.cpp
6
7 Dominic Mazzoni
8 and lots of other contributors
9
10 Implements CellularPanel.
11
12********************************************************************//*****************************************************************/
29
30
31#include "CellularPanel.h"
32
33#include <wx/eventfilter.h>
34#include <wx/setup.h> // for wxUSE_* macros
35#include "KeyboardCapture.h"
36#include "UIHandle.h"
38#include "HitTestResult.h"
39#include "RefreshCode.h"
40#include "TrackPanelCell.h"
41
42// A singleton class that intercepts escape key presses when some cellular
43// panel is dragging
44struct CellularPanel::Filter : wxEventFilter
45{
47 {
48 wxEvtHandler::AddFilter( this );
49 }
50
52 {
53 wxEvtHandler::RemoveFilter( this );
54 }
55
56 static void Create()
57 {
58 static Filter instance;
59 }
60
61 int FilterEvent( wxEvent &event ) override
62 {
63 const auto type = event.GetEventType();
64 if (type == wxEVT_KEY_DOWN &&
65 static_cast< wxKeyEvent& >( event ).GetKeyCode() == WXK_ESCAPE ) {
66 bool eatEvent = false;
67 for (const auto &pPanel: {spClickedPanel, spEnteredPanel}) {
68 if (pPanel) {
69 eatEvent = true;
70 // Handle escape either in the clicked panel to abort a drag, or
71 // to switch among hit test candidates before button down in the
72 // entered (but not yet clicked) panel
73 pPanel->HandleEscapeKey( true );
74 }
75 }
76 if (eatEvent)
77 return Event_Processed;
78 }
79 else if ((type == wxEVT_LEFT_DOWN ||
80 type == wxEVT_RIGHT_DOWN ||
81 type == wxEVT_MIDDLE_DOWN)) {
82 if ( spClickedPanel &&
83 spClickedPanel != event.GetEventObject() ) {
84 // Clicking away from the panel doesn't necessarily change wxWidgets
85 // focus, so we use this global filter instead
86 spClickedPanel->DoKillFocus();
87 // Don't eat the event
88 }
89 }
90 return Event_Skip;
91 }
92
93 static wxWeakRef< CellularPanel > spClickedPanel;
94 static wxWeakRef< CellularPanel > spEnteredPanel;
95};
96
97wxWeakRef< CellularPanel > CellularPanel::Filter::spClickedPanel = nullptr;
98wxWeakRef< CellularPanel > CellularPanel::Filter::spEnteredPanel = nullptr;
99
101{
103
104 std::weak_ptr<TrackPanelCell> mLastCell;
105 std::vector<UIHandlePtr> mTargets;
106 size_t mTarget {};
108
111
112 std::weak_ptr<TrackPanelCell> mpClickedCell;
113
115};
116
117
118BEGIN_EVENT_TABLE(CellularPanel, OverlayPanel)
119 EVT_MOUSE_EVENTS(CellularPanel::OnMouseEvent)
120 EVT_MOUSE_CAPTURE_LOST(CellularPanel::OnCaptureLost)
121 EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, CellularPanel::OnCaptureKey)
122 EVT_KEY_DOWN(CellularPanel::OnKeyDown)
123 EVT_KEY_UP(CellularPanel::OnKeyUp)
124 EVT_CHAR(CellularPanel::OnChar)
125 EVT_SET_FOCUS(CellularPanel::OnSetFocus)
126 EVT_KILL_FOCUS(CellularPanel::OnKillFocus)
127 EVT_CONTEXT_MENU(CellularPanel::OnContextMenu)
129
131 wxWindow * parent, wxWindowID id,
132 const wxPoint & pos, const wxSize & size,
133 ViewInfo *viewInfo,
134 long style)
135: OverlayPanel(parent, id, pos, size, style)
136, mViewInfo( viewInfo )
137, mState{ std::make_unique<State>() }
138{
139 // Create the global event filter instance for CellularPanels only when the
140 // first CellularPanel is created, not sooner, so that the filter will be
141 // invoked before that for the application.
142 Filter::Create();
143}
144
146
148{
149 auto &state = *mState;
150 if (state.mUIHandle &&
151 state.mUIHandle->StopsOnKeystroke() ) {
152 // The bogus id isn't used anywhere, but may help with debugging.
153 // as this is sending a bogus mouse up. The mouse button is still actually down
154 // and may go up again.
155 const int idBogusUp = 2;
156 wxMouseEvent evt { wxEVT_LEFT_UP };
157 evt.SetId( idBogusUp );
158 evt.SetPosition(this->ScreenToClient(::wxGetMousePosition()));
159 this->ProcessEvent(evt);
160 }
161}
162
163void CellularPanel::Uncapture(bool escaping, wxMouseState *pState)
164{
165 auto state = ::wxGetMouseState();
166 if (!pState) {
167 // Remap the position
168 state.SetPosition(this->ScreenToClient(state.GetPosition()));
169 pState = &state;
170 }
171
172 if (HasCapture())
173 ReleaseMouse();
174 HandleMotion( *pState );
175
176 if ( escaping || !AcceptsFocus() )
177 Filter::spClickedPanel = nullptr;
178}
179
180bool CellularPanel::CancelDragging( bool escaping )
181{
182 auto &state = *mState;
183 if (state.mUIHandle) {
184 // copy shared_ptr for safety, as in HandleClick
185 auto handle = state.mUIHandle;
186 // UIHANDLE CANCEL
187 UIHandle::Result refreshResult = handle->Cancel(GetProject());
188 auto pClickedCell = state.mpClickedCell.lock();
189 if (pClickedCell)
191 pClickedCell.get(), {},
192 refreshResult | state.mMouseOverUpdateFlags );
193 state.mpClickedCell.reset();
194 state.mUIHandle.reset(), handle.reset(), ClearTargets();
195 Uncapture( escaping );
196 return true;
197 }
198 return false;
199}
200
202{
203 if (!down)
204 return false;
205
206 {
207 auto target = Target();
208 const auto pProject = GetProject();
209 if (target && target->HasEscape(pProject) && target->Escape(pProject)) {
211 return true;
212 }
213 }
214
215 auto &state = *mState;
216 if (state.mUIHandle) {
217 CancelDragging( true );
218 return true;
219 }
220
221 if (ChangeTarget(true, false)) {
223 return true;
224 }
225
226 return false;
227}
228
229void CellularPanel::UpdateMouseState(const wxMouseState &state)
230{
231 mLastMouseState = state;
232
233 // Simulate a down button if none, so hit test routines can anticipate
234 // which button will be clicked
235 if (!state.ButtonIsDown(wxMOUSE_BTN_ANY)) {
236#ifdef __WXOSX__
237 if (state.RawControlDown())
238 // On Mac we can distinctly anticipate "right" click (as Control+click)
239 mLastMouseState.SetRightDown( true ),
240 mLastMouseState.SetLeftDown( false );
241 else
242#endif
243 // Anticipate a left click by default
244 mLastMouseState.SetRightDown( false ),
245 mLastMouseState.SetLeftDown( true );
246 }
247}
248
250{
252}
253
255{
256 // Come here on modifier key or mouse button transitions,
257 // or on starting or stopping of play or record,
258 // or change of toolbar button,
259 // and change the cursor appropriately.
260
261 // Get the button and key states
262 auto state = ::wxGetMouseState();
263 // Remap the position
264 state.SetPosition(this->ScreenToClient(state.GetPosition()));
265
266 HandleMotion( state, doHit );
267}
268
276void CellularPanel::HandleMotion( wxMouseState &inState, bool doHit )
277{
278 UpdateMouseState( inState );
279
280 const auto foundCell = FindCell( inState.m_x, inState.m_y );
281 auto &rect = foundCell.rect;
282 auto &pCell = foundCell.pCell;
283 const TrackPanelMouseState tpmState{ mLastMouseState, rect, pCell };
284 HandleMotion( tpmState, doHit );
285}
286
288( const TrackPanelMouseState &tpmState, bool doHit )
289{
290 auto &state = *mState;
291 auto handle = state.mUIHandle;
292
293 auto newCell = tpmState.pCell;
294 auto oldCell = state.mLastCell.lock();
295 auto oldHandle = Target();
296
297 TranslatableString status, tooltip;
298 wxCursor *pCursor{};
299 unsigned refreshCode = 0;
300 if ( ! doHit ) {
301 // Dragging or not
302 handle = Target();
303
304 // Assume cell does not change but target does
305 refreshCode = state.mMouseOverUpdateFlags;
306 state.mMouseOverUpdateFlags = 0;
307 }
308 else if ( !state.mUIHandle ) {
309 // Not yet dragging.
310
311 unsigned updateFlags = state.mMouseOverUpdateFlags;
312
313 // First check whether crossing cell to cell
314 if ( newCell == oldCell )
315 oldCell.reset();
316 else {
317 // Forget old targets
318 ClearTargets();
319 // Re-draw any highlighting
320 if (oldCell) {
322 oldCell.get(), oldCell.get(), updateFlags);
323 }
324 }
325
326 auto oldPosition = state.mTarget;
327
328 // Now do the
329 // UIHANDLE HIT TEST !
330 state.mTargets.clear();
331 if (newCell)
332 state.mTargets = newCell->HitTest(tpmState, GetProject());
333 state.mTarget = 0;
334
335 // Find the old target's NEW place if we can
336 if (oldHandle) {
337 auto begin = state.mTargets.begin(), end = state.mTargets.end(),
338 iter = std::find(begin, end, oldHandle);
339 if (iter != end) {
340 size_t newPosition = iter - begin;
341 if (newPosition <= oldPosition)
342 state.mTarget = newPosition;
343 // else, some NEW hit at this position takes priority
344 }
345 }
346
347 handle = Target();
348
349 state.mLastCell = newCell;
350
351 // These lines caused P2 Bug 2617, repeated refreshing using all CPU.
352 // Disabling them might be causing something to not refresh,
353 // but so far I have not found a downside to disabling them. JKC
354
355 // VS: https://github.com/audacity/audacity/issues/1363
356 // Extensive refresh request fixed by using std::move on
357 // new envelope handle instance
358 if (!oldCell && oldHandle != handle)
359 // Did not move cell to cell, but did change the target
360 refreshCode = updateFlags;
361
362
363 if (handle && handle != oldHandle)
364 handle->Enter(true, GetProject());
365
366 if (oldHandle == handle)
367 oldHandle.reset();
368 }
369
370 // UIHANDLE PREVIEW
371 // Update status message and cursor, whether dragging or not
372 if (handle) {
373 auto preview = handle->Preview( tpmState, GetProject() );
374 status = preview.message;
375 tooltip = preview.tooltip;
376 pCursor = preview.cursor;
377 auto code = handle->GetChangeHighlight();
378 handle->SetChangeHighlight(RefreshCode::RefreshNone);
379 refreshCode |= code;
380 state.mMouseOverUpdateFlags |= code;
381 }
382 if (newCell &&
383 (!pCursor || status.empty() || tooltip.empty())) {
384 // Defaulting of cursor, tooltip, and status if there is no handle,
385 // or if the handle does not specify them
386 const auto preview = newCell->DefaultPreview( tpmState, GetProject() );
387 if (!pCursor)
388 pCursor = preview.cursor;
389 if (status.empty())
390 status = preview.message;
391 if (tooltip.empty())
392 tooltip = preview.tooltip;
393 }
394 if (!pCursor) {
395 // Ultimate default cursor
396 static wxCursor defaultCursor{ wxCURSOR_DEFAULT };
397 pCursor = &defaultCursor;
398 }
399
400 // Update status, tooltip, and cursor only if we're dragging, or the mouse
401 // was in one of our cells and nobody else is dragging
402 if (handle || (newCell && !wxWindow::GetCapture())) {
403 UpdateStatusMessage(status);
404
405#if wxUSE_TOOLTIPS
406 if (tooltip.Translation() != GetToolTipText()) {
407 // Unset first, by analogy with AButton
408 UnsetToolTip();
409 if (handle != oldHandle)
410 SetToolTip(tooltip);
411 }
412#endif
413
414 if (pCursor)
415 SetCursor( *pCursor );
416 }
417 else if ( oldCell || oldHandle )
418 // Leaving a cell or hit test target with no replacement
420
421 if (newCell)
422 ProcessUIHandleResult(newCell.get(), newCell.get(), refreshCode);
423}
424
426{
427 // Make transition into an empty CellularPanel state
428 auto state = ::wxGetMouseState();
429 const wxRect rect;
430 std::shared_ptr<TrackPanelCell> pCell;
431 TrackPanelMouseState tpmState{ state, rect, pCell };
432 HandleMotion( tpmState );
433}
434
436{
437 auto &state = *mState;
438 // Is there a nontrivial TAB key rotation?
439 if ( state.mTargets.size() > 1 )
440 return true;
441 auto target = Target();
442 return target && target->HasRotation();
443}
444
446{
447 if (IsMouseCaptured())
448 return true;
449
450 auto &state = *mState;
451 if (state.mTarget + 1 == state.mTargets.size() &&
452 Target() &&
454 return false;
455
456 return state.mTargets.size() > 0;
457}
458
459bool CellularPanel::ChangeTarget(bool forward, bool cycle)
460{
461 auto &state = *mState;
462 auto size = state.mTargets.size();
463
464 auto target = Target();
465 if (target && target->HasRotation()) {
466 if(target->Rotate(forward))
467 return true;
468 else if (cycle && (size == 1 || IsMouseCaptured())) {
469 // Rotate through the states of this target only.
470 target->Enter(forward, GetProject());
471 return true;
472 }
473 }
474
475 if (!cycle &&
476 ((forward && state.mTarget + 1 == size) ||
477 (!forward && state.mTarget == 0)))
478 return false;
479
480 if (size > 1) {
481 if (forward)
482 ++state.mTarget;
483 else
484 state.mTarget += size - 1;
485 state.mTarget %= size;
486 if (Target())
487 Target()->Enter(forward, GetProject());
488 return true;
489 }
490
491 return false;
492}
493
496{
497 auto &state = *mState;
498 return state.mUIHandle != NULL;
499}
500
501void CellularPanel::OnContextMenu(wxContextMenuEvent & WXUNUSED(event))
502{
504}
505
508{
509 auto pCell = tpmEvent.pCell;
510 if (!pCell)
511 return;
512
513 auto &event = tpmEvent.event;
514 double steps {};
515#if defined(__WXMAC__) && defined(EVT_MAGNIFY)
516 // PRL:
517 // Pinch and spread implemented in wxWidgets 3.1.0, or cherry-picked from
518 // the future in custom build of 3.0.2
519 if (event.Magnify()) {
520 event.SetControlDown(true);
521 steps = 2 * event.GetMagnification();
522 }
523 else
524#endif
525 {
526 steps = event.m_wheelRotation /
527 (event.m_wheelDelta > 0 ? (double)event.m_wheelDelta : 120.0);
528 }
529
530 if(event.GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL) {
531 // Two-fingered horizontal swipe on mac is treated like shift-mousewheel
532 event.SetShiftDown(true);
533 // This makes the wave move in the same direction as the fingers, and the scrollbar
534 // thumb moves oppositely
535 steps *= -1;
536 }
537
538 tpmEvent.steps = steps;
539
540 if(!event.HasAnyModifiers()) {
541 // We will later un-skip if we do anything, but if we don't,
542 // propagate the event up for the sake of the scrubber
543 event.Skip();
544 event.ResumePropagation(wxEVENT_PROPAGATE_MAX);
545 }
546
547 unsigned result =
548 pCell->HandleWheelRotation( tpmEvent, GetProject() );
550 pCell.get(), pCell.get(), result);
551}
552
553void CellularPanel::OnCaptureKey(wxCommandEvent & event)
554{
555 auto &state = *mState;
556 state.mEnableTab = false;
557 wxKeyEvent *kevent = static_cast<wxKeyEvent *>(event.GetEventObject());
558 const auto code = kevent->GetKeyCode();
559 if ( WXK_ESCAPE != code )
561
562 // Give focused cell precedence
563 const auto t = GetFocusedCell();
564 if (t) {
565 const unsigned refreshResult =
566 t->CaptureKey(*kevent, *mViewInfo, this, GetProject());
567 ProcessUIHandleResult(t, t, refreshResult);
568 event.Skip(kevent->GetSkipped());
569 }
570
571#if 0
572 // Special TAB key handling, but only if the cell didn't capture it
573 if ( !(t && !kevent->GetSkipped()) &&
574 WXK_TAB == code && HasRotation() ) {
575 // Override TAB navigation in wxWidgets, by not skipping
576 event.Skip(false);
577 mEnableTab = true;
578 return;
579 }
580 else
581#endif
582 if (!t)
583 event.Skip();
584}
585
586void CellularPanel::OnKeyDown(wxKeyEvent & event)
587{
588 switch (event.GetKeyCode())
589 {
590 case WXK_ESCAPE:
591 // This switch case is now redundant with the global filter
592 if(HandleEscapeKey(true))
593 // Don't skip the event, eat it so that
594 // AudacityApp does not also stop any playback.
595 return;
596 else
597 break;
598
599 case WXK_ALT:
600 case WXK_SHIFT:
601 case WXK_CONTROL:
602#ifdef __WXOSX__
603 case WXK_RAW_CONTROL:
604#endif
606 break;
607
608#if 0
609 case WXK_TAB:
610 if ( mEnableTab && HasRotation() ) {
611 ChangeTarget( !event.ShiftDown(), true );
613 return;
614 }
615 else
616 break;
617#endif
618 }
619
620 const auto t = GetFocusedCell();
621
622 if (t) {
623 const unsigned refreshResult =
624 t->KeyDown(event, *mViewInfo, this, GetProject());
625 ProcessUIHandleResult(t, t, refreshResult);
626 }
627 else
628 event.Skip();
629}
630
631void CellularPanel::OnChar(wxKeyEvent & event)
632{
633 switch (event.GetKeyCode())
634 {
635 case WXK_ESCAPE:
636 case WXK_ALT:
637 case WXK_SHIFT:
638 case WXK_CONTROL:
639 case WXK_PAGEUP:
640 case WXK_PAGEDOWN:
641 return;
642 }
643
644 const auto t = GetFocusedCell();
645 if (t) {
646 const unsigned refreshResult =
647 t->Char(event, *mViewInfo, this, GetProject());
648 ProcessUIHandleResult(t, t, refreshResult);
649 }
650 else
651 event.Skip();
652}
653
654void CellularPanel::OnKeyUp(wxKeyEvent & event)
655{
656 bool didSomething = false;
657 switch (event.GetKeyCode())
658 {
659 case WXK_ESCAPE:
660 didSomething = HandleEscapeKey(false);
661 break;
662
663 case WXK_ALT:
664 case WXK_SHIFT:
665 case WXK_CONTROL:
666#ifdef __WXOSX__
667 case WXK_RAW_CONTROL:
668#endif
670 break;
671 }
672
673 if (didSomething)
674 return;
675
676 const auto t = GetFocusedCell();
677 if (t) {
678 const unsigned refreshResult =
679 t->KeyUp(event, *mViewInfo, this, GetProject());
680 ProcessUIHandleResult(t, t, refreshResult);
681 return;
682 }
683
684 event.Skip();
685}
686
688void CellularPanel::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event))
689{
690 auto &state = *mState;
691 state.mUIHandle.reset();
692 Leave();
693
694 // This is bad. We are lying abou the event by saying it is a mouse up.
695 wxMouseEvent e(wxEVT_LEFT_UP);
696 e.SetId( kCaptureLostEventId );
697
698 e.m_x = state.mMouseMostRecentX;
699 e.m_y = state.mMouseMostRecentY;
700
701 OnMouseEvent(e);
702}
703
707void CellularPanel::OnMouseEvent(wxMouseEvent & event)
708try
709{
710 const auto foundCell = FindCell( event.m_x, event.m_y );
711 auto &rect = foundCell.rect;
712 auto &pCell = foundCell.pCell;
713
714 const auto size = GetSize();
715 TrackPanelMouseEvent tpmEvent{ event, rect, size, pCell };
716
717#if defined(__WXMAC__) && defined(EVT_MAGNIFY)
718 // PRL:
719 // Pinch and spread implemented in wxWidgets 3.1.0, or cherry-picked from
720 // the future in custom build of 3.0.2
721 if (event.Magnify()) {
722 HandleWheelRotation( tpmEvent );
723 }
724#endif
725
726 // If a mouse event originates from a keyboard context menu event then
727 // event.GetPosition() == wxDefaultPosition. wxContextMenu events are handled in
728 // CellularPanel::OnContextMenu(), and therefore associated mouse events are ignored here.
729 // Not ignoring them was causing bug 613: the mouse events were interpreted as clicking
730 // outside the tracks.
731 if (event.GetPosition() == wxDefaultPosition && (event.RightDown() || event.RightUp())) {
732 event.Skip();
733 return;
734 }
735
736 if (event.m_wheelRotation != 0)
737 HandleWheelRotation( tpmEvent );
738
739 if (event.LeftDown() || event.LeftIsDown() || event.Moving()) {
740 // Skip, even if we do something, so that the left click or drag
741 // may have an additional effect in the scrubber.
742 event.Skip();
743 event.ResumePropagation(wxEVENT_PROPAGATE_MAX);
744 }
745
746 auto &state = *mState;
747 state.mMouseMostRecentX = event.m_x;
748 state.mMouseMostRecentY = event.m_y;
749
750 if (event.LeftDown()) {
751 // The activate event is used to make the
752 // parent window 'come alive' if it didn't have focus.
753 wxActivateEvent e;
754 GetParent()->GetEventHandler()->ProcessEvent(e);
755 }
756
757 if (event.Entering())
758 {
760 }
761 else if (event.Leaving())
762 {
763 if (Filter::spEnteredPanel == this)
764 Filter::spEnteredPanel = nullptr;
765 Leave();
766
767 auto buttons =
768 // Bug 1325: button state in Leaving events is unreliable on Mac.
769 // Poll the global state instead.
770 // event.ButtonIsDown(wxMOUSE_BTN_ANY);
771 ::wxGetMouseState().ButtonIsDown(wxMOUSE_BTN_ANY);
772
773 if (!buttons) {
774 CancelDragging( false );
775
776#if defined(__WXMAC__)
777
778 // We must install the cursor ourselves since the window under
779 // the mouse is no longer this one and wx2.8.12 makes that check.
780 // Should re-evaluate with wx3.
781 wxSTANDARD_CURSOR->MacInstall();
782#endif
783 }
784 }
785
786 if (state.mUIHandle) {
787 auto pClickedCell = state.mpClickedCell.lock();
788 if (event.Dragging()) {
789 // UIHANDLE DRAG
790 // copy shared_ptr for safety, as in HandleClick
791 auto handle = state.mUIHandle;
792 const UIHandle::Result refreshResult =
793 handle->Drag( tpmEvent, GetProject() );
795 (pClickedCell.get(), pCell.get(), refreshResult);
796 state.mMouseOverUpdateFlags |= refreshResult;
797 if (refreshResult & RefreshCode::Cancelled) {
798 // Drag decided to abort itself
799 state.mUIHandle.reset(), handle.reset(), ClearTargets();
800 state.mpClickedCell.reset();
801 Uncapture( false, &event );
802 }
803 else {
804 UpdateMouseState(event);
805 TrackPanelMouseState tpmState{ mLastMouseState, rect, pCell };
806 HandleMotion( tpmState );
807 }
808 }
809 else if (event.ButtonUp()) {
810 // UIHANDLE RELEASE
811 // copy shared_ptr for safety, as in HandleClick
812 auto handle = state.mUIHandle;
813 unsigned moreFlags = state.mMouseOverUpdateFlags;
814 UIHandle::Result refreshResult =
815 handle->Release( tpmEvent, GetProject(), this );
817 (pClickedCell.get(), pCell.get(),
818 refreshResult | moreFlags);
819 state.mUIHandle.reset(), handle.reset(), ClearTargets();
820 state.mpClickedCell.reset();
821 // will also Uncapture() below
822 }
823 }
824 else if ( event.GetEventType() == wxEVT_MOTION )
825 // Update status message and cursor, not during drag
826 // consider it not a drag, even if button is down during motion, if
827 // mUIHandle is null, as it becomes during interrupted drag
828 // (e.g. by hitting space to play while dragging an envelope point)
829 HandleMotion( event );
830 else if ( event.ButtonDown() || event.ButtonDClick() )
831 HandleClick( tpmEvent );
832
833 if (event.ButtonDown() && IsMouseCaptured()) {
834 if (!HasCapture())
835 CaptureMouse();
836 }
837
838 if (event.ButtonUp())
839 Uncapture( false );
840}
841catch( ... )
842{
843 // Abort any dragging, as if by hitting Esc
844 if ( CancelDragging( true ) )
845 ;
846 else {
847 Uncapture( true );
848 Refresh(false);
849 }
850 throw;
851}
852
853namespace {
854
856public:
858 const std::shared_ptr<TrackPanelCell> &pCell )
859 : mwCell{ pCell }
860 {}
861
863
864 std::shared_ptr<const Channel> FindChannel() const override
865 { return nullptr; }
866
867 virtual Result Click
868 (const TrackPanelMouseEvent &event, AudacityProject *pProject) override
869 {
871 }
872
873 virtual Result Drag
874 (const TrackPanelMouseEvent &event, AudacityProject *pProject) override
875 {
877 }
878
880 (const TrackPanelMouseState &state, AudacityProject *pProject) override
881 {
882 return {};
883 }
884
886 (const TrackPanelMouseEvent &event, AudacityProject *pProject,
887 wxWindow *pParent) override
888 {
889 if (auto pCell = mwCell.lock()) {
890 auto point = event.event.GetPosition();
891 return pCell->DoContextMenu(event.rect, pParent, &point, pProject);
892 }
894 }
895
896 virtual Result Cancel(AudacityProject *pProject) override
897 {
899 }
900
901private:
902 std::weak_ptr<TrackPanelCell> mwCell;
903};
904
905DefaultRightButtonHandler::~DefaultRightButtonHandler()
906{
907}
908
909}
910
912{
913 auto pCell = tpmEvent.pCell;
914 // Do hit test once more, in case the button really pressed was not the
915 // one "anticipated."
916 {
917 TrackPanelMouseState tpmState{
918 tpmEvent.event,
919 tpmEvent.rect,
920 tpmEvent.pCell
921 };
922 HandleMotion( tpmState );
923 }
924
925 auto &state = *mState;
926 state.mUIHandle = Target();
927 if (tpmEvent.event.RightDown() &&
928 !(state.mUIHandle && state.mUIHandle->HandlesRightClick())) {
929 if (auto pCell = state.mLastCell.lock())
930 state.mUIHandle = std::make_shared<DefaultRightButtonHandler>(pCell);
931 }
932
933 if (state.mUIHandle) {
934 // UIHANDLE CLICK
935 // Make another shared pointer to the handle, in case recursive
936 // event dispatching otherwise tries to delete the handle.
937 auto handle = state.mUIHandle;
938 UIHandle::Result refreshResult =
939 handle->Click( tpmEvent, GetProject() );
940 if (refreshResult & RefreshCode::Cancelled)
941 state.mUIHandle.reset(), handle.reset(), ClearTargets();
942 else {
944
945#if wxUSE_TOOLTIPS
946 // Remove any outstanding tooltip
947 UnsetToolTip();
948#endif
949
950 if( !HasFocus() && AcceptsFocus() )
951 SetFocusIgnoringChildren();
952
953 state.mpClickedCell = pCell;
954
955 // Perhaps the clicked handle wants to update cursor and state message
956 // after a click.
957 TrackPanelMouseState tpmState{
958 tpmEvent.event,
959 tpmEvent.rect,
960 tpmEvent.pCell
961 };
962 HandleMotion( tpmState );
963 }
965 pCell.get(), pCell.get(), refreshResult);
966 state.mMouseOverUpdateFlags |= refreshResult;
967 }
968}
969
971{
972 if( !pCell ) {
973 pCell = GetFocusedCell();
974 if( !pCell )
975 return;
976 }
977
978 const auto delegate = pCell->ContextMenuDelegate();
979 if (!delegate)
980 return;
981
982 auto rect = FindRect( *delegate );
983 const UIHandle::Result refreshResult =
984 delegate->DoContextMenu(rect, this, nullptr, GetProject());
985
986 // To do: use safer shared_ptr to pCell
987 ProcessUIHandleResult(pCell, pCell, refreshResult);
988}
989
990void CellularPanel::OnSetFocus(wxFocusEvent &event)
991{
993 Refresh( false);
994}
995
997{
998 if (auto pCell = GetFocusedCell()) {
999 auto refreshResult = pCell->LoseFocus(GetProject());
1000 auto &state = *mState;
1001 auto pClickedCell = state.mpClickedCell.lock();
1002 if (pClickedCell)
1003 ProcessUIHandleResult( pClickedCell.get(), {}, refreshResult );
1004 }
1005 Refresh( false);
1006}
1007
1008void CellularPanel::OnKillFocus(wxFocusEvent & WXUNUSED(event))
1009{
1010 DoKillFocus();
1012 {
1014 }
1015}
1016
1017// Empty out-of-line default functions to fill Visitor's vtable
1020 const wxRect &rect, TrackPanelCell &cell ) {}
1022 const wxRect &rect, TrackPanelGroup &group ) {}
1024 const wxRect &rect, TrackPanelGroup &group ) {}
1025
1026// Public, top-level entry for generalized Visit
1028{
1029 Visit( GetClientRect(), Root(), visitor );
1030}
1031
1032// Common utility class for the functions that follow
1033namespace {
1037
1038 // Visit cells only
1039 Adaptor( const SimpleCellVisitor& function_ )
1040 : function{ [&](const wxRect &rect, TrackPanelNode &cell) {
1041 return function_( rect, static_cast<TrackPanelCell&>(cell) );
1042 } }
1043 {}
1044
1045 // Visit cells and groups, each once only, choosing pre- or post- ordering
1046 // for the groups
1047 Adaptor( const SimpleNodeVisitor &function_, bool pre_ )
1048 : function{ function_ }, pre{ pre_ }, post{ ! pre_ } {}
1049
1050 void VisitCell( const wxRect &rect, TrackPanelCell &cell ) override
1051 { return function( rect, cell ); }
1052 void BeginGroup( const wxRect &rect, TrackPanelGroup &group ) override
1053 { if (pre) return function( rect, group ); }
1054 void EndGroup( const wxRect &rect, TrackPanelGroup &group ) override
1055 { if (post) return function( rect, group ); }
1056
1058 const bool pre{ false }, post{ false };
1059 };
1060}
1061
1062// Simplified entry points for visits that don't need all the generality of
1063// CellularPanel::Visitor
1065{
1066 Adaptor adaptor{ visitor };
1067 Visit( adaptor );
1068}
1069
1071{
1072 Adaptor adaptor{ visitor, true };
1073 Visit( adaptor );
1074}
1075
1077{
1078 Adaptor adaptor{ visitor, false };
1079 Visit( adaptor );
1080}
1081
1082namespace {
1084 const wxRect &rect, bool divideX,
1085 const TrackPanelGroup::Refinement &children,
1086 const TrackPanelGroup::Refinement::const_iterator iter)
1087 {
1088 const auto next = iter + 1;
1089 const auto end = children.end();
1090 wxCoord nextCoord;
1091 if (next == end)
1092 nextCoord = std::max( iter->first,
1093 divideX ? rect.GetRight() : rect.GetBottom() );
1094 else
1095 nextCoord = next->first - 1;
1096
1097 auto lesser = iter->first;
1098 auto greater = nextCoord;
1099
1100 auto result = rect;
1101 if (divideX)
1102 result.SetLeft(lesser), result.SetRight(greater);
1103 else
1104 result.SetTop(lesser), result.SetBottom(greater);
1105
1106 return result;
1107 };
1108}
1109
1110// Private, recursive implementation function of Visit
1112 const wxRect &rect, const std::shared_ptr<TrackPanelNode> &node,
1113 Visitor &visitor )
1114{
1115 if (auto pCell = dynamic_cast<TrackPanelCell*>(node.get()))
1116 visitor.VisitCell( rect, *pCell );
1117 else if (auto pGroup = dynamic_cast<TrackPanelGroup*>(node.get())) {
1118 visitor.BeginGroup( rect, *pGroup );
1119
1120 // Recur on children
1121 const auto results = pGroup->Children( rect );
1122 const bool divideX = results.first == TrackPanelGroup::Axis::X;
1123 const auto &children = results.second;
1124 const auto begin = children.begin(), end = children.end();
1125 for (auto iter = begin; iter != end; ++iter)
1126 Visit(
1127 Subdivide(rect, divideX, children, iter), iter->second, visitor );
1128
1129 visitor.EndGroup( rect, *pGroup );
1130 }
1131 else
1132 return;
1133}
1134
1135auto CellularPanel::FindCell(int mouseX, int mouseY) -> FoundCell
1136{
1137 auto rect = this->GetClientRect();
1138 auto node = Root();
1139 while (node) {
1140 if ( auto pCell = std::dynamic_pointer_cast< TrackPanelCell >( node ) )
1141 // Found the bottom of the hierarchy
1142 return { pCell, rect };
1143 else if ( auto pGroup = dynamic_cast< TrackPanelGroup* >( node.get() ) ) {
1144 // Ask node for its subdivision
1145 const auto results = pGroup->Children( rect );
1146 const bool divideX = results.first == TrackPanelGroup::Axis::X;
1147 const auto &children = results.second;
1148
1149 // Find the correct child
1150 const auto begin = children.begin(), end = children.end();
1151 auto iter = std::upper_bound( begin, end,
1152 (divideX ? mouseX : mouseY),
1153 [&]( wxCoord coord, const TrackPanelGroup::Child &child ) {
1154 return coord < child.first;
1155 }
1156 );
1157 if (iter == begin)
1158 break;
1159 --iter;
1160
1161 // Descend the hierarchy of nodes
1162 rect = Subdivide(rect, divideX, children, iter);
1163 node = iter->second;
1164 }
1165 else
1166 // Nulls in the array of children are allowed, to define a void with
1167 // no cell
1168 break;
1169 }
1170
1171 return { {}, {} };
1172}
1173
1175{
1176 wxRect result;
1177
1178 struct Stop{};
1179 try { VisitCells( [&]( const wxRect &rect, TrackPanelCell &visited ) {
1180 if ( &visited == &cell )
1181 result = rect, throw Stop{};
1182 } ); }
1183 catch ( const Stop& ) {}
1184
1185 return result;
1186}
1187
1189 const std::function< bool( TrackPanelNode& ) > &pred)
1190{
1191 wxRect result;
1192
1193 struct Stop{};
1194 try { VisitPreorder( [&]( const wxRect &rect, TrackPanelNode &visited ) {
1195 if ( pred( visited ) )
1196 result = rect, throw Stop{};
1197 } ); }
1198 catch ( const Stop& ) {}
1199
1200 return result;
1201}
1202
1204{
1205 auto &state = *mState;
1206 if (state.mTargets.size())
1207 return state.mTargets[state.mTarget];
1208 else
1209 return {};
1210}
1211
1213{
1214 auto &state = *mState;
1215 return state.mMouseMostRecentX;
1216}
1217
1219{
1220 auto &state = *mState;
1221 // Forget the rotation of hit test candidates when the mouse moves from
1222 // cell to cell or outside of the panel entirely.
1223 state.mLastCell.reset();
1224 state.mTargets.clear();
1225 state.mTarget = 0;
1226 state.mMouseOverUpdateFlags = 0;
1227}
1228
1229std::shared_ptr<TrackPanelCell> CellularPanel::LastCell() const
1230{
1231 auto &state = *mState;
1232 return state.mLastCell.lock();
1233}
1234
1235void CellularPanel::Draw( TrackPanelDrawingContext &context, unsigned nPasses )
1236{
1237 const auto panelRect = GetClientRect();
1238 auto lastCell = LastCell();
1239 for ( unsigned iPass = 0; iPass < nPasses; ++iPass ) {
1240
1241 VisitPostorder( [&]( const wxRect &rect, TrackPanelNode &node ) {
1242
1243 // Draw the node
1244 const auto newRect = node.DrawingArea(
1245 context, rect, panelRect, iPass );
1246 if ( newRect.Intersects( panelRect ) )
1247 node.Draw( context, newRect, iPass );
1248
1249 // Draw the current handle if it is associated with the node
1250 if ( &node == lastCell.get() ) {
1251 auto target = Target();
1252 if ( target ) {
1253 const auto targetRect =
1254 target->DrawingArea( context, rect, panelRect, iPass );
1255 if ( targetRect.Intersects( panelRect ) )
1256 target->Draw( context, targetRect, iPass );
1257 }
1258 }
1259
1260 } ); // nodes
1261
1262 } // passes
1263}
END_EVENT_TABLE()
std::shared_ptr< UIHandle > UIHandlePtr
Definition: CellularPanel.h:28
struct State mState
EVT_COMMAND(wxID_ANY, EVT_FREQUENCYTEXTCTRL_UPDATED, LabelDialog::OnFreqUpdate) LabelDialog
Definition: LabelDialog.cpp:89
const int kCaptureLostEventId
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Formerly part of TrackPanel, this abstract base class has no special knowledge of Track objects and i...
Definition: CellularPanel.h:34
void Draw(TrackPanelDrawingContext &context, unsigned nPasses)
bool IsMouseCaptured()
Determines if a modal tool is active.
void DoContextMenu(TrackPanelCell *pCell=nullptr)
void UpdateMouseState(const wxMouseState &state)
virtual std::shared_ptr< TrackPanelNode > Root()=0
virtual AudacityProject * GetProject() const =0
void HandleModifierKey()
std::unique_ptr< State > mState
void VisitCells(const SimpleCellVisitor &visitor)
UIHandlePtr Target()
ViewInfo * mViewInfo
void OnKeyDown(wxKeyEvent &event)
void VisitPreorder(const SimpleNodeVisitor &visitor)
FoundCell FindCell(int mouseX, int mouseY)
void HandleWheelRotation(TrackPanelMouseEvent &tpmEvent)
Handle mouse wheel rotation (for zoom in/out, vertical and horizontal scrolling)
wxRect FindRect(const TrackPanelCell &cell)
~CellularPanel() override
void OnCaptureKey(wxCommandEvent &event)
bool HandleEscapeKey(bool down)
bool CancelDragging(bool escaping)
void OnMouseEvent(wxMouseEvent &event)
void OnKeyUp(wxKeyEvent &event)
void OnChar(wxKeyEvent &event)
std::shared_ptr< TrackPanelCell > LastCell() const
void VisitPostorder(const SimpleNodeVisitor &visitor)
void HandleMotion(wxMouseState &state, bool doHit=true)
virtual void UpdateStatusMessage(const TranslatableString &)=0
void HandleClick(const TrackPanelMouseEvent &tpmEvent)
void OnSetFocus(wxFocusEvent &event)
void OnCaptureLost(wxMouseCaptureLostEvent &event)
Should handle the case when the mouse capture is lost. (MSW only)
std::function< void(const wxRect &rect, TrackPanelCell &cell) > SimpleCellVisitor
Definition: CellularPanel.h:75
virtual void SetFocusedCell()=0
void HandleInterruptedDrag()
void OnKillFocus(wxFocusEvent &event)
wxMouseState mLastMouseState
void OnContextMenu(wxContextMenuEvent &event)
std::function< void(const wxRect &rect, TrackPanelNode &node) > SimpleNodeVisitor
Definition: CellularPanel.h:80
bool ChangeTarget(bool forward, bool cycle)
void Uncapture(bool escaping, wxMouseState *pState=nullptr)
virtual TrackPanelCell * GetFocusedCell()=0
void Visit(Visitor &visitor)
wxCoord MostRecentXCoord() const
virtual void ProcessUIHandleResult(TrackPanelCell *pClickedCell, TrackPanelCell *pLatestCell, unsigned refreshResult)=0
void HandleCursorForPresentMouseState(bool doHit=true)
virtual std::shared_ptr< TrackPanelCell > ContextMenuDelegate()
virtual void Draw(TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass)
virtual wxRect DrawingArea(TrackPanelDrawingContext &context, const wxRect &rect, const wxRect &panelRect, unsigned iPass)
std::vector< Child > Refinement
std::pair< wxCoord, std::shared_ptr< TrackPanelNode > > Child
The TrackPanel is built up of nodes, subtrees of the CellularPanel's area Common base class for Track...
Holds a msgid for the translation catalog; may also bind format arguments.
wxString Translation() const
Short-lived drawing and event-handling object associated with a TrackPanelCell.
Definition: UIHandle.h:36
unsigned Result
Definition: UIHandle.h:39
virtual Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
virtual Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
virtual Result Cancel(AudacityProject *pProject) override
DefaultRightButtonHandler(const std::shared_ptr< TrackPanelCell > &pCell)
virtual HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
std::shared_ptr< const Channel > FindChannel() const override
virtual Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
void SetToolTip(const TranslatableString &toolTip)
bool IsHandler(const wxWindow *handler)
void Release(wxWindow *handler)
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
auto begin(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:150
wxRect Subdivide(const wxRect &rect, bool divideX, const TrackPanelGroup::Refinement &children, const TrackPanelGroup::Refinement::const_iterator iter)
int FilterEvent(wxEvent &event) override
static wxWeakRef< CellularPanel > spClickedPanel
static wxWeakRef< CellularPanel > spEnteredPanel
std::weak_ptr< TrackPanelCell > mLastCell
std::vector< UIHandlePtr > mTargets
std::weak_ptr< TrackPanelCell > mpClickedCell
virtual void EndGroup(const wxRect &rect, TrackPanelGroup &group)
virtual void BeginGroup(const wxRect &rect, TrackPanelGroup &group)
virtual void VisitCell(const wxRect &rect, TrackPanelCell &cell)
std::shared_ptr< TrackPanelCell > pCell
std::shared_ptr< TrackPanelCell > pCell
void EndGroup(const wxRect &rect, TrackPanelGroup &group) override
void VisitCell(const wxRect &rect, TrackPanelCell &cell) override
CellularPanel::SimpleCellVisitor SimpleCellVisitor
Adaptor(const SimpleCellVisitor &function_)
CellularPanel::SimpleNodeVisitor SimpleNodeVisitor
void BeginGroup(const wxRect &rect, TrackPanelGroup &group) override
Adaptor(const SimpleNodeVisitor &function_, bool pre_)