1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-16 16:10:06 +02:00

Implement drag-scrub, compatibly with the existing move-scrub...

... Start scrub by click or double click on the scrub head; release button or
not; then move.

If you release before moving, you get scrubbing as before, controlled by
motion.  Click or drag to switch in and out of seeking.  Stop with ESC,
spacebar, etc.  No change of selection.

But now if you drag, then scrubbing contines until you release the mouse or
otherwise stop with a key.

If by release of the mouse, then the selection changes as if by a click at
the last play position.  If you hold shift, then, as if by shift-click.

If drag begins with a double-click, then the play head remains centered and
the track moves.
This commit is contained in:
Paul Licameli 2016-05-10 09:28:06 -04:00
parent 5944391e24
commit 19ef2f6681
8 changed files with 135 additions and 19 deletions

View File

@ -402,6 +402,14 @@ struct AudioIO::ScrubQueue
} }
~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) bool Producer(double end, double maxSpeed, bool bySpeed, bool maySkip)
{ {
// Main thread indicates a scrubbing interval // Main thread indicates a scrubbing interval
@ -670,7 +678,7 @@ private:
const double mRate; const double mRate;
const long mMinStutter; const long mMinStutter;
wxLongLong mLastScrubTimeMillis; wxLongLong mLastScrubTimeMillis;
wxCriticalSection mUpdating; mutable wxCriticalSection mUpdating;
}; };
#endif #endif
@ -2426,6 +2434,15 @@ bool AudioIO::EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool mayS
else else
return false; return false;
} }
double AudioIO::GetLastTimeInScrubQueue() const
{
if (mScrubQueue)
return mScrubQueue->LastTimeInQueue();
else
return -1.0;
}
#endif #endif
bool AudioIO::IsBusy() bool AudioIO::IsBusy()

View File

@ -208,6 +208,10 @@ class AUDACITY_DLL_API AudioIO final {
* Return true if some work was really enqueued. * Return true if some work was really enqueued.
*/ */
bool EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip); bool EnqueueScrubBySignedSpeed(double speed, double maxSpeed, bool maySkip);
/** \brief return the ending time of the last enqueued scrub interval.
*/
double GetLastTimeInScrubQueue() const;
#endif #endif
/** \brief Returns true if audio i/o is busy starting, stopping, playing, /** \brief Returns true if audio i/o is busy starting, stopping, playing,

View File

@ -2281,8 +2281,13 @@ void AudacityProject::OnRecordAppend()
GetControlToolBar()->OnRecord(evt); GetControlToolBar()->OnRecord(evt);
} }
// The code for "OnPlayStopSelect" is simply the code of "OnPlayStop" and "OnStopSelect" merged.
void AudacityProject::OnPlayStopSelect() 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; wxCommandEvent evt;
ControlToolBar *toolbar = GetControlToolBar(); ControlToolBar *toolbar = GetControlToolBar();
@ -2291,7 +2296,36 @@ void AudacityProject::OnPlayStopSelect()
if (gAudioIO->IsStreamActive(GetAudioIOToken())) { if (gAudioIO->IsStreamActive(GetAudioIOToken())) {
toolbar->SetPlay(false); //Pops toolbar->SetPlay(false); //Pops
toolbar->SetStop(true); //Pushes stop down 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 ModifyState(false); // without bWantsAutoSave
toolbar->OnStop(evt); toolbar->OnStop(evt);
} }

View File

@ -80,6 +80,7 @@ void OnSeekRightLong();
bool MakeReadyToPlay(bool loop = false, bool cutpreview = false); // Helper function that sets button states etc. bool MakeReadyToPlay(bool loop = false, bool cutpreview = false); // Helper function that sets button states etc.
void OnPlayStop(); void OnPlayStop();
void DoPlayStopSelect(bool click, bool shift);
void OnPlayStopSelect(); void OnPlayStopSelect();
void OnPlayOneSecond(); void OnPlayOneSecond();
void OnPlayToSelection(); void OnPlayToSelection();

View File

@ -230,6 +230,9 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
if (IsScrubbing()) if (IsScrubbing())
return false; return false;
else { else {
const auto state = ::wxGetMouseState();
mDragging = state.LeftIsDown();
const bool busy = gAudioIO->IsBusy(); const bool busy = gAudioIO->IsBusy();
if (busy && gAudioIO->GetNumCaptureChannels() > 0) { if (busy && gAudioIO->GetNumCaptureChannels() > 0) {
// Do not stop recording, and don't try to start scrubbing after // Do not stop recording, and don't try to start scrubbing after
@ -259,6 +262,14 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
mScrubStartPosition = position; 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()); AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions());
options.timeTrack = NULL; options.timeTrack = NULL;
options.scrubDelay = (kTimerInterval / 1000.0); options.scrubDelay = (kTimerInterval / 1000.0);
@ -272,8 +283,10 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
p->GetTranscriptionToolBar()->GetPlaySpeed(); p->GetTranscriptionToolBar()->GetPlaySpeed();
} }
#else #else
// That idea seems unpopular... just make it one // That idea seems unpopular... just make it one for move-scrub,
mMaxScrubSpeed = options.maxScrubSpeed = 1.0; // but big for drag-scrub
mMaxScrubSpeed = options.maxScrubSpeed =
mDragging ? AudioIO::GetMaxScrubSpeed() : 1.0;
#endif #endif
options.maxScrubTime = mProject->GetTracks()->GetEndTime(); options.maxScrubTime = mProject->GetTracks()->GetEndTime();
ControlToolBar::PlayAppearance appearance = ControlToolBar::PlayAppearance appearance =
@ -300,6 +313,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
if (IsScrubbing()) { if (IsScrubbing()) {
mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub); mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub);
mScrubHasFocus = true; mScrubHasFocus = true;
mLastScrubPosition = xx;
} }
// Return true whether we started scrub, or are still waiting to decide. // Return true whether we started scrub, or are still waiting to decide.
@ -309,13 +323,19 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
void Scrubber::ContinueScrubbing() 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, // Thus scrubbing relies mostly on periodic polling of mouse and keys,
// not event notifications. But there are a few event handlers that // not event notifications. But there are a few event handlers that
// leave messages for this routine, in mScrubSeekPress and in mScrubHasFocus. // leave messages for this routine, in mScrubSeekPress and in mScrubHasFocus.
// Seek only when the pointer is in the panel. Else, scrub. // Seek only when the pointer is in the panel. Else, scrub.
const wxMouseState state(::wxGetMouseState());
TrackPanel *const trackPanel = mProject->GetTrackPanel(); TrackPanel *const trackPanel = mProject->GetTrackPanel();
// Decide whether to skip play, because either mouse is down now, // 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()); 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; bool result = false;
if (!mScrubHasFocus) if (!mScrubHasFocus)
// When we don't have focus, enqueue silent scrubs until we regain focus.
result = gAudioIO->EnqueueScrubBySignedSpeed(0, mMaxScrubSpeed, false); 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 { else {
const double time = mProject->GetViewInfo().PositionToTime(position.x, trackPanel->GetLeftOffset()); const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset());
if (seek) if (seek)
// Cause OnTimer() to suppress the speed display // Cause OnTimer() to suppress the speed display
mScrubSpeedDisplayCountdown = 1; mScrubSpeedDisplayCountdown = 1;
@ -372,6 +400,7 @@ void Scrubber::StopScrubbing()
mScrubStartPosition = -1; mScrubStartPosition = -1;
mProject->GetPlaybackScroller().Activate(false); mProject->GetPlaybackScroller().Activate(false);
mDragging = false;
if (!IsScrubbing()) if (!IsScrubbing())
{ {
@ -382,6 +411,11 @@ void Scrubber::StopScrubbing()
} }
mProject->GetRulerPanel()->HideQuickPlayIndicator(); 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 bool Scrubber::IsScrubbing() const
@ -400,6 +434,9 @@ bool Scrubber::IsScrubbing() const
bool Scrubber::ShouldDrawScrubSpeed() bool Scrubber::ShouldDrawScrubSpeed()
{ {
if (mDragging)
return false;
return IsScrubbing() && return IsScrubbing() &&
mScrubHasFocus && ( mScrubHasFocus && (
// Draw for (non-scroll) scrub, sometimes, but never for seek // 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) 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; const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
static const double maxScrubSpeedBase = static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
@ -450,7 +491,8 @@ void Scrubber::Forwarder::OnMouse(wxMouseEvent &event)
if (isScrubbing && !event.HasAnyModifiers()) { if (isScrubbing && !event.HasAnyModifiers()) {
if(event.LeftDown() || if(event.LeftDown() ||
(event.LeftIsDown() && event.Dragging())) { (event.LeftIsDown() && event.Dragging())) {
scrubber.mScrubSeekPress = true; if (!scrubber.mDragging)
scrubber.mScrubSeekPress = true;
auto xx = ruler->ScreenToClient(::wxGetMousePosition()).x; auto xx = ruler->ScreenToClient(::wxGetMousePosition()).x;
ruler->UpdateQuickPlayPos(xx); ruler->UpdateQuickPlayPos(xx);
ruler->ShowQuickPlayIndicator(); ruler->ShowQuickPlayIndicator();
@ -541,12 +583,13 @@ void ScrubbingOverlay::OnTimer(wxCommandEvent &event)
auto position = ::wxGetMousePosition(); auto position = ::wxGetMousePosition();
{ {
auto xx = ruler->ScreenToClient(position).x; if(scrubber.HasStartedScrubbing()) {
ruler->UpdateQuickPlayPos(xx); auto xx = ruler->ScreenToClient(position).x;
ruler->UpdateQuickPlayPos(xx);
if(!isScrubbing && scrubber.HasStartedScrubbing()) { if (!isScrubbing)
// Really start scrub if motion is far enough // Really start scrub if motion is far enough
scrubber.MaybeStartScrubbing(xx); scrubber.MaybeStartScrubbing(xx);
} }
if (!isScrubbing) { if (!isScrubbing) {
@ -627,7 +670,7 @@ Scrubber &ScrubbingOverlay::GetScrubber()
bool Scrubber::PollIsSeeking() bool Scrubber::PollIsSeeking()
{ {
return mAlwaysSeeking || ::wxGetMouseState().LeftIsDown(); return !mDragging && (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown());
} }
void Scrubber::DoScrub(bool scroll, bool seek) void Scrubber::DoScrub(bool scroll, bool seek)

View File

@ -109,10 +109,12 @@ private:
bool mScrubHasFocus; bool mScrubHasFocus;
int mScrubSpeedDisplayCountdown; int mScrubSpeedDisplayCountdown;
wxCoord mScrubStartPosition; wxCoord mScrubStartPosition;
wxCoord mLastScrubPosition {};
double mMaxScrubSpeed; double mMaxScrubSpeed;
bool mScrubSeekPress; bool mScrubSeekPress;
bool mSmoothScrollingScrub; bool mSmoothScrollingScrub;
bool mAlwaysSeeking{}; bool mAlwaysSeeking {};
bool mDragging {};
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
int mLogMaxScrubSpeed; int mLogMaxScrubSpeed;

View File

@ -2582,6 +2582,17 @@ bool AdornedRulerPanel::IsWithinMarker(int mousePosX, double markerTime)
void AdornedRulerPanel::OnMouseEvents(wxMouseEvent &evt) 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? // PRL: why do I need these two lines on Windows but not on Mac?
if (evt.ButtonDown(wxMOUSE_BTN_ANY)) if (evt.ButtonDown(wxMOUSE_BTN_ANY))
SetFocus(); SetFocus();
@ -2910,10 +2921,12 @@ void AdornedRulerPanel::HandleQPRelease(wxMouseEvent &evt)
if (mDoubleClick) if (mDoubleClick)
return; return;
HideQuickPlayIndicator();
if (HasCapture()) if (HasCapture())
ReleaseMouse(); ReleaseMouse();
else
return;
HideQuickPlayIndicator();
mCaptureState = CaptureState{}; mCaptureState = CaptureState{};

View File

@ -350,6 +350,7 @@ public:
void ShowQuickPlayIndicator(); void ShowQuickPlayIndicator();
void HideQuickPlayIndicator(); void HideQuickPlayIndicator();
void UpdateQuickPlayPos(wxCoord &mousPosX); void UpdateQuickPlayPos(wxCoord &mousPosX);
void IgnoreMouseUp() { mIgnoreMouseUp = true; }
private: private:
void OnCapture(wxCommandEvent & evt); void OnCapture(wxCommandEvent & evt);
@ -531,6 +532,7 @@ private:
mutable wxFont mButtonFont; mutable wxFont mButtonFont;
bool mDoubleClick {}; bool mDoubleClick {};
bool mIgnoreMouseUp {};
DECLARE_EVENT_TABLE() DECLARE_EVENT_TABLE()