diff --git a/src/HitTestResult.h b/src/HitTestResult.h index 7ddf15569..295b8bb55 100644 --- a/src/HitTestResult.h +++ b/src/HitTestResult.h @@ -24,17 +24,12 @@ struct HitTestPreview HitTestPreview() {} - HitTestPreview(const wxString &message_, wxCursor *cursor_ - , unsigned refreshCode_ = 0) - : message(message_), cursor(cursor_), refreshCode(refreshCode_) + HitTestPreview(const wxString &message_, wxCursor *cursor_) + : message(message_), cursor(cursor_) {} wxString message {}; wxCursor *cursor {}; - - // Making this non-zero allows mouse-over highlighting - // See RefreshCode.h for bit flags: - unsigned refreshCode {}; }; struct HitTestResult diff --git a/src/LabelTrack.cpp b/src/LabelTrack.cpp index 7cb560623..613f513e4 100644 --- a/src/LabelTrack.cpp +++ b/src/LabelTrack.cpp @@ -102,14 +102,9 @@ LabelTrack::Holder TrackFactory::NewLabelTrack() LabelTrack::LabelTrack(const std::shared_ptr &projDirManager): Track(projDirManager), - mbHitCenter(false), - mOldEdge(-1), mSelIndex(-1), - mMouseOverLabelLeft(-1), - mMouseOverLabelRight(-1), mRestoreFocus(-1), mClipLen(0.0), - mIsAdjustingLabel(false), miLastLabel(-1) { SetDefaultName(_("Label Track")); @@ -129,13 +124,8 @@ LabelTrack::LabelTrack(const std::shared_ptr &projDirManager): LabelTrack::LabelTrack(const LabelTrack &orig) : Track(orig), - mbHitCenter(false), - mOldEdge(-1), mSelIndex(-1), - mMouseOverLabelLeft(-1), - mMouseOverLabelRight(-1), - mClipLen(0.0), - mIsAdjustingLabel(false) + mClipLen(0.0) { for (auto &original: orig.mLabels) { LabelStruct l { original.selectedRegion, original.title }; @@ -766,6 +756,23 @@ void LabelTrack::CalcHighlightXs(int *x1, int *x2) const labelStruct.getXPos(dc, x2, pos2); } +#include "tracks/labeltrack/ui/LabelGlyphHandle.h" +// TODO: don't rely on the global ::GetActiveProject() to find this. +// Rather, give TrackPanelCell a drawing function and pass context into it. +namespace { + LabelTrackHit *findHit() + { + // Fetch the highlighting state + auto target = GetActiveProject()->GetTrackPanel()->Target(); + if (target) { + auto handle = dynamic_cast( target->handle.get() ); + if (handle) + return &handle->mHit; + } + return nullptr; + } +} + /// Draw calls other functions to draw the LabelTrack. /// @param dc the device context /// @param r the LabelTrack rectangle. @@ -773,6 +780,8 @@ void LabelTrack::Draw(wxDC & dc, const wxRect & r, const SelectedRegion &selectedRegion, const ZoomInfo &zoomInfo) const { + auto pHit = findHit(); + if(msFont.Ok()) dc.SetFont(msFont); @@ -819,10 +828,10 @@ void LabelTrack::Draw(wxDC & dc, const wxRect & r, { int i = -1; for (auto &labelStruct : mLabels) { ++i; GlyphLeft=0; GlyphRight=1; - if( i==mMouseOverLabelLeft ) - GlyphLeft = mbHitCenter ? 6:9; - if( i==mMouseOverLabelRight ) - GlyphRight = mbHitCenter ? 7:4; + if( pHit && i == pHit->mMouseOverLabelLeft ) + GlyphLeft = (pHit->mEdge & 4) ? 6:9; + if( pHit && i == pHit->mMouseOverLabelRight ) + GlyphRight = (pHit->mEdge & 4) ? 7:4; labelStruct.DrawGlyphs( dc, r, GlyphLeft, GlyphRight ); }} @@ -1119,20 +1128,10 @@ void LabelTrack::SetSelected(bool s) Unselect(); } -/// OverGlyph returns 0 if not over a glyph, -/// 1 if over the left-hand glyph, and -/// 2 if over the right-hand glyph on a label. -/// 3 if over both right and left. -/// -/// It also sets up member variables: -/// mMouseLabelLeft - index of any left label hit -/// mMouseLabelRight - index of any right label hit -/// mbHitCenter - if (x,y) 'hits the spot'. -/// /// TODO: Investigate what happens with large /// numbers of labels, might need a binary search /// rather than a linear one. -int LabelTrack::OverGlyph(int x, int y) +void LabelTrack::OverGlyph(LabelTrackHit &hit, int x, int y) const { //Determine the NEW selection. int result=0; @@ -1140,9 +1139,9 @@ int LabelTrack::OverGlyph(int x, int y) const int d2=5; //distance in pixels, used for have we hit drag handle center. //If not over a label, reset it - mMouseOverLabelLeft = -1; - mMouseOverLabelRight = -1; - mbHitCenter = false; + hit.mMouseOverLabelLeft = -1; + hit.mMouseOverLabelRight = -1; + hit.mEdge = 0; { int i = -1; for (auto &labelStruct : mLabels) { ++i; //over left or right selection bound //Check right bound first, since it is drawn after left bound, @@ -1150,16 +1149,16 @@ int LabelTrack::OverGlyph(int x, int y) if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 && abs(labelStruct.x1 - d2 -x) < d1) { - mMouseOverLabelRight = i; + hit.mMouseOverLabelRight = i; if(abs(labelStruct.x1 - x) < d2 ) { - mbHitCenter = true; + result |= 4; // If left and right co-incident at this resolution, then we drag both. // We could be a little less stringent about co-incidence here if we liked. if( abs(labelStruct.x1-labelStruct.x) < 1.0 ) { result |=1; - mMouseOverLabelLeft = i; + hit.mMouseOverLabelLeft = i; } } result |= 2; @@ -1169,9 +1168,9 @@ int LabelTrack::OverGlyph(int x, int y) else if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 && abs(labelStruct.x + d2 - x) < d1 ) { - mMouseOverLabelLeft = i; + hit.mMouseOverLabelLeft = i; if(abs(labelStruct.x - x) < d2 ) - mbHitCenter = true; + result |= 4; result |= 1; } @@ -1182,7 +1181,7 @@ int LabelTrack::OverGlyph(int x, int y) } }} - return result; + hit.mEdge = result; } int LabelTrack::OverATextBox(int xx, int yy) const @@ -1403,7 +1402,8 @@ auto LabelStruct::RegionRelation( /// @iEdge - which edge is requested to move, -1 for left +1 for right. /// @bAllowSwapping - if we can switch which edge is being dragged. /// fNewTime - the NEW time for this edge of the label. -void LabelTrack::MayAdjustLabel( int iLabel, int iEdge, bool bAllowSwapping, double fNewTime) +void LabelTrack::MayAdjustLabel +( LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime) { if( iLabel < 0 ) return; @@ -1424,9 +1424,7 @@ void LabelTrack::MayAdjustLabel( int iLabel, int iEdge, bool bAllowSwapping, dou } // Swap our record of what we are dragging. - int Temp = mMouseOverLabelLeft; - mMouseOverLabelLeft = mMouseOverLabelRight; - mMouseOverLabelRight = Temp; + std::swap( hit.mMouseOverLabelLeft, hit.mMouseOverLabelRight ); } // If the index is for a real label, adjust its left and right boundary. @@ -1450,27 +1448,28 @@ static int Constrain( int value, int min, int max ) return result; } -bool LabelTrack::HandleGlyphDragRelease(const wxMouseEvent & evt, - wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel) +bool LabelTrack::HandleGlyphDragRelease +(LabelTrackHit &hit, const wxMouseEvent & evt, + wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel) { if(evt.LeftUp()) { bool lupd = false, rupd = false; - if(mMouseOverLabelLeft>=0) { - auto &labelStruct = mLabels[mMouseOverLabelLeft]; + if( hit.mMouseOverLabelLeft >= 0 ) { + auto &labelStruct = mLabels[ hit.mMouseOverLabelLeft ]; lupd = labelStruct.updated; labelStruct.updated = false; } - if(mMouseOverLabelRight>=0) { - auto &labelStruct = mLabels[mMouseOverLabelRight]; + if( hit.mMouseOverLabelRight >= 0 ) { + auto &labelStruct = mLabels[ hit.mMouseOverLabelRight ]; rupd = labelStruct.updated; labelStruct.updated = false; } - mIsAdjustingLabel = false; - mMouseOverLabelLeft = -1; - mMouseOverLabelRight = -1; + hit.mIsAdjustingLabel = false; + hit.mMouseOverLabelLeft = -1; + hit.mMouseOverLabelRight = -1; return lupd || rupd; } @@ -1483,23 +1482,25 @@ bool LabelTrack::HandleGlyphDragRelease(const wxMouseEvent & evt, int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width); // If exactly one edge is selected we allow swapping - bool bAllowSwapping = (mMouseOverLabelLeft >=0 ) ^ ( mMouseOverLabelRight >= 0); + bool bAllowSwapping = + ( hit.mMouseOverLabelLeft >=0 ) != + ( hit.mMouseOverLabelRight >= 0); // If we're on the 'dot' and nowe're moving, // Though shift-down inverts that. // and if both edges the same, then we're always moving the label. - bool bLabelMoving = mbIsMoving; + bool bLabelMoving = hit.mbIsMoving; bLabelMoving ^= evt.ShiftDown(); - bLabelMoving |= mMouseOverLabelLeft==mMouseOverLabelRight; + bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); double fNewX = zoomInfo.PositionToTime(x, 0); if( bLabelMoving ) { - MayMoveLabel( mMouseOverLabelLeft, -1, fNewX ); - MayMoveLabel( mMouseOverLabelRight, +1, fNewX ); + MayMoveLabel( hit.mMouseOverLabelLeft, -1, fNewX ); + MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX ); } else { - MayAdjustLabel( mMouseOverLabelLeft, -1, bAllowSwapping, fNewX ); - MayAdjustLabel( mMouseOverLabelRight, +1, bAllowSwapping, fNewX ); + MayAdjustLabel( hit, hit.mMouseOverLabelLeft, -1, bAllowSwapping, fNewX ); + MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX ); } if( mSelIndex >=0 ) @@ -1508,7 +1509,7 @@ bool LabelTrack::HandleGlyphDragRelease(const wxMouseEvent & evt, //the NEW size of the label. *newSel = mLabels[mSelIndex].selectedRegion; } - SortLabels(); + SortLabels( &hit ); } return false; @@ -1556,23 +1557,24 @@ void LabelTrack::HandleTextDragRelease(const wxMouseEvent & evt) return; } -void LabelTrack::HandleGlyphClick(const wxMouseEvent & evt, - const wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel) +void LabelTrack::HandleGlyphClick +(LabelTrackHit &hit, const wxMouseEvent & evt, + const wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel) { if (evt.ButtonDown()) { //OverGlyph sets mMouseOverLabel to be the chosen label. - int iGlyph = OverGlyph(evt.m_x, evt.m_y); - mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) && - iGlyph != 0; + OverGlyph(hit, evt.m_x, evt.m_y); + hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) && + ( hit.mEdge & 3 ) != 0; - if (mIsAdjustingLabel) + if (hit.mIsAdjustingLabel) { float t = 0.0; // We move if we hit the centre, we adjust one edge if we hit a chevron. // This is if we are moving just one edge. - mbIsMoving = mbHitCenter; + hit.mbIsMoving = hit.mEdge & 4; // When we start dragging the label(s) we don't want them to jump. // so we calculate the displacement of the mouse from the drag center // and use that in subsequent dragging calculations. The mouse stays @@ -1585,27 +1587,27 @@ void LabelTrack::HandleGlyphClick(const wxMouseEvent & evt, // position when we start dragging. // Dragging of three label edges at the same time is not supported (yet). - if( (mMouseOverLabelRight >=0) && - (mMouseOverLabelLeft >=0) + if( ( hit.mMouseOverLabelRight >= 0 ) && + ( hit.mMouseOverLabelLeft >= 0 ) ) { - t = (mLabels[mMouseOverLabelRight].getT1() + - mLabels[mMouseOverLabelLeft].getT0()) / 2.0f; + t = (mLabels[ hit.mMouseOverLabelRight ].getT1() + + mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f; // If we're moving two edges, then it's a move (label size preserved) // if both edges are the same label, and it's an adjust (label sizes change) // if we're on a boundary between two different labels. - mbIsMoving = (mMouseOverLabelLeft == mMouseOverLabelRight); + hit.mbIsMoving = + ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight ); } - else if(mMouseOverLabelRight >=0) + else if( hit.mMouseOverLabelRight >=0) { - t = mLabels[mMouseOverLabelRight].getT1(); + t = mLabels[ hit.mMouseOverLabelRight ].getT1(); } - else if(mMouseOverLabelLeft >=0) + else if( hit.mMouseOverLabelLeft >=0) { - t = mLabels[mMouseOverLabelLeft].getT0(); + t = mLabels[ hit.mMouseOverLabelLeft ].getT0(); } mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x; - return; } } } @@ -2879,7 +2881,7 @@ bool LabelTrack::IsGoodLabelEditKey(const wxKeyEvent & evt) /// This function is called often (whilst dragging a label) /// We expect them to be very nearly in order, so insertion /// sort (with a linear search) is a reasonable choice. -void LabelTrack::SortLabels() +void LabelTrack::SortLabels( LabelTrackHit *pHit ) { const auto begin = mLabels.begin(); const auto nn = (int)mLabels.size(); @@ -2914,8 +2916,10 @@ void LabelTrack::SortLabels() ++index; } }; - update(mMouseOverLabelLeft); - update(mMouseOverLabelRight); + if ( pHit ) { + update( pHit->mMouseOverLabelLeft ); + update( pHit->mMouseOverLabelRight ); + } update(mSelIndex); } } diff --git a/src/LabelTrack.h b/src/LabelTrack.h index 353b51b29..da4f3067f 100644 --- a/src/LabelTrack.h +++ b/src/LabelTrack.h @@ -39,6 +39,8 @@ class TimeWarper; class ZoomInfo; +struct LabelTrackHit; + class LabelStruct { public: @@ -150,7 +152,6 @@ class AUDACITY_DLL_API LabelTrack final : public Track const ZoomInfo &zoomInfo) const; int getSelectedIndex() const { return mSelIndex; } - bool IsAdjustingLabel() const { return mIsAdjustingLabel; } int GetKind() const override { return Label; } @@ -180,7 +181,7 @@ class AUDACITY_DLL_API LabelTrack final : public Track void Silence(double t0, double t1) override; void InsertSilence(double t, double len) override; - int OverGlyph(int x, int y); + void OverGlyph(LabelTrackHit &hit, int x, int y) const; static wxBitmap & GetGlyph( int i); @@ -206,13 +207,16 @@ class AUDACITY_DLL_API LabelTrack final : public Track static bool IsTextClipSupported(); void HandleGlyphClick - (const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, + (LabelTrackHit &hit, + const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, SelectedRegion *newSel); void HandleTextClick (const wxMouseEvent & evt, const wxRect & r, const ZoomInfo &zoomInfo, SelectedRegion *newSel); - bool HandleGlyphDragRelease(const wxMouseEvent & evt, wxRect & r, const ZoomInfo &zoomInfo, - SelectedRegion *newSel); + bool HandleGlyphDragRelease + (LabelTrackHit &hit, + const wxMouseEvent & evt, wxRect & r, const ZoomInfo &zoomInfo, + SelectedRegion *newSel); void HandleTextDragRelease(const wxMouseEvent & evt); bool OnKeyDown(SelectedRegion &sel, wxKeyEvent & event); @@ -243,7 +247,9 @@ class AUDACITY_DLL_API LabelTrack final : public Track void CalcHighlightXs(int *x1, int *x2) const; - void MayAdjustLabel( int iLabel, int iEdge, bool bAllowSwapping, double fNewTime); + void MayAdjustLabel + ( LabelTrackHit &hit, + int iLabel, int iEdge, bool bAllowSwapping, double fNewTime); void MayMoveLabel( int iLabel, int iEdge, double fNewTime); // This pastes labels without shifting existing ones @@ -266,19 +272,12 @@ class AUDACITY_DLL_API LabelTrack final : public Track int FindPrevLabel(const SelectedRegion& currentSelection); public: - void SortLabels(); - //These two are used by a TrackPanel KLUDGE, which is why they are public. - bool mbHitCenter; - //The edge variable tells us what state the icon is in. - //mOldEdge is useful for telling us when there has been a state change. - int mOldEdge; + void SortLabels(LabelTrackHit *pHit = nullptr); private: void ShowContextMenu(); void OnContextMenu(wxCommandEvent & evt); int mSelIndex; /// Keeps track of the currently selected label - int mMouseOverLabelLeft; /// Keeps track of which left label the mouse is currently over. - int mMouseOverLabelRight; /// Keeps track of which right label the mouse is currently over. int mxMouseDisplacement; /// Displacement of mouse cursor from the centre being dragged. LabelArray mLabels; @@ -314,9 +313,6 @@ private: void calculateFontHeight(wxDC & dc) const; void RemoveSelectedText(); - bool mIsAdjustingLabel; - bool mbIsMoving; - static wxFont msFont; std::weak_ptr mGlyphHandle; diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index cd48714bf..e3a20e393 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -758,7 +758,7 @@ void TrackPanel::Uncapture(wxMouseEvent *pEvent) { if (HasCapture()) ReleaseMouse(); - HandleCursor( pEvent ); + HandleMotion( pEvent ); } void TrackPanel::CancelDragging() @@ -768,9 +768,10 @@ void TrackPanel::CancelDragging() auto pTrack = GetTracks()->Lock(mpClickedTrack); if (pTrack) ProcessUIHandleResult( - this, mRuler, pTrack.get(), NULL, refreshResult); + this, mRuler, pTrack.get(), NULL, + refreshResult | mMouseOverUpdateFlags ); mpClickedTrack.reset(); - mUIHandle.reset(); + mUIHandle.reset(), ClearTargets(); Uncapture(); } } @@ -830,7 +831,7 @@ void TrackPanel::HandleCursorForLastMouseState() // Come here on modifier key or mouse button transitions, // or on starting or stopping of play or record, // and change the cursor appropriately. - HandleCursor( &mLastMouseState ); + HandleMotion( &mLastMouseState ); } bool TrackPanel::IsAudioActive() @@ -840,10 +841,14 @@ bool TrackPanel::IsAudioActive() } -/// TrackPanel::HandleCursor( ) sets the cursor drawn at the mouse location. +/// TrackPanel::HandleMotion( ) sets the cursor drawn at the mouse location, +/// and updates the status bar message. +/// We treat certain other changes of mouse button and key state as "motions" +/// too, and also starting and stopping of playback or recording, all of which +/// may cause the appropriate cursor and message to change. /// As this procedure checks which region the mouse is over, it is /// appropriate to establish the message in the status bar. -void TrackPanel::HandleCursor( wxMouseState *pState ) +void TrackPanel::HandleMotion( wxMouseState *pState ) { wxMouseState dummy; if (!pState) @@ -857,39 +862,78 @@ void TrackPanel::HandleCursor( wxMouseState *pState ) auto &rect = foundCell.rect; auto &pCell = foundCell.pCell; const TrackPanelMouseState tpmState{ state, rect, pCell }; - HandleCursor( tpmState ); + HandleMotion( tpmState ); } -void TrackPanel::HandleCursor( const TrackPanelMouseState &tpmState ) +void TrackPanel::HandleMotion( const TrackPanelMouseState &tpmState ) { - if ( mUIHandle ) { - // UIHANDLE PREVIEW - // Update status message and cursor during drag - HitTestPreview preview = mUIHandle->Preview( tpmState, GetProject() ); - mListener->TP_DisplayStatusMessage( preview.message ); - if ( preview.cursor ) - SetCursor( *preview.cursor ); - } - else { - wxCursor *pCursor = NULL; + HitTestResult result; + auto handle = mUIHandle; - wxString tip; + auto oldHandle = mLastHitTest.handle; + auto oldCell = mLastCell.lock(); + auto newCell = tpmState.pCell; - auto pCell = tpmState.pCell; - auto track = static_cast( pCell.get() )->FindTrack(); - if (pCell && pCursor == NULL && tip == wxString()) { - HitTestResult hitTest( pCell->HitTest(tpmState, GetProject()) ); - tip = hitTest.preview.message; - ProcessUIHandleResult - (this, mRuler, track.get(), track.get(), hitTest.preview.refreshCode); - pCursor = hitTest.preview.cursor; - if (pCursor) - SetCursor(*pCursor); + std::shared_ptr newTrack; + if ( newCell ) + newTrack = static_cast( newCell.get() )->FindTrack(); + + std::shared_ptr oldTrack; + if ( oldCell ) + oldTrack = static_cast( oldCell.get() )->FindTrack(); + + wxString tip{}; + wxCursor *pCursor{}; + unsigned refreshCode = 0; + + if ( !mUIHandle ) { + // Not yet dragging. + unsigned updateFlags = mMouseOverUpdateFlags; + + // First check whether crossing cell to cell + if ( newCell == oldCell ) + oldCell.reset(); + else { + // Forget old targets + ClearTargets(); + // Re-draw any highlighting + if (oldCell) { + ProcessUIHandleResult( + this, GetRuler(), oldTrack.get(), oldTrack.get(), updateFlags); + } } - - if (pCursor != NULL || tip != wxString()) - mListener->TP_DisplayStatusMessage(tip); + + // Now do the + // UIHANDLE HIT TEST ! + result = newCell->HitTest(tpmState, GetProject()); + handle = result.handle; + + mLastCell = newCell; + mLastHitTestValid = true; + mLastHitTest = result; + + if (!oldCell && oldHandle != handle) + // Did not move cell to cell, but did change the target + refreshCode = updateFlags; } + + // UIHANDLE PREVIEW + // Update status message and cursor, whether dragging or not + if (handle) { + auto preview = handle->Preview( tpmState, GetProject() ); + tip = preview.message; + pCursor = preview.cursor; + auto code = handle->GetChangeHighlight(); + handle->SetChangeHighlight(RefreshCode::RefreshNone); + refreshCode |= code; + mMouseOverUpdateFlags |= code; + } + mListener->TP_DisplayStatusMessage(tip); + if (pCursor) + SetCursor( *pCursor ); + + ProcessUIHandleResult( + this, GetRuler(), newTrack.get(), newTrack.get(), refreshCode); } void TrackPanel::UpdateSelectionDisplay() @@ -1238,7 +1282,8 @@ void TrackPanel::HandleWheelRotation( TrackPanelMouseEvent &tpmEvent ) unsigned result = pCell->HandleWheelRotation( tpmEvent, GetProject() ); auto pTrack = static_cast(pCell.get())->FindTrack(); - ProcessUIHandleResult(this, mRuler, pTrack.get(), pTrack.get(), result); + ProcessUIHandleResult( + this, mRuler, pTrack.get(), pTrack.get(), result); } /// Filter captured keys typed into LabelTracks. @@ -1361,6 +1406,8 @@ void TrackPanel::OnKeyUp(wxKeyEvent & event) /// Should handle the case when the mouse capture is lost. void TrackPanel::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event)) { + ClearTargets(); + wxMouseEvent e(wxEVT_LEFT_UP); e.m_x = mMouseMostRecentX; @@ -1436,6 +1483,9 @@ try if (event.Leaving()) { + if ( !mUIHandle ) + ClearTargets(); + auto buttons = // Bug 1325: button state in Leaving events is unreliable on Mac. // Poll the global state instead. @@ -1463,32 +1513,32 @@ try mUIHandle->Drag( tpmEvent, GetProject() ); ProcessUIHandleResult (this, mRuler, pClickedTrack.get(), pTrack.get(), refreshResult); + mMouseOverUpdateFlags |= refreshResult; if (refreshResult & RefreshCode::Cancelled) { // Drag decided to abort itself - mUIHandle.reset(); + mUIHandle.reset(), ClearTargets(); mpClickedTrack.reset(); Uncapture( &event ); } else { - TrackPanelMouseState tpmState{ - tpmEvent.event, - tpmEvent.rect, - tpmEvent.pCell - }; - HandleCursor( tpmState ); + UpdateMouseState(event); + TrackPanelMouseState tpmState{ mLastMouseState, rect, pCell }; + HandleMotion( tpmState ); } } else if (event.ButtonUp()) { // UIHANDLE RELEASE auto uiHandle = mUIHandle; - // Null this pointer out first before calling Release -- because on Windows, we can + // Null mUIHandle out first before calling Release -- because on Windows, we can // come back recursively to this place during handling of the context menu, // because of a capture lost event. - mUIHandle.reset(); + unsigned moreFlags = mMouseOverUpdateFlags; + mUIHandle.reset(), ClearTargets(); UIHandle::Result refreshResult = uiHandle->Release( tpmEvent, GetProject(), this ); ProcessUIHandleResult - (this, mRuler, pClickedTrack.get(), pTrack.get(), refreshResult); + (this, mRuler, pClickedTrack.get(), pTrack.get(), + refreshResult | moreFlags); mpClickedTrack.reset(); // will also Uncapture() below } @@ -1498,7 +1548,7 @@ try // consider it not a drag, even if button is down during motion, if // mUIHandle is null, as it becomes during interrupted drag // (e.g. by hitting space to play while dragging an envelope point) - HandleCursor( &event ); + HandleMotion( &event ); else if ( event.ButtonDown() || event.ButtonDClick() ) HandleClick( tpmEvent ); @@ -1538,32 +1588,44 @@ void TrackPanel::HandleClick( const TrackPanelMouseEvent &tpmEvent ) const auto &rect = tpmEvent.rect; auto pTrack = static_cast( pCell.get() )->FindTrack(); - if ( !mUIHandle && pCell ) { + // Do hit test once more, in case the button really pressed was not the + // one "anticipated." + { TrackPanelMouseState tpmState{ tpmEvent.event, tpmEvent.rect, tpmEvent.pCell }; - mUIHandle = - pCell->HitTest( tpmState, GetProject() ).handle; + HandleMotion( tpmState ); } + auto target = Target(); + if (target) + mUIHandle = target->handle; + else + mUIHandle = {}; + if (mUIHandle) { // UIHANDLE CLICK UIHandle::Result refreshResult = mUIHandle->Click( tpmEvent, GetProject() ); if (refreshResult & RefreshCode::Cancelled) - mUIHandle.reset(); - else + mUIHandle.reset(), ClearTargets(); + else { mpClickedTrack = pTrack; - ProcessUIHandleResult - (this, mRuler, pTrack.get(), pTrack.get(), refreshResult); - TrackPanelMouseState tpmState{ - tpmEvent.event, - tpmEvent.rect, - tpmEvent.pCell - }; - HandleCursor( tpmState ); + + // Perhaps the clicked handle wants to update cursor and state message + // after a click. + TrackPanelMouseState tpmState{ + tpmEvent.event, + tpmEvent.rect, + tpmEvent.pCell + }; + HandleMotion( tpmState ); + } + ProcessUIHandleResult( + this, mRuler, pTrack.get(), pTrack.get(), refreshResult); + mMouseOverUpdateFlags |= refreshResult; } } @@ -1574,6 +1636,9 @@ double TrackPanel::GetMostRecentXPos() void TrackPanel::RefreshTrack(Track *trk, bool refreshbacking) { + if (!trk) + return; + Track *link = trk->GetLink(); if (link && !trk->GetLinked()) { diff --git a/src/TrackPanel.h b/src/TrackPanel.h index 1259c204e..faf1ad724 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -18,6 +18,8 @@ #include "Experimental.h" +#include "HitTestResult.h" + #include "SelectedRegion.h" #include "widgets/OverlayPanel.h" @@ -373,8 +375,8 @@ protected: }; FoundCell FindCell(int mouseX, int mouseY); - void HandleCursor( wxMouseState *pState ); - void HandleCursor( const TrackPanelMouseState &tpmState ); + void HandleMotion( wxMouseState *pState ); + void HandleMotion( const TrackPanelMouseState &tpmState ); // If label, rectangle includes track control panel only. // If !label, rectangle includes all of that, and the vertical ruler, and @@ -528,6 +530,31 @@ protected: wxSize vrulerSize; protected: + std::weak_ptr mLastCell; + HitTestResult mLastHitTest{}; + bool mLastHitTestValid{}; + unsigned mMouseOverUpdateFlags{}; + + public: + HitTestResult *Target() + { + if ( mLastHitTestValid ) + return &mLastHitTest; + else + return nullptr; + } + + protected: + void ClearTargets() + { + // Forget the rotation of hit test candidates when the mouse moves from + // cell to cell or outside of the TrackPanel entirely. + mLastCell.reset(); + mLastHitTestValid = false; + mLastHitTest = {}; + mMouseOverUpdateFlags = 0; + } + std::weak_ptr mpClickedTrack; UIHandlePtr mUIHandle; diff --git a/src/UIHandle.h b/src/UIHandle.h index 2315d95bb..08f4f97b9 100644 --- a/src/UIHandle.h +++ b/src/UIHandle.h @@ -60,7 +60,9 @@ public: virtual Result Drag (const TrackPanelMouseEvent &event, AudacityProject *pProject) = 0; - // Update the cursor and status message. + // Can be called when the handle has been hit but not yet clicked, + // or called after Drag(). + // Specifies cursor and status bar message. virtual HitTestPreview Preview (const TrackPanelMouseState &state, const AudacityProject *pProject) = 0; @@ -99,11 +101,35 @@ public: // to avoid dangling pointers to tracks. But maybe there will be a future // use? virtual void OnProjectChange(AudacityProject *pProject); + +public: + Result GetChangeHighlight() const { return mChangeHighlight; } + void SetChangeHighlight(Result val) { mChangeHighlight = val; } + + // If AssignUIHandlePtr is used, then this function is also called before any + // overwrite. + // Make overloads of this for other subclasses, to cause refresh + // of the cell during mouse motion within it. + static UIHandle::Result NeedChangeHighlight + (const UIHandle &/*oldState*/, const UIHandle &/*newState*/) + { + return 0; + } + +protected: + // Derived classes can set this nonzero in a constructor, which is enough + // to cause repaint of the cell whenever the pointer hits the target, + // or leaves it without clicking, or releases or escapes from a drag. + Result mChangeHighlight { 0 }; + }; using UIHandlePtr = std::shared_ptr; -// A frequent convenience +// A frequent convenience for defining a hit test. +// Construct a NEW handle as if hit the first time; then either keep it, or +// use it to overwrite the state of a previously constructed handle that has not +// yet been released. template std::shared_ptr AssignUIHandlePtr ( std::weak_ptr &holder, const std::shared_ptr &pNew ) @@ -118,7 +144,9 @@ std::shared_ptr AssignUIHandlePtr return pNew; } else { + auto code = Subclass::NeedChangeHighlight( *ptr, *pNew ); *ptr = std::move(*pNew); + ptr->SetChangeHighlight( code ); return ptr; } } diff --git a/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp b/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp index 0a2956c84..214ffc00e 100644 --- a/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp +++ b/src/tracks/labeltrack/ui/LabelGlyphHandle.cpp @@ -24,23 +24,32 @@ Paul Licameli split from TrackPanel.cpp #include LabelGlyphHandle::LabelGlyphHandle -(const std::shared_ptr &pLT, const wxRect &rect) +(const std::shared_ptr &pLT, + const wxRect &rect, const LabelTrackHit &hit) : mpLT{ pLT } , mRect{ rect } -{} + , mHit{ hit } +{ + mChangeHighlight = RefreshCode::RefreshCell; +} -HitTestPreview LabelGlyphHandle::HitPreview -(bool hitCenter, unsigned refreshResult) +UIHandle::Result LabelGlyphHandle::NeedChangeHighlight +(const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState) +{ + if (oldState.mHit.mEdge != newState.mHit.mEdge) + // pointer moves between the circle and the chevron + return RefreshCode::RefreshCell; + return 0; +} + +HitTestPreview LabelGlyphHandle::HitPreview(bool hitCenter) { static wxCursor arrowCursor{ wxCURSOR_ARROW }; return { (hitCenter ? _("Drag one or more label boundaries.") : _("Drag label boundary.")), - &arrowCursor, - // Unusually, can have a non-zero third member of HitTestPreview, so that - // mouse-over highlights it. - refreshResult + &arrowCursor }; } @@ -49,39 +58,22 @@ HitTestResult LabelGlyphHandle::HitTest const wxMouseState &state, const std::shared_ptr &pLT, const wxRect &rect) { - using namespace RefreshCode; - unsigned refreshResult = RefreshNone; - - // Note: this has side effects on pLT! - int edge = pLT->OverGlyph(state.m_x, state.m_y); - - //KLUDGE: We refresh the whole Label track when the icon hovered over - //changes colouration. Inefficient. - edge += pLT->mbHitCenter ? 4 : 0; - if (edge != pLT->mOldEdge) - { - pLT->mOldEdge = edge; - refreshResult |= RefreshCell; - } + LabelTrackHit hit{}; + pLT->OverGlyph(hit, state.m_x, state.m_y); // IF edge!=0 THEN we've set the cursor and we're done. // signal this by setting the tip. - if (edge != 0) + if ( hit.mEdge & 3 ) { - auto result = std::make_shared( pLT, rect ); + auto result = std::make_shared( pLT, rect, hit ); result = AssignUIHandlePtr(holder, result); return { - HitPreview(pLT->mbHitCenter, refreshResult), + HitPreview( hit.mEdge & 4 ), result }; } - else { - // An empty result, except maybe, unusually, the refresh - return { - { wxString{}, nullptr, refreshResult }, - {} - }; - } + + return {}; } LabelGlyphHandle::~LabelGlyphHandle() @@ -96,9 +88,10 @@ UIHandle::Result LabelGlyphHandle::Click const wxMouseEvent &event = evt.event; ViewInfo &viewInfo = pProject->GetViewInfo(); - mpLT->HandleGlyphClick(event, mRect, viewInfo, &viewInfo.selectedRegion); + mpLT->HandleGlyphClick + (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion); - if (! mpLT->IsAdjustingLabel() ) + if (! mHit.mIsAdjustingLabel ) { // The positive hit test should have ensured otherwise //wxASSERT(false); @@ -125,7 +118,8 @@ UIHandle::Result LabelGlyphHandle::Drag const wxMouseEvent &event = evt.event; ViewInfo &viewInfo = pProject->GetViewInfo(); - mpLT->HandleGlyphDragRelease(event, mRect, viewInfo, &viewInfo.selectedRegion); + mpLT->HandleGlyphDragRelease + (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion); // Refresh all so that the change of selection is redrawn in all tracks return result | RefreshCode::RefreshAll | RefreshCode::DrawOverlays; @@ -134,7 +128,7 @@ UIHandle::Result LabelGlyphHandle::Drag HitTestPreview LabelGlyphHandle::Preview (const TrackPanelMouseState &, const AudacityProject *) { - return HitPreview(mpLT->mbHitCenter, 0); + return HitPreview( mHit.mEdge & 4 ); } UIHandle::Result LabelGlyphHandle::Release @@ -142,11 +136,11 @@ UIHandle::Result LabelGlyphHandle::Release wxWindow *pParent) { auto result = LabelDefaultClickHandle::Release( evt, pProject, pParent ); - mpLT->mOldEdge = 0; const wxMouseEvent &event = evt.event; ViewInfo &viewInfo = pProject->GetViewInfo(); - if (mpLT->HandleGlyphDragRelease(event, mRect, viewInfo, &viewInfo.selectedRegion)) { + if (mpLT->HandleGlyphDragRelease + (mHit, event, mRect, viewInfo, &viewInfo.selectedRegion)) { pProject->PushState(_("Modified Label"), _("Label Edit"), UndoPush::CONSOLIDATE); @@ -158,7 +152,6 @@ UIHandle::Result LabelGlyphHandle::Release UIHandle::Result LabelGlyphHandle::Cancel(AudacityProject *pProject) { - mpLT->mOldEdge = 0; pProject->RollbackState(); auto result = LabelDefaultClickHandle::Cancel( pProject ); return result | RefreshCode::RefreshAll; diff --git a/src/tracks/labeltrack/ui/LabelGlyphHandle.h b/src/tracks/labeltrack/ui/LabelGlyphHandle.h index e35d2d87b..9718f81fd 100644 --- a/src/tracks/labeltrack/ui/LabelGlyphHandle.h +++ b/src/tracks/labeltrack/ui/LabelGlyphHandle.h @@ -19,14 +19,33 @@ class wxMouseState; struct HitTestResult; class LabelTrack; +/// mEdge: +/// 0 if not over a glyph, +/// else a bitwise or of : +/// 1 if over the left-hand glyph, +/// 2 if over the right-hand glyph on a label, +/// 4 if over center. +/// +/// mMouseLabelLeft - index of any left label hit +/// mMouseLabelRight - index of any right label hit +/// +struct LabelTrackHit { + int mEdge{}; + int mMouseOverLabelLeft{ -1 }; /// Keeps track of which left label the mouse is currently over. + int mMouseOverLabelRight{ -1 }; /// Keeps track of which right label the mouse is currently over. + bool mbIsMoving {}; + bool mIsAdjustingLabel {}; +}; + class LabelGlyphHandle final : public LabelDefaultClickHandle { LabelGlyphHandle(const LabelGlyphHandle&) = delete; - static HitTestPreview HitPreview(bool hitCenter, unsigned refreshResult); + static HitTestPreview HitPreview(bool hitCenter); public: explicit LabelGlyphHandle - (const std::shared_ptr &pLT, const wxRect &rect); + (const std::shared_ptr &pLT, + const wxRect &rect, const LabelTrackHit &hit); LabelGlyphHandle &operator=(LabelGlyphHandle&&) = default; @@ -55,6 +74,11 @@ public: bool StopsOnKeystroke() override { return true; } + LabelTrackHit mHit{}; + + static UIHandle::Result NeedChangeHighlight + (const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState); + private: std::shared_ptr mpLT {}; wxRect mRect {}; diff --git a/src/tracks/labeltrack/ui/LabelTrackUI.cpp b/src/tracks/labeltrack/ui/LabelTrackUI.cpp index bf192020d..c6798ca66 100644 --- a/src/tracks/labeltrack/ui/LabelTrackUI.cpp +++ b/src/tracks/labeltrack/ui/LabelTrackUI.cpp @@ -31,13 +31,11 @@ HitTestResult LabelTrack::DetailedHitTest // Try label movement handles first result = LabelGlyphHandle::HitTest( mGlyphHandle, state, Pointer(this), st.rect); - auto refresh = result.preview.refreshCode; // kludge if ( !result.handle ) { // Missed glyph, try text box result = LabelTextHandle::HitTest( mTextHandle, state, Pointer(this)); - result.preview.refreshCode |= refresh; // kludge } return result; diff --git a/src/tracks/ui/TrackUI.cpp b/src/tracks/ui/TrackUI.cpp index 5f3d85229..4c27314a4 100644 --- a/src/tracks/ui/TrackUI.cpp +++ b/src/tracks/ui/TrackUI.cpp @@ -46,9 +46,6 @@ HitTestResult Track::HitTest // If there is no detailed hit for the subclass, there are still some // general cases. - // Label track kludge! - auto refresh = result.preview.refreshCode; - // Sliding applies in more than one track type. if ( !result.handle && !isMultiTool && currentTool == slideTool ) result = TimeShiftHandle::HitAnywhere( @@ -65,7 +62,6 @@ HitTestResult Track::HitTest result = SelectHandle::HitTest( mSelectHandle, st, pProject, Pointer(this)); - result.preview.refreshCode |= refresh; return result; }