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/Experimental.h b/src/Experimental.h index d69528842..d5f93f9df 100644 --- a/src/Experimental.h +++ b/src/Experimental.h @@ -173,7 +173,6 @@ // The following enable parts of the scrubbing user interface. #define EXPERIMENTAL_SCRUBBING_BASIC #ifdef EXPERIMENTAL_SCRUBBING_BASIC - #define EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL #define EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL #endif #endif 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/Project.cpp b/src/Project.cpp index 02e85036c..487378388 100644 --- a/src/Project.cpp +++ b/src/Project.cpp @@ -5364,7 +5364,6 @@ void AudacityProject::PlaybackScroller::OnTimer(wxCommandEvent &event) // Let other listeners get the notification event.Skip(); -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (mActive && mProject->IsAudioActive()) { // Pan the view, so that we center the play indicator. @@ -5382,5 +5381,4 @@ void AudacityProject::PlaybackScroller::OnTimer(wxCommandEvent &event) viewInfo.h = std::max(0.0, viewInfo.h); trackPanel->Refresh(false); } -#endif } diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index f78c09c22..ce36069d1 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -5472,12 +5472,9 @@ void TrackPanel::HandleWheelRotation(wxMouseEvent & event) (event.m_wheelDelta > 0 ? (double)event.m_wheelDelta : 120.0); if (event.ShiftDown() -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL // Don't pan during smooth scrolling. That would conflict with keeping // the play indicator centered. - && !GetProject()->GetScrubber().IsScrollScrubbing() -#endif - ) + && !GetProject()->GetScrubber().IsScrollScrubbing()) { // MM: Scroll left/right when used with Shift key down mListener->TP_ScrollWindow( @@ -5506,15 +5503,12 @@ void TrackPanel::HandleWheelRotation(wxMouseEvent & event) // Time corresponding to mouse position wxCoord xx; double center_h; -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (GetProject()->GetScrubber().IsScrollScrubbing()) { // Expand or contract about the center, ignoring mouse position center_h = mViewInfo->h + (GetScreenEndTime() - mViewInfo->h) / 2.0; xx = mViewInfo->TimeToPosition(center_h, trackLeftEdge); } - else -#endif - { + else { xx = event.m_x; center_h = mViewInfo->PositionToTime(xx, trackLeftEdge); } diff --git a/src/prefs/MousePrefs.cpp b/src/prefs/MousePrefs.cpp index 4e49aa059..e51bc752b 100644 --- a/src/prefs/MousePrefs.cpp +++ b/src/prefs/MousePrefs.cpp @@ -110,13 +110,6 @@ void MousePrefs::CreateList() AddItem(_("Left-Drag"), _("Select"), _("Set Selection Range")); AddItem(_("Shift-Left-Click"), _("Select"), _("Extend Selection Range")); AddItem(_("Left-Double-Click"), _("Select"), _("Select Clip or Entire Track")); -#ifdef EXPERIMENTAL_SCRUBBING_BASIC - AddItem(CTRL + _("-Left-Click"), _("Select"), _("Scrub")); - AddItem(CTRL + _("-Left-Drag"), _("Select"), _("Seek")); -#endif -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL - AddItem(CTRL + _("-Left-Double-Click"), _("Select"), _("Scroll-scrub")); -#endif #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL AddItem(_("Wheel-Rotate"), _("Select"), _("Change scrub speed")); #endif diff --git a/src/tracks/ui/Scrubbing.cpp b/src/tracks/ui/Scrubbing.cpp index 3a420820b..7175175ad 100644 --- a/src/tracks/ui/Scrubbing.cpp +++ b/src/tracks/ui/Scrubbing.cpp @@ -188,11 +188,7 @@ namespace { void Scrubber::MarkScrubStart( // Assume xx is relative to the left edge of TrackPanel! - wxCoord xx -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL - , bool smoothScrolling -#endif - , bool alwaysSeeking + wxCoord xx, bool smoothScrolling, bool alwaysSeeking ) { UncheckAllMenuItems(); @@ -200,9 +196,7 @@ void Scrubber::MarkScrubStart( // Don't actually start scrubbing, but collect some information // needed for the decision to start scrubbing later when handling // drag events. -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL - SetScrollScrubbing (smoothScrolling); -#endif + mSmoothScrollingScrub = smoothScrolling; mAlwaysSeeking = alwaysSeeking; ControlToolBar * const ctb = mProject->GetControlToolBar(); @@ -236,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 @@ -265,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); @@ -278,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 = @@ -303,8 +310,11 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) // Wait to test again mScrubStartClockTimeMillis = ::wxGetLocalTimeMillis(); - if (IsScrubbing()) + if (IsScrubbing()) { + mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub); mScrubHasFocus = true; + mLastScrubPosition = xx; + } // Return true whether we started scrub, or are still waiting to decide. return true; @@ -313,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, @@ -337,26 +353,32 @@ 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; -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (mSmoothScrollingScrub) { const double speed = FindScrubSpeed(seek, time); result = gAudioIO->EnqueueScrubBySignedSpeed(speed, mMaxScrubSpeed, seek); } else -#endif result = gAudioIO->EnqueueScrubByPosition - (time, seek ? 1.0 : mMaxScrubSpeed, seek); + (time, seek ? 1.0 : mMaxScrubSpeed, seek); } if (result) @@ -364,12 +386,9 @@ void Scrubber::ContinueScrubbing() // else, if seek requested, try again at a later time when we might // enqueue a long enough stutter -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (mSmoothScrollingScrub) ; - else -#endif - { + else { if (mScrubSpeedDisplayCountdown > 0) --mScrubSpeedDisplayCountdown; } @@ -380,7 +399,8 @@ void Scrubber::StopScrubbing() UncheckAllMenuItems(); mScrubStartPosition = -1; - SetScrollScrubbing (false); + mProject->GetPlaybackScroller().Activate(false); + mDragging = false; if (!IsScrubbing()) { @@ -391,12 +411,11 @@ void Scrubber::StopScrubbing() } mProject->GetRulerPanel()->HideQuickPlayIndicator(); -} -void Scrubber::SetScrollScrubbing(bool scrollScrubbing) -{ - mSmoothScrollingScrub = scrollScrubbing; - mProject->GetPlaybackScroller().Activate(scrollScrubbing); + // 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 @@ -408,24 +427,22 @@ bool Scrubber::IsScrubbing() const else { const_cast(*this).mScrubToken = -1; const_cast(*this).mScrubStartPosition = -1; -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL - // Don't call SetScrollScrubbing const_cast(*this).mSmoothScrollingScrub = false; -#endif return false; } } bool Scrubber::ShouldDrawScrubSpeed() { + if (mDragging) + return false; + return IsScrubbing() && mScrubHasFocus && ( // Draw for (non-scroll) scrub, sometimes, but never for seek (!PollIsSeeking() && mScrubSpeedDisplayCountdown > 0) -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL // Draw always for scroll-scrub and for scroll-seek || mSmoothScrollingScrub -#endif ); } @@ -439,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); @@ -447,9 +468,7 @@ void Scrubber::HandleScrollWheel(int steps) newSpeed <= AudioIO::GetMaxScrubSpeed()) { mLogMaxScrubSpeed = newLogMaxScrubSpeed; mMaxScrubSpeed = newSpeed; -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (!mSmoothScrollingScrub) -#endif // Show the speed for one second mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1; } @@ -472,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(); @@ -544,11 +564,9 @@ void ScrubbingOverlay::Draw(OverlayPanel &, wxDC &dc) // (b) Error alerts // So they were changed to 'orange' and 'lime'. static const wxColour clrNoScroll(215, 162, 0), clrScroll(0, 204, 153); -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL if (scrubber.IsScrollScrubbing()) dc.SetTextForeground(clrScroll); else -#endif dc.SetTextForeground(clrNoScroll); dc.DrawText(mLastScrubSpeedText, mLastScrubRect.GetX(), mLastScrubRect.GetY()); @@ -565,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) { @@ -603,23 +622,17 @@ void ScrubbingOverlay::OnTimer(wxCommandEvent &event) // Find the text const double maxScrubSpeed = GetScrubber().GetMaxScrubSpeed(); const double speed = -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL scrubber.IsScrollScrubbing() ? scrubber.FindScrubSpeed (seeking, mProject->GetViewInfo().PositionToTime(position.x, trackPanel->GetLeftOffset())) - : -#endif - maxScrubSpeed; + : maxScrubSpeed; const wxChar *format = -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL scrubber.IsScrollScrubbing() ? seeking ? wxT("%+.2fX") : wxT("%+.2f") - : -#endif - wxT("%.2f"); + : wxT("%.2f"); mNextScrubSpeedText = wxString::Format(format, speed); @@ -657,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) @@ -677,7 +690,8 @@ void Scrubber::DoScrub(bool scroll, bool seek) MarkScrubStart(xx, scroll, seek); } else if(!match) { - SetScrollScrubbing(scroll); + mSmoothScrollingScrub = scroll; + mProject->GetPlaybackScroller().Activate(scroll); mAlwaysSeeking = seek; UncheckAllMenuItems(); CheckMenuItem(); diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h index 315221008..9a1a6d797 100644 --- a/src/tracks/ui/Scrubbing.h +++ b/src/tracks/ui/Scrubbing.h @@ -29,11 +29,8 @@ public: // Assume xx is relative to the left edge of TrackPanel! void MarkScrubStart( - wxCoord xx -#ifdef EXPERIMENTAL_SCRUBBING_SMOOTH_SCROLL - , bool smoothScrolling -#endif - , bool alwaysSeeking // if false, can switch seeking or scrubbing + wxCoord xx, bool smoothScrolling, + bool alwaysSeeking // if false, can switch seeking or scrubbing // by mouse button state ); @@ -57,7 +54,6 @@ public: bool IsScrollScrubbing() const // If true, implies HasStartedScrubbing() { return mSmoothScrollingScrub; } - void SetScrollScrubbing(bool scrollScrubbing); bool IsAlwaysSeeking() const { return mAlwaysSeeking; } @@ -113,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 c7917487d..8e1f78b48 100644 --- a/src/widgets/Ruler.cpp +++ b/src/widgets/Ruler.cpp @@ -2326,6 +2326,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(); @@ -2654,10 +2665,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()