diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index b3b9c2443..d757e4da3 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -402,6 +402,14 @@ struct AudioIO::ScrubQueue } ~ScrubQueue() {} + double LastTimeInQueue() const + { + // Needed by the main thread sometimes + wxCriticalSectionLocker locker(mUpdating); + const Entry &previous = mEntries[(mLeadingIdx + Size - 1) % Size]; + return previous.mS1 / mRate; + } + bool Producer(double end, double maxSpeed, bool bySpeed, bool maySkip) { // Main thread indicates a scrubbing interval @@ -670,7 +678,7 @@ private: const double mRate; const long mMinStutter; wxLongLong mLastScrubTimeMillis; - wxCriticalSection mUpdating; + mutable wxCriticalSection mUpdating; }; #endif @@ -2426,6 +2434,15 @@ bool AudioIO::EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool mayS else return false; } + +double AudioIO::GetLastTimeInScrubQueue() const +{ + if (mScrubQueue) + return mScrubQueue->LastTimeInQueue(); + else + return -1.0; +} + #endif bool AudioIO::IsBusy() diff --git a/src/AudioIO.h b/src/AudioIO.h index 8bb62bc67..c57501276 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -208,6 +208,10 @@ class AUDACITY_DLL_API AudioIO final { * Return true if some work was really enqueued. */ bool EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip); + + /** \brief return the ending time of the last enqueued scrub interval. + */ + double GetLastTimeInScrubQueue() const; #endif /** \brief Returns true if audio i/o is busy starting, stopping, playing, diff --git a/src/Menus.cpp b/src/Menus.cpp index d78b6aecf..0aa854920 100644 --- a/src/Menus.cpp +++ b/src/Menus.cpp @@ -2281,8 +2281,13 @@ void AudacityProject::OnRecordAppend() GetControlToolBar()->OnRecord(evt); } -// The code for "OnPlayStopSelect" is simply the code of "OnPlayStop" and "OnStopSelect" merged. void AudacityProject::OnPlayStopSelect() +{ + DoPlayStopSelect(false, false); +} + +// The code for "OnPlayStopSelect" is simply the code of "OnPlayStop" and "OnStopSelect" merged. +void AudacityProject::DoPlayStopSelect(bool click, bool shift) { wxCommandEvent evt; ControlToolBar *toolbar = GetControlToolBar(); @@ -2291,7 +2296,36 @@ void AudacityProject::OnPlayStopSelect() if (gAudioIO->IsStreamActive(GetAudioIOToken())) { toolbar->SetPlay(false); //Pops toolbar->SetStop(true); //Pushes stop down - mViewInfo.selectedRegion.setT0(gAudioIO->GetStreamTime(), false); + + // change the selection + auto time = gAudioIO->GetStreamTime(); + auto &selection = mViewInfo.selectedRegion; + if (shift && click) { + // Change the region selection, as if by shift-click at the play head + auto t0 = selection.t0(), t1 = selection.t1(); + if (time < t0) + // Grow selection + t0 = time; + else if (time > t1) + // Grow selection + t1 = time; + else { + // Shrink selection, changing the nearer boundary + if (fabs(t0 - time) < fabs(t1 - time)) + t0 = time; + else + t1 = time; + } + selection.setTimes(t0, t1); + } + else if (click) + // Set a point selection, as if by a click at the play head + selection.setTimes(time, time); + else + // How stop and set cursor always worked + // -- change t0, collapsing to point only if t1 was greater + selection.setT0(time, false); + ModifyState(false); // without bWantsAutoSave toolbar->OnStop(evt); } diff --git a/src/Menus.h b/src/Menus.h index edd2d8fbd..7a32590d7 100644 --- a/src/Menus.h +++ b/src/Menus.h @@ -80,6 +80,7 @@ void OnSeekRightLong(); bool MakeReadyToPlay(bool loop = false, bool cutpreview = false); // Helper function that sets button states etc. void OnPlayStop(); +void DoPlayStopSelect(bool click, bool shift); void OnPlayStopSelect(); void OnPlayOneSecond(); void OnPlayToSelection(); diff --git a/src/tracks/ui/Scrubbing.cpp b/src/tracks/ui/Scrubbing.cpp index 19306230f..7175175ad 100644 --- a/src/tracks/ui/Scrubbing.cpp +++ b/src/tracks/ui/Scrubbing.cpp @@ -230,6 +230,9 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) if (IsScrubbing()) return false; else { + const auto state = ::wxGetMouseState(); + mDragging = state.LeftIsDown(); + const bool busy = gAudioIO->IsBusy(); if (busy && gAudioIO->GetNumCaptureChannels() > 0) { // Do not stop recording, and don't try to start scrubbing after @@ -259,6 +262,14 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) mScrubStartPosition = position; } + if (mDragging && mSmoothScrollingScrub) { + auto delta = time0 - time1; + time0 = std::max(0.0, std::min(maxTime, + (viewInfo.h + mProject->GetScreenEndTime()) / 2 + )); + time1 = time0 + delta; + } + AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions()); options.timeTrack = NULL; options.scrubDelay = (kTimerInterval / 1000.0); @@ -272,8 +283,10 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) p->GetTranscriptionToolBar()->GetPlaySpeed(); } #else - // That idea seems unpopular... just make it one - mMaxScrubSpeed = options.maxScrubSpeed = 1.0; + // That idea seems unpopular... just make it one for move-scrub, + // but big for drag-scrub + mMaxScrubSpeed = options.maxScrubSpeed = + mDragging ? AudioIO::GetMaxScrubSpeed() : 1.0; #endif options.maxScrubTime = mProject->GetTracks()->GetEndTime(); ControlToolBar::PlayAppearance appearance = @@ -300,6 +313,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) if (IsScrubbing()) { mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub); mScrubHasFocus = true; + mLastScrubPosition = xx; } // Return true whether we started scrub, or are still waiting to decide. @@ -309,13 +323,19 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) void Scrubber::ContinueScrubbing() { + const wxMouseState state(::wxGetMouseState()); + + if (mDragging && !state.LeftIsDown()) { + // Stop and set cursor + mProject->DoPlayStopSelect(true, state.ShiftDown()); + return; + } // Thus scrubbing relies mostly on periodic polling of mouse and keys, // not event notifications. But there are a few event handlers that // leave messages for this routine, in mScrubSeekPress and in mScrubHasFocus. // Seek only when the pointer is in the panel. Else, scrub. - const wxMouseState state(::wxGetMouseState()); TrackPanel *const trackPanel = mProject->GetTrackPanel(); // Decide whether to skip play, because either mouse is down now, @@ -333,13 +353,21 @@ void Scrubber::ContinueScrubbing() } const wxPoint position = trackPanel->ScreenToClient(state.GetPosition()); - // When we don't have focus, enqueue silent scrubs until we regain focus. + const auto &viewInfo = mProject->GetViewInfo(); + bool result = false; if (!mScrubHasFocus) + // When we don't have focus, enqueue silent scrubs until we regain focus. result = gAudioIO->EnqueueScrubBySignedSpeed(0, mMaxScrubSpeed, false); + else if (mDragging && mSmoothScrollingScrub) { + const auto lastTime = gAudioIO->GetLastTimeInScrubQueue(); + const auto delta = mLastScrubPosition - position.x; + const double time = viewInfo.OffsetTimeByPixels(lastTime, delta); + result = gAudioIO->EnqueueScrubByPosition(time, mMaxScrubSpeed, false); + mLastScrubPosition = position.x; + } else { - const double time = mProject->GetViewInfo().PositionToTime(position.x, trackPanel->GetLeftOffset()); - + const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset()); if (seek) // Cause OnTimer() to suppress the speed display mScrubSpeedDisplayCountdown = 1; @@ -372,6 +400,7 @@ void Scrubber::StopScrubbing() mScrubStartPosition = -1; mProject->GetPlaybackScroller().Activate(false); + mDragging = false; if (!IsScrubbing()) { @@ -382,6 +411,11 @@ void Scrubber::StopScrubbing() } mProject->GetRulerPanel()->HideQuickPlayIndicator(); + + // Need this in case ruler gets the mouse-up event after escaping scrubbing: + // prevent reappearance of the + // quick play guideline + mProject->GetRulerPanel()->IgnoreMouseUp(); } bool Scrubber::IsScrubbing() const @@ -400,6 +434,9 @@ bool Scrubber::IsScrubbing() const bool Scrubber::ShouldDrawScrubSpeed() { + if (mDragging) + return false; + return IsScrubbing() && mScrubHasFocus && ( // Draw for (non-scroll) scrub, sometimes, but never for seek @@ -419,6 +456,10 @@ double Scrubber::FindScrubSpeed(bool seeking, double time) const void Scrubber::HandleScrollWheel(int steps) { + if (mDragging) + // Not likely you would spin it with the left button down, but... + return; + const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps; static const double maxScrubSpeedBase = pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); @@ -450,7 +491,8 @@ void Scrubber::Forwarder::OnMouse(wxMouseEvent &event) if (isScrubbing && !event.HasAnyModifiers()) { if(event.LeftDown() || (event.LeftIsDown() && event.Dragging())) { - scrubber.mScrubSeekPress = true; + if (!scrubber.mDragging) + scrubber.mScrubSeekPress = true; auto xx = ruler->ScreenToClient(::wxGetMousePosition()).x; ruler->UpdateQuickPlayPos(xx); ruler->ShowQuickPlayIndicator(); @@ -541,12 +583,13 @@ void ScrubbingOverlay::OnTimer(wxCommandEvent &event) auto position = ::wxGetMousePosition(); { - auto xx = ruler->ScreenToClient(position).x; - ruler->UpdateQuickPlayPos(xx); + if(scrubber.HasStartedScrubbing()) { + auto xx = ruler->ScreenToClient(position).x; + ruler->UpdateQuickPlayPos(xx); - if(!isScrubbing && scrubber.HasStartedScrubbing()) { - // Really start scrub if motion is far enough - scrubber.MaybeStartScrubbing(xx); + if (!isScrubbing) + // Really start scrub if motion is far enough + scrubber.MaybeStartScrubbing(xx); } if (!isScrubbing) { @@ -627,7 +670,7 @@ Scrubber &ScrubbingOverlay::GetScrubber() bool Scrubber::PollIsSeeking() { - return mAlwaysSeeking || ::wxGetMouseState().LeftIsDown(); + return !mDragging && (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown()); } void Scrubber::DoScrub(bool scroll, bool seek) diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h index c8d405758..9a1a6d797 100644 --- a/src/tracks/ui/Scrubbing.h +++ b/src/tracks/ui/Scrubbing.h @@ -109,10 +109,12 @@ private: bool mScrubHasFocus; int mScrubSpeedDisplayCountdown; wxCoord mScrubStartPosition; + wxCoord mLastScrubPosition {}; double mMaxScrubSpeed; bool mScrubSeekPress; bool mSmoothScrollingScrub; - bool mAlwaysSeeking{}; + bool mAlwaysSeeking {}; + bool mDragging {}; #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL int mLogMaxScrubSpeed; diff --git a/src/widgets/Ruler.cpp b/src/widgets/Ruler.cpp index 23c279ed7..9174b2272 100644 --- a/src/widgets/Ruler.cpp +++ b/src/widgets/Ruler.cpp @@ -2582,6 +2582,17 @@ bool AdornedRulerPanel::IsWithinMarker(int mousePosX, double markerTime) void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt) { + if (mIgnoreMouseUp) { + if (evt.Dragging()) + return; + else if (evt.ButtonUp()) { + mIgnoreMouseUp = false; + return; + } + else + mIgnoreMouseUp = false; + } + // PRL: why do I need these two lines on Windows but not on Mac? if (evt.ButtonDown(wxMOUSE_BTN_ANY)) SetFocus(); @@ -2910,10 +2921,12 @@ void AdornedRulerPanel::HandleQPRelease(wxMouseEvent &evt) if (mDoubleClick) return; - HideQuickPlayIndicator(); - if (HasCapture()) ReleaseMouse(); + else + return; + + HideQuickPlayIndicator(); mCaptureState = CaptureState{}; diff --git a/src/widgets/Ruler.h b/src/widgets/Ruler.h index 177177a0b..f533c0736 100644 --- a/src/widgets/Ruler.h +++ b/src/widgets/Ruler.h @@ -350,6 +350,7 @@ public: void ShowQuickPlayIndicator(); void HideQuickPlayIndicator(); void UpdateQuickPlayPos(wxCoord &mousPosX); + void IgnoreMouseUp() { mIgnoreMouseUp = true; } private: void OnCapture(wxCommandEvent & evt); @@ -531,6 +532,7 @@ private: mutable wxFont mButtonFont; bool mDoubleClick {}; + bool mIgnoreMouseUp {}; DECLARE_EVENT_TABLE()