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{
503 DoContextMenu({});
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.get(), t.get(), 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 if (t) {
622 const unsigned refreshResult =
623 t->KeyDown(event, *mViewInfo, this, GetProject());
624 ProcessUIHandleResult(t.get(), t.get(), refreshResult);
625 }
626 else
627 event.Skip();
628}
629
630void CellularPanel::OnChar(wxKeyEvent & event)
631{
632 switch (event.GetKeyCode())
633 {
634 case WXK_ESCAPE:
635 case WXK_ALT:
636 case WXK_SHIFT:
637 case WXK_CONTROL:
638 case WXK_PAGEUP:
639 case WXK_PAGEDOWN:
640 return;
641 }
642
643 const auto t = GetFocusedCell();
644 if (t) {
645 const unsigned refreshResult =
646 t->Char(event, *mViewInfo, this, GetProject());
647 ProcessUIHandleResult(t.get(), t.get(), refreshResult);
648 }
649 else
650 event.Skip();
651}
652
653void CellularPanel::OnKeyUp(wxKeyEvent & event)
654{
655 bool didSomething = false;
656 switch (event.GetKeyCode())
657 {
658 case WXK_ESCAPE:
659 didSomething = HandleEscapeKey(false);
660 break;
661
662 case WXK_ALT:
663 case WXK_SHIFT:
664 case WXK_CONTROL:
665#ifdef __WXOSX__
666 case WXK_RAW_CONTROL:
667#endif
669 break;
670 }
671
672 if (didSomething)
673 return;
674
675 const auto t = GetFocusedCell();
676 if (t) {
677 const unsigned refreshResult =
678 t->KeyUp(event, *mViewInfo, this, GetProject());
679 ProcessUIHandleResult(t.get(), t.get(), refreshResult);
680 return;
681 }
682
683 event.Skip();
684}
685
687void CellularPanel::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event))
688{
689 auto &state = *mState;
690 state.mUIHandle.reset();
691 Leave();
692
693 // This is bad. We are lying abou the event by saying it is a mouse up.
694 wxMouseEvent e(wxEVT_LEFT_UP);
695 e.SetId( kCaptureLostEventId );
696
697 e.m_x = state.mMouseMostRecentX;
698 e.m_y = state.mMouseMostRecentY;
699
700 OnMouseEvent(e);
701}
702
706void CellularPanel::OnMouseEvent(wxMouseEvent & event)
707try
708{
709 const auto foundCell = FindCell( event.m_x, event.m_y );
710 auto &rect = foundCell.rect;
711 auto &pCell = foundCell.pCell;
712
713 const auto size = GetSize();
714 TrackPanelMouseEvent tpmEvent{ event, rect, size, pCell };
715
716#if defined(__WXMAC__) && defined(EVT_MAGNIFY)
717 // PRL:
718 // Pinch and spread implemented in wxWidgets 3.1.0, or cherry-picked from
719 // the future in custom build of 3.0.2
720 if (event.Magnify()) {
721 HandleWheelRotation( tpmEvent );
722 }
723#endif
724
725 // If a mouse event originates from a keyboard context menu event then
726 // event.GetPosition() == wxDefaultPosition. wxContextMenu events are handled in
727 // CellularPanel::OnContextMenu(), and therefore associated mouse events are ignored here.
728 // Not ignoring them was causing bug 613: the mouse events were interpreted as clicking
729 // outside the tracks.
730 if (event.GetPosition() == wxDefaultPosition && (event.RightDown() || event.RightUp())) {
731 event.Skip();
732 return;
733 }
734
735 if (event.m_wheelRotation != 0)
736 HandleWheelRotation( tpmEvent );
737
738 if (event.LeftDown() || event.LeftIsDown() || event.Moving()) {
739 // Skip, even if we do something, so that the left click or drag
740 // may have an additional effect in the scrubber.
741 event.Skip();
742 event.ResumePropagation(wxEVENT_PROPAGATE_MAX);
743 }
744
745 auto &state = *mState;
746 state.mMouseMostRecentX = event.m_x;
747 state.mMouseMostRecentY = event.m_y;
748
749 if (event.LeftDown()) {
750 // The activate event is used to make the
751 // parent window 'come alive' if it didn't have focus.
752 wxActivateEvent e;
753 GetParent()->GetEventHandler()->ProcessEvent(e);
754 }
755
756 if (event.Entering())
757 {
759 }
760 else if (event.Leaving())
761 {
762 if (Filter::spEnteredPanel == this)
763 Filter::spEnteredPanel = nullptr;
764 Leave();
765
766 auto buttons =
767 // Bug 1325: button state in Leaving events is unreliable on Mac.
768 // Poll the global state instead.
769 // event.ButtonIsDown(wxMOUSE_BTN_ANY);
770 ::wxGetMouseState().ButtonIsDown(wxMOUSE_BTN_ANY);
771
772 if (!buttons) {
773 CancelDragging( false );
774
775#if defined(__WXMAC__)
776
777 // We must install the cursor ourselves since the window under
778 // the mouse is no longer this one and wx2.8.12 makes that check.
779 // Should re-evaluate with wx3.
780 wxSTANDARD_CURSOR->MacInstall();
781#endif
782 }
783 }
784
785 if (state.mUIHandle) {
786 auto pClickedCell = state.mpClickedCell.lock();
787 if (event.Dragging()) {
788 // UIHANDLE DRAG
789 // copy shared_ptr for safety, as in HandleClick
790 auto handle = state.mUIHandle;
791 const UIHandle::Result refreshResult =
792 handle->Drag( tpmEvent, GetProject() );
794 (pClickedCell.get(), pCell.get(), refreshResult);
795 state.mMouseOverUpdateFlags |= refreshResult;
796 if (refreshResult & RefreshCode::Cancelled) {
797 // Drag decided to abort itself
798 state.mUIHandle.reset(), handle.reset(), ClearTargets();
799 state.mpClickedCell.reset();
800 Uncapture( false, &event );
801 }
802 else {
803 UpdateMouseState(event);
804 TrackPanelMouseState tpmState{ mLastMouseState, rect, pCell };
805 HandleMotion( tpmState );
806 }
807 }
808 else if (event.ButtonUp()) {
809 // UIHANDLE RELEASE
810 // copy shared_ptr for safety, as in HandleClick
811 auto handle = state.mUIHandle;
812 unsigned moreFlags = state.mMouseOverUpdateFlags;
813 UIHandle::Result refreshResult =
814 handle->Release( tpmEvent, GetProject(), this );
816 (pClickedCell.get(), pCell.get(),
817 refreshResult | moreFlags);
818 state.mUIHandle.reset(), handle.reset(), ClearTargets();
819 state.mpClickedCell.reset();
820 // will also Uncapture() below
821 }
822 }
823 else if ( event.GetEventType() == wxEVT_MOTION )
824 // Update status message and cursor, not during drag
825 // consider it not a drag, even if button is down during motion, if
826 // mUIHandle is null, as it becomes during interrupted drag
827 // (e.g. by hitting space to play while dragging an envelope point)
828 HandleMotion( event );
829 else if ( event.ButtonDown() || event.ButtonDClick() )
830 HandleClick( tpmEvent );
831
832 if (event.ButtonDown() && IsMouseCaptured()) {
833 if (!HasCapture())
834 CaptureMouse();
835 }
836
837 if (event.ButtonUp())
838 Uncapture( false );
839}
840catch( ... )
841{
842 // Abort any dragging, as if by hitting Esc
843 if ( CancelDragging( true ) )
844 ;
845 else {
846 Uncapture( true );
847 Refresh(false);
848 }
849 throw;
850}
851
852namespace {
853
855public:
857 const std::shared_ptr<TrackPanelCell> &pCell )
858 : mwCell{ pCell }
859 {}
860
862
863 std::shared_ptr<const Track> FindTrack() const override
864 { return nullptr; }
865
866 virtual Result Click
867 (const TrackPanelMouseEvent &event, AudacityProject *pProject) override
868 {
870 }
871
872 virtual Result Drag
873 (const TrackPanelMouseEvent &event, AudacityProject *pProject) override
874 {
876 }
877
879 (const TrackPanelMouseState &state, AudacityProject *pProject) override
880 {
881 return {};
882 }
883
885 (const TrackPanelMouseEvent &event, AudacityProject *pProject,
886 wxWindow *pParent) override
887 {
888 if (auto pCell = mwCell.lock()) {
889 auto point = event.event.GetPosition();
890 return pCell->DoContextMenu(event.rect, pParent, &point, pProject);
891 }
893 }
894
895 virtual Result Cancel(AudacityProject *pProject) override
896 {
898 }
899
900private:
901 std::weak_ptr<TrackPanelCell> mwCell;
902};
903
904DefaultRightButtonHandler::~DefaultRightButtonHandler()
905{
906}
907
908}
909
911{
912 auto pCell = tpmEvent.pCell;
913 // Do hit test once more, in case the button really pressed was not the
914 // one "anticipated."
915 {
916 TrackPanelMouseState tpmState{
917 tpmEvent.event,
918 tpmEvent.rect,
919 tpmEvent.pCell
920 };
921 HandleMotion( tpmState );
922 }
923
924 auto &state = *mState;
925 state.mUIHandle = Target();
926 if (tpmEvent.event.RightDown() &&
927 !(state.mUIHandle && state.mUIHandle->HandlesRightClick())) {
928 if (auto pCell = state.mLastCell.lock())
929 state.mUIHandle = std::make_shared<DefaultRightButtonHandler>(pCell);
930 }
931
932 if (state.mUIHandle) {
933 // UIHANDLE CLICK
934 // Make another shared pointer to the handle, in case recursive
935 // event dispatching otherwise tries to delete the handle.
936 auto handle = state.mUIHandle;
937 UIHandle::Result refreshResult =
938 handle->Click( tpmEvent, GetProject() );
939 if (refreshResult & RefreshCode::Cancelled)
940 state.mUIHandle.reset(), handle.reset(), ClearTargets();
941 else {
943
944#if wxUSE_TOOLTIPS
945 // Remove any outstanding tooltip
946 UnsetToolTip();
947#endif
948
949 if( !HasFocus() && AcceptsFocus() )
950 SetFocusIgnoringChildren();
951
952 state.mpClickedCell = pCell;
953
954 // Perhaps the clicked handle wants to update cursor and state message
955 // after a click.
956 TrackPanelMouseState tpmState{
957 tpmEvent.event,
958 tpmEvent.rect,
959 tpmEvent.pCell
960 };
961 HandleMotion( tpmState );
962 }
964 pCell.get(), pCell.get(), refreshResult);
965 state.mMouseOverUpdateFlags |= refreshResult;
966 }
967}
968
969void CellularPanel::DoContextMenu( std::shared_ptr<TrackPanelCell> pCell )
970{
971 if( !pCell ) {
972 pCell = GetFocusedCell();
973 if( !pCell )
974 return;
975 }
976
977 std::weak_ptr<TrackPanelCell> wCell = pCell;
978
979 const auto delegate = pCell->ContextMenuDelegate();
980 if (!delegate)
981 return;
982
983 auto rect = FindRect( *delegate );
984 const UIHandle::Result refreshResult =
985 delegate->DoContextMenu(rect, this, nullptr, GetProject());
986 pCell.reset();
987
988 ProcessUIHandleResult(wCell.lock().get(), wCell.lock().get(), refreshResult);
989}
990
991void CellularPanel::OnSetFocus(wxFocusEvent &event)
992{
994 Refresh( false);
995}
996
998{
999 if (auto pCell = GetFocusedCell()) {
1000 auto refreshResult = pCell->LoseFocus(GetProject());
1001 auto &state = *mState;
1002 auto pClickedCell = state.mpClickedCell.lock();
1003 if (pClickedCell)
1004 ProcessUIHandleResult( pClickedCell.get(), {}, refreshResult );
1005 }
1006 Refresh( false);
1007}
1008
1009void CellularPanel::OnKillFocus(wxFocusEvent & WXUNUSED(event))
1010{
1011 DoKillFocus();
1013 {
1015 }
1016}
1017
1018// Empty out-of-line default functions to fill Visitor's vtable
1021 const wxRect &rect, TrackPanelCell &cell ) {}
1023 const wxRect &rect, TrackPanelGroup &group ) {}
1025 const wxRect &rect, TrackPanelGroup &group ) {}
1026
1027// Public, top-level entry for generalized Visit
1029{
1030 Visit( GetClientRect(), Root(), visitor );
1031}
1032
1033// Common utility class for the functions that follow
1034namespace {
1038
1039 // Visit cells only
1040 Adaptor( const SimpleCellVisitor& function_ )
1041 : function{ [&](const wxRect &rect, TrackPanelNode &cell) {
1042 return function_( rect, static_cast<TrackPanelCell&>(cell) );
1043 } }
1044 {}
1045
1046 // Visit cells and groups, each once only, choosing pre- or post- ordering
1047 // for the groups
1048 Adaptor( const SimpleNodeVisitor &function_, bool pre_ )
1049 : function{ function_ }, pre{ pre_ }, post{ ! pre_ } {}
1050
1051 void VisitCell( const wxRect &rect, TrackPanelCell &cell ) override
1052 { return function( rect, cell ); }
1053 void BeginGroup( const wxRect &rect, TrackPanelGroup &group ) override
1054 { if (pre) return function( rect, group ); }
1055 void EndGroup( const wxRect &rect, TrackPanelGroup &group ) override
1056 { if (post) return function( rect, group ); }
1057
1059 const bool pre{ false }, post{ false };
1060 };
1061}
1062
1063// Simplified entry points for visits that don't need all the generality of
1064// CellularPanel::Visitor
1066{
1067 Adaptor adaptor{ visitor };
1068 Visit( adaptor );
1069}
1070
1072{
1073 Adaptor adaptor{ visitor, true };
1074 Visit( adaptor );
1075}
1076
1078{
1079 Adaptor adaptor{ visitor, false };
1080 Visit( adaptor );
1081}
1082
1083namespace {
1085 const wxRect &rect, bool divideX,
1086 const TrackPanelGroup::Refinement &children,
1087 const TrackPanelGroup::Refinement::const_iterator iter)
1088 {
1089 const auto next = iter + 1;
1090 const auto end = children.end();
1091 wxCoord nextCoord;
1092 if (next == end)
1093 nextCoord = std::max( iter->first,
1094 divideX ? rect.GetRight() : rect.GetBottom() );
1095 else
1096 nextCoord = next->first - 1;
1097
1098 auto lesser = iter->first;
1099 auto greater = nextCoord;
1100
1101 auto result = rect;
1102 if (divideX)
1103 result.SetLeft(lesser), result.SetRight(greater);
1104 else
1105 result.SetTop(lesser), result.SetBottom(greater);
1106
1107 return result;
1108 };
1109}
1110
1111// Private, recursive implementation function of Visit
1113 const wxRect &rect, const std::shared_ptr<TrackPanelNode> &node,
1114 Visitor &visitor )
1115{
1116 if (auto pCell = dynamic_cast<TrackPanelCell*>(node.get()))
1117 visitor.VisitCell( rect, *pCell );
1118 else if (auto pGroup = dynamic_cast<TrackPanelGroup*>(node.get())) {
1119 visitor.BeginGroup( rect, *pGroup );
1120
1121 // Recur on children
1122 const auto results = pGroup->Children( rect );
1123 const bool divideX = results.first == TrackPanelGroup::Axis::X;
1124 const auto &children = results.second;
1125 const auto begin = children.begin(), end = children.end();
1126 for (auto iter = begin; iter != end; ++iter)
1127 Visit(
1128 Subdivide(rect, divideX, children, iter), iter->second, visitor );
1129
1130 visitor.EndGroup( rect, *pGroup );
1131 }
1132 else
1133 return;
1134}
1135
1136auto CellularPanel::FindCell(int mouseX, int mouseY) -> FoundCell
1137{
1138 auto rect = this->GetClientRect();
1139 auto node = Root();
1140 while (node) {
1141 if ( auto pCell = std::dynamic_pointer_cast< TrackPanelCell >( node ) )
1142 // Found the bottom of the hierarchy
1143 return { pCell, rect };
1144 else if ( auto pGroup = dynamic_cast< TrackPanelGroup* >( node.get() ) ) {
1145 // Ask node for its subdivision
1146 const auto results = pGroup->Children( rect );
1147 const bool divideX = results.first == TrackPanelGroup::Axis::X;
1148 const auto &children = results.second;
1149
1150 // Find the correct child
1151 const auto begin = children.begin(), end = children.end();
1152 auto iter = std::upper_bound( begin, end,
1153 (divideX ? mouseX : mouseY),
1154 [&]( wxCoord coord, const TrackPanelGroup::Child &child ) {
1155 return coord < child.first;
1156 }
1157 );
1158 if (iter == begin)
1159 break;
1160 --iter;
1161
1162 // Descend the hierarchy of nodes
1163 rect = Subdivide(rect, divideX, children, iter);
1164 node = iter->second;
1165 }
1166 else
1167 // Nulls in the array of children are allowed, to define a void with
1168 // no cell
1169 break;
1170 }
1171
1172 return { {}, {} };
1173}
1174
1176{
1177 wxRect result;
1178
1179 struct Stop{};
1180 try { VisitCells( [&]( const wxRect &rect, TrackPanelCell &visited ) {
1181 if ( &visited == &cell )
1182 result = rect, throw Stop{};
1183 } ); }
1184 catch ( const Stop& ) {}
1185
1186 return result;
1187}
1188
1190 const std::function< bool( TrackPanelNode& ) > &pred)
1191{
1192 wxRect result;
1193
1194 struct Stop{};
1195 try { VisitPreorder( [&]( const wxRect &rect, TrackPanelNode &visited ) {
1196 if ( pred( visited ) )
1197 result = rect, throw Stop{};
1198 } ); }
1199 catch ( const Stop& ) {}
1200
1201 return result;
1202}
1203
1205{
1206 auto &state = *mState;
1207 if (state.mTargets.size())
1208 return state.mTargets[state.mTarget];
1209 else
1210 return {};
1211}
1212
1214{
1215 auto &state = *mState;
1216 return state.mMouseMostRecentX;
1217}
1218
1220{
1221 auto &state = *mState;
1222 // Forget the rotation of hit test candidates when the mouse moves from
1223 // cell to cell or outside of the panel entirely.
1224 state.mLastCell.reset();
1225 state.mTargets.clear();
1226 state.mTarget = 0;
1227 state.mMouseOverUpdateFlags = 0;
1228}
1229
1230std::shared_ptr<TrackPanelCell> CellularPanel::LastCell() const
1231{
1232 auto &state = *mState;
1233 return state.mLastCell.lock();
1234}
1235
1236void CellularPanel::Draw( TrackPanelDrawingContext &context, unsigned nPasses )
1237{
1238 const auto panelRect = GetClientRect();
1239 auto lastCell = LastCell();
1240 for ( unsigned iPass = 0; iPass < nPasses; ++iPass ) {
1241
1242 VisitPostorder( [&]( const wxRect &rect, TrackPanelNode &node ) {
1243
1244 // Draw the node
1245 const auto newRect = node.DrawingArea(
1246 context, rect, panelRect, iPass );
1247 if ( newRect.Intersects( panelRect ) )
1248 node.Draw( context, newRect, iPass );
1249
1250 // Draw the current handle if it is associated with the node
1251 if ( &node == lastCell.get() ) {
1252 auto target = Target();
1253 if ( target ) {
1254 const auto targetRect =
1255 target->DrawingArea( context, rect, panelRect, iPass );
1256 if ( targetRect.Intersects( panelRect ) )
1257 target->Draw( context, targetRect, iPass );
1258 }
1259 }
1260
1261 } ); // nodes
1262
1263 } // passes
1264}
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 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)
virtual std::shared_ptr< TrackPanelCell > GetFocusedCell()=0
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
void DoContextMenu(std::shared_ptr< TrackPanelCell > pCell)
bool ChangeTarget(bool forward, bool cycle)
void Uncapture(bool escaping, wxMouseState *pState=nullptr)
void Visit(Visitor &visitor)
wxCoord MostRecentXCoord() const
virtual void ProcessUIHandleResult(TrackPanelCell *pClickedCell, TrackPanelCell *pLatestCell, unsigned refreshResult)=0
void HandleCursorForPresentMouseState(bool doHit=true)
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:37
unsigned Result
Definition: UIHandle.h:40
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)
std::shared_ptr< const Track > FindTrack() const override
virtual HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
virtual Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
void SetToolTip(const TranslatableString &toolTip)
bool IsHandler(const wxWindow *handler)
void Release(wxWindow *handler)
wxRect Subdivide(const wxRect &rect, bool divideX, const TrackPanelGroup::Refinement &children, const TrackPanelGroup::Refinement::const_iterator iter)
const char * end(const char *str) noexcept
Definition: StringUtils.h:106
const char * begin(const char *str) noexcept
Definition: StringUtils.h:101
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_)