From 2eb6285ddcbcefcec958cb864de6b662f3ac4c5f Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Thu, 11 May 2017 15:44:40 -0400 Subject: [PATCH 1/6] Group some TrackPanel fields into a structure --- src/TrackPanel.cpp | 89 ++++++++++++++++++++++------------------------ src/TrackPanel.h | 21 ++++++----- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index b7ddadc8c..09b30bfea 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -480,10 +480,6 @@ TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id, #endif #ifdef USE_MIDI - mStretchMode = stretchCenter; - mStretching = false; - mStretched = false; - mStretchStart = 0; mStretchCursor = MakeCursor( wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16); mStretchLeftCursor = MakeCursor( wxCURSOR_BULLSEYE, StretchLeftCursorXpm, 16, 16); @@ -2065,7 +2061,7 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, mSnapLeft = mSnapRight = -1; #ifdef USE_MIDI - mStretching = false; + mStretchState = StretchState{}; bool stretch = HitTestStretch(pTrack, rect, event); #endif @@ -2236,19 +2232,19 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, double minPeriod = 0.05; // minimum beat period double qBeat0, qBeat1; double centerBeat = 0.0f; - mStretchSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); - mStretchSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); + mStretchState.mSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); + mStretchState.mSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); // If there is not (almost) a beat to stretch that is slower // than 20 beats per second, don't stretch if (within(qBeat0, qBeat1, 0.9) || - (mStretchSel1 - mStretchSel0) / (qBeat1 - qBeat0) < minPeriod) return; + (mStretchState.mSel1 - mStretchState.mSel0) / (qBeat1 - qBeat0) < minPeriod) return; if (startNewSelection) { // mouse is not at an edge, but after // quantization, we could be indicating the selection edge mSelStartValid = true; mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x)); - mStretchStart = nt->NearestBeatTime(mSelStart, ¢erBeat); + mStretchState.mStart = nt->NearestBeatTime(mSelStart, ¢erBeat); if (within(qBeat0, centerBeat, 0.1)) { mListener->TP_DisplayStatusMessage( _("Click and drag to stretch selected region.")); @@ -2269,28 +2265,28 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, } if (startNewSelection) { - mStretchMode = stretchCenter; - mStretchLeftBeats = qBeat1 - centerBeat; - mStretchRightBeats = centerBeat - qBeat0; + mStretchState.mMode = stretchCenter; + mStretchState.mLeftBeats = qBeat1 - centerBeat; + mStretchState.mRightBeats = centerBeat - qBeat0; } else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) { // note that at this point, mSelStart is at the opposite // end of the selection from the cursor. If the cursor is // over sel0, then mSelStart is at sel1. - mStretchMode = stretchLeft; + mStretchState.mMode = stretchLeft; } else { - mStretchMode = stretchRight; + mStretchState.mMode = stretchRight; } - if (mStretchMode == stretchLeft) { - mStretchLeftBeats = 0; - mStretchRightBeats = qBeat1 - qBeat0; - } else if (mStretchMode == stretchRight) { - mStretchLeftBeats = qBeat1 - qBeat0; - mStretchRightBeats = 0; + if (mStretchState.mMode == stretchLeft) { + mStretchState.mLeftBeats = 0; + mStretchState.mRightBeats = qBeat1 - qBeat0; + } else if (mStretchState.mMode == stretchRight) { + mStretchState.mLeftBeats = qBeat1 - qBeat0; + mStretchState.mRightBeats = 0; } - mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1); - mStretching = true; - mStretched = false; + mViewInfo->selectedRegion.setTimes(mStretchState.mSel0, mStretchState.mSel1); + mStretchState.mStretching = true; + mStretchState.mStretched = false; /* i18n-hint: (noun) The track that is used for MIDI notes which can be dragged to change their duration.*/ @@ -2778,7 +2774,7 @@ void TrackPanel::ResetFreqSelectionPin(double hintFrequency, bool logF) void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack) { - if (mStretched) { // Undo stretch and redo it with NEW mouse coordinates + if (mStretchState.mStretched) { // Undo stretch and redo it with NEW mouse coordinates // Drag handling was not originally implemented with Undo in mind -- // there are saved pointers to tracks that are not supposed to change. // Undo will change tracks, so convert pTrack, mCapturedTrack to index @@ -2793,9 +2789,10 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, // Undo brings us back to the pre-click state, but we want to // quantize selected region to integer beat boundaries. These // were saved in mStretchSel[12] variables: - mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1); + mViewInfo->selectedRegion.setTimes + (mStretchState.mSel0, mStretchState.mSel1); - mStretched = false; + mStretchState.mStretched = false; int index = 0; for (Track *t = iter.First(GetTracks()); t; t = iter.Next()) { if (index == pTrackIndex) pTrack = t; @@ -2822,16 +2819,16 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, pNt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); // We could be moving 3 things: left edge, right edge, a point between - switch (mStretchMode) { + switch (mStretchState.mMode) { case stretchLeft: { // make sure target duration is not too short double dur = mViewInfo->selectedRegion.t1() - moveto; - if (dur < mStretchRightBeats * minPeriod) { - dur = mStretchRightBeats * minPeriod; + if (dur < mStretchState.mRightBeats * minPeriod) { + dur = mStretchState.mRightBeats * minPeriod; moveto = mViewInfo->selectedRegion.t1() - dur; } - if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) { - pNt->SetOffset(pNt->GetOffset() + moveto - mStretchSel0); + if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { + pNt->SetOffset(pNt->GetOffset() + moveto - mStretchState.mSel0); mViewInfo->selectedRegion.setT0(moveto); } break; @@ -2839,11 +2836,11 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, case stretchRight: { // make sure target duration is not too short double dur = moveto - mViewInfo->selectedRegion.t0(); - if (dur < mStretchLeftBeats * minPeriod) { - dur = mStretchLeftBeats * minPeriod; - moveto = mStretchSel0 + dur; + if (dur < mStretchState.mLeftBeats * minPeriod) { + dur = mStretchState.mLeftBeats * minPeriod; + moveto = mStretchState.mSel0 + dur; } - if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) { + if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { mViewInfo->selectedRegion.setT1(moveto); } break; @@ -2854,16 +2851,16 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, double right_dur = mViewInfo->selectedRegion.t1() - moveto; double centerBeat; pNt->NearestBeatTime(mSelStart, ¢erBeat); - if (left_dur < mStretchLeftBeats * minPeriod) { - left_dur = mStretchLeftBeats * minPeriod; - moveto = mStretchSel0 + left_dur; + if (left_dur < mStretchState.mLeftBeats * minPeriod) { + left_dur = mStretchState.mLeftBeats * minPeriod; + moveto = mStretchState.mSel0 + left_dur; } - if (right_dur < mStretchRightBeats * minPeriod) { - right_dur = mStretchRightBeats * minPeriod; - moveto = mStretchSel1 - right_dur; + if (right_dur < mStretchState.mRightBeats * minPeriod) { + right_dur = mStretchState.mRightBeats * minPeriod; + moveto = mStretchState.mSel1 - right_dur; } - pNt->StretchRegion(mStretchStart, mStretchSel1, right_dur); - pNt->StretchRegion(mStretchSel0, mStretchStart, left_dur); + pNt->StretchRegion(mStretchState.mStart, mStretchState.mSel1, right_dur); + pNt->StretchRegion(mStretchState.mSel0, mStretchState.mStart, left_dur); break; } default: @@ -2872,7 +2869,7 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, } MakeParentPushState(_("Stretch Note Track"), _("Stretch"), UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE); - mStretched = true; + mStretchState.mStretched = true; Refresh(false); } #endif @@ -2920,7 +2917,7 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack) // Abandon this drag if selecting < 5 pixels. if (wxLongLong(SelStart-x).Abs() < minimumSizedSelection #ifdef USE_MIDI // limiting selection size is good, and not starting - && !mStretching // stretch unless mouse moves 5 pixels is good, but + && !mStretchState.mStretching // stretch unless mouse moves 5 pixels is good, but #endif // once stretching starts, it's ok to move even 1 pixel ) return; @@ -2933,7 +2930,7 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack) SelectRangeOfTracks(sTrack, eTrack); #ifdef USE_MIDI - if (mStretching) { + if (mStretchState.mStretching) { // the following is also in ExtendSelection, called below // probably a good idea to "hoist" the code to before this "if" stmt if (clickedTrack == NULL && mCapturedTrack != NULL) diff --git a/src/TrackPanel.h b/src/TrackPanel.h index d786f03e3..ac798c96f 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -324,15 +324,18 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { stretchCenter, stretchRight }; - StretchEnum mStretchMode; // remembers what to drag - bool mStretching; // true between mouse down and mouse up - bool mStretched; // true after drag has pushed state - double mStretchStart; // time of initial mouse position, quantized - // to the nearest beat - double mStretchSel0; // initial sel0 (left) quantized to nearest beat - double mStretchSel1; // initial sel1 (left) quantized to nearest beat - double mStretchLeftBeats; // how many beats from left to cursor - double mStretchRightBeats; // how many beats from cursor to right + struct StretchState { + StretchEnum mMode { stretchCenter }; // remembers what to drag + bool mStretching {}; // true between mouse down and mouse up + bool mStretched {}; // true after drag has pushed state + double mStart {}; // time of initial mouse position, quantized + // to the nearest beat + double mSel0 {}; // initial sel0 (left) quantized to nearest beat + double mSel1 {}; // initial sel1 (left) quantized to nearest beat + double mLeftBeats {}; // how many beats from left to cursor + double mRightBeats {}; // how many beats from cursor to right + } mStretchState; + virtual bool HitTestStretch(Track *track, const wxRect &rect, const wxMouseEvent & event); virtual void Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack); #endif From 9fb7185ea4f995307efe1f5dda548803b69e7b6f Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Mon, 22 May 2017 13:13:21 -0400 Subject: [PATCH 2/6] Rewrite undo handling for Stretch to be more like other drags... ... that is, push a state only at button-up. The undo during each drag increment didn't actually happen, because of a check in OnUndo whether there is a drag in progress! But this is not necessary to fix the excessive number of undo items, because commit 63ae687bafd1123bf4bf7d00070e1dd9f38dc8a2 already did that by other means. It only removes an unusual usage. --- src/TrackPanel.cpp | 71 ++++++++++++++-------------------------------- src/TrackPanel.h | 1 - 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 09b30bfea..0b46995d1 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -1837,20 +1837,28 @@ void TrackPanel::HandleSelect(wxMouseEvent & event) SelectionHandleClick(event, t, rect); } else if (event.LeftUp() || event.RightUp()) { mSnapManager.reset(); - // Do not draw yellow lines - if (mSnapLeft != -1 || mSnapRight != -1) { - mSnapLeft = mSnapRight = -1; + if ( mStretchState.mStretching ) { + MakeParentPushState(_("Stretch Note Track"), _("Stretch"), + UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE); Refresh(false); + SetCapturedTrack( NULL ); } + else { + // Do not draw yellow lines + if (mSnapLeft != -1 || mSnapRight != -1) { + mSnapLeft = mSnapRight = -1; + Refresh(false); + } - SetCapturedTrack( NULL ); - //Send the NEW selection state to the undo/redo stack: - MakeParentModifyState(false); + SetCapturedTrack( NULL ); + //Send the NEW selection state to the undo/redo stack: + MakeParentModifyState(false); #ifdef EXPERIMENTAL_SPECTRAL_EDITING - // This stops center snapping with mouse movement - mFreqSelMode = FREQ_SEL_INVALID; + // This stops center snapping with mouse movement + mFreqSelMode = FREQ_SEL_INVALID; #endif + } } else if (event.LeftDClick() && !event.ShiftDown()) { if (!mCapturedTrack) { @@ -2284,18 +2292,13 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, mStretchState.mLeftBeats = qBeat1 - qBeat0; mStretchState.mRightBeats = 0; } - mViewInfo->selectedRegion.setTimes(mStretchState.mSel0, mStretchState.mSel1); - mStretchState.mStretching = true; - mStretchState.mStretched = false; - /* i18n-hint: (noun) The track that is used for MIDI notes which can be - dragged to change their duration.*/ - MakeParentPushState(_("Stretch Note Track"), - /* i18n-hint: In the history list, indicates a MIDI note has - been dragged to change its duration (stretch it). Using either past - or present tense is fine here. If unsure, go for whichever is - shorter.*/ - _("Stretch")); + // Do this before we change the selection + MakeParentModifyState( false ); + + mViewInfo->selectedRegion.setTimes + (mStretchState.mSel0, mStretchState.mSel1); + mStretchState.mStretching = true; // Full refresh since the label area may need to indicate // newly selected tracks. (I'm really not sure if the label area @@ -2774,33 +2777,6 @@ void TrackPanel::ResetFreqSelectionPin(double hintFrequency, bool logF) void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack) { - if (mStretchState.mStretched) { // Undo stretch and redo it with NEW mouse coordinates - // Drag handling was not originally implemented with Undo in mind -- - // there are saved pointers to tracks that are not supposed to change. - // Undo will change tracks, so convert pTrack, mCapturedTrack to index - // values, then look them up after the Undo - TrackListIterator iter(GetTracks()); - int pTrackIndex = pTrack->GetIndex(); - int capturedTrackIndex = - (mCapturedTrack ? mCapturedTrack->GetIndex() : 0); - - GetProject()->OnUndo(); - - // Undo brings us back to the pre-click state, but we want to - // quantize selected region to integer beat boundaries. These - // were saved in mStretchSel[12] variables: - mViewInfo->selectedRegion.setTimes - (mStretchState.mSel0, mStretchState.mSel1); - - mStretchState.mStretched = false; - int index = 0; - for (Track *t = iter.First(GetTracks()); t; t = iter.Next()) { - if (index == pTrackIndex) pTrack = t; - if (mCapturedTrack && index == capturedTrackIndex) mCapturedTrack = t; - index++; - } - } - if (pTrack == NULL && mCapturedTrack != NULL) pTrack = mCapturedTrack; @@ -2867,9 +2843,6 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, wxASSERT(false); break; } - MakeParentPushState(_("Stretch Note Track"), _("Stretch"), - UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE); - mStretchState.mStretched = true; Refresh(false); } #endif diff --git a/src/TrackPanel.h b/src/TrackPanel.h index ac798c96f..e006789a6 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -327,7 +327,6 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { struct StretchState { StretchEnum mMode { stretchCenter }; // remembers what to drag bool mStretching {}; // true between mouse down and mouse up - bool mStretched {}; // true after drag has pushed state double mStart {}; // time of initial mouse position, quantized // to the nearest beat double mSel0 {}; // initial sel0 (left) quantized to nearest beat From 90eb4ec142f7d575d0870dd9c755589bff520cbe Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Thu, 11 May 2017 13:41:26 -0400 Subject: [PATCH 3/6] Make MIDI track stretch path-independent --- src/NoteTrack.cpp | 27 +++++----- src/NoteTrack.h | 8 ++- src/TrackPanel.cpp | 126 +++++++++++++++++++++++++-------------------- src/TrackPanel.h | 10 ++-- 4 files changed, 95 insertions(+), 76 deletions(-) diff --git a/src/NoteTrack.cpp b/src/NoteTrack.cpp index e1597ab19..ca14c956d 100644 --- a/src/NoteTrack.cpp +++ b/src/NoteTrack.cpp @@ -562,29 +562,30 @@ bool NoteTrack::Shift(double t) // t is always seconds return true; } -double NoteTrack::NearestBeatTime(double time, double *beat) const +QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const { wxASSERT(mSeq); // Alg_seq knows nothing about offset, so remove offset time double seq_time = time - GetOffset(); - seq_time = mSeq->nearest_beat_time(seq_time, beat); + double beat; + seq_time = mSeq->nearest_beat_time(seq_time, &beat); // add the offset back in to get "actual" audacity track time - return seq_time + GetOffset(); + return { seq_time + GetOffset(), beat }; } -bool NoteTrack::StretchRegion(double t0, double t1, double dur) +bool NoteTrack::StretchRegion + ( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur ) { - wxASSERT(mSeq); - // Alg_seq::stretch_region uses beats, so we translate time - // to beats first: - t0 -= GetOffset(); - t1 -= GetOffset(); - double b0 = mSeq->get_time_map()->time_to_beat(t0); - double b1 = mSeq->get_time_map()->time_to_beat(t1); - bool result = mSeq->stretch_region(b0, b1, dur); + bool result = mSeq->stretch_region( t0.second, t1.second, newDur ); if (result) { + const auto oldDur = t1.first - t0.first; +#if 0 + // PRL: Would this be better ? + mSeq->set_real_dur(mSeq->get_real_dur() + newDur - oldDur); +#else mSeq->convert_to_seconds(); - mSeq->set_dur(mSeq->get_dur() + dur - (t1 - t0)); + mSeq->set_dur(mSeq->get_dur() + newDur - oldDur); +#endif } return result; } diff --git a/src/NoteTrack.h b/src/NoteTrack.h index 6097c4b5d..b02e97239 100644 --- a/src/NoteTrack.h +++ b/src/NoteTrack.h @@ -11,6 +11,7 @@ #ifndef __AUDACITY_NOTETRACK__ #define __AUDACITY_NOTETRACK__ +#include #include #include "Audacity.h" #include "Experimental.h" @@ -58,6 +59,8 @@ using NoteTrackBase = #endif ; +using QuantizedTimeAndBeat = std::pair< double, double >; + class AUDACITY_DLL_API NoteTrack final : public NoteTrackBase { @@ -112,8 +115,9 @@ class AUDACITY_DLL_API NoteTrack final void SetVelocity(float velocity) { mVelocity = velocity; } #endif - double NearestBeatTime(double time, double *beat) const; - bool StretchRegion(double b0, double b1, double dur); + QuantizedTimeAndBeat NearestBeatTime( double time ) const; + bool StretchRegion + ( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur ); int GetBottomNote() const { return mBottomNote; } int GetPitchHeight() const { return mPitchHeight; } diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 0b46995d1..e57110578 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -2238,22 +2238,29 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, const auto nt = static_cast(pTrack); // find nearest beat to sel0, sel1 double minPeriod = 0.05; // minimum beat period - double qBeat0, qBeat1; - double centerBeat = 0.0f; - mStretchState.mSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); - mStretchState.mSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); + mStretchState.mBeatCenter = { 0, 0 }; + + mStretchState.mBeat0 = + nt->NearestBeatTime( mViewInfo->selectedRegion.t0() ); + mStretchState.mBeat1 = + nt->NearestBeatTime( mViewInfo->selectedRegion.t1() ); // If there is not (almost) a beat to stretch that is slower // than 20 beats per second, don't stretch - if (within(qBeat0, qBeat1, 0.9) || - (mStretchState.mSel1 - mStretchState.mSel0) / (qBeat1 - qBeat0) < minPeriod) return; + if ( within( mStretchState.mBeat0.second, + mStretchState.mBeat1.second, 0.9 ) || + ( mStretchState.mBeat1.first - mStretchState.mBeat0.first ) / + ( mStretchState.mBeat1.second - mStretchState.mBeat0.second ) + < minPeriod ) + return; if (startNewSelection) { // mouse is not at an edge, but after // quantization, we could be indicating the selection edge mSelStartValid = true; mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x)); - mStretchState.mStart = nt->NearestBeatTime(mSelStart, ¢erBeat); - if (within(qBeat0, centerBeat, 0.1)) { + mStretchState.mBeatCenter = nt->NearestBeatTime( mSelStart ); + if ( within( mStretchState.mBeat0.second, + mStretchState.mBeatCenter.second, 0.1 ) ) { mListener->TP_DisplayStatusMessage( _("Click and drag to stretch selected region.")); SetCursor(*mStretchLeftCursor); @@ -2261,7 +2268,9 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, mSelStart = mViewInfo->selectedRegion.t1(); // condition that implies stretchLeft startNewSelection = false; - } else if (within(qBeat1, centerBeat, 0.1)) { + } + else if ( within( mStretchState.mBeat1.second, + mStretchState.mBeatCenter.second, 0.1 ) ) { mListener->TP_DisplayStatusMessage( _("Click and drag to stretch selected region.")); SetCursor(*mStretchRightCursor); @@ -2274,22 +2283,28 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, if (startNewSelection) { mStretchState.mMode = stretchCenter; - mStretchState.mLeftBeats = qBeat1 - centerBeat; - mStretchState.mRightBeats = centerBeat - qBeat0; - } else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) { + mStretchState.mLeftBeats = + mStretchState.mBeat1.second - mStretchState.mBeatCenter.second; + mStretchState.mRightBeats = + mStretchState.mBeatCenter.second - mStretchState.mBeat0.second; + } + else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) { // note that at this point, mSelStart is at the opposite // end of the selection from the cursor. If the cursor is // over sel0, then mSelStart is at sel1. mStretchState.mMode = stretchLeft; - } else { + } + else { mStretchState.mMode = stretchRight; } if (mStretchState.mMode == stretchLeft) { mStretchState.mLeftBeats = 0; - mStretchState.mRightBeats = qBeat1 - qBeat0; + mStretchState.mRightBeats = + mStretchState.mBeat1.second - mStretchState.mBeat0.second; } else if (mStretchState.mMode == stretchRight) { - mStretchState.mLeftBeats = qBeat1 - qBeat0; + mStretchState.mLeftBeats = + mStretchState.mBeat1.second - mStretchState.mBeat0.second; mStretchState.mRightBeats = 0; } @@ -2297,7 +2312,7 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, MakeParentModifyState( false ); mViewInfo->selectedRegion.setTimes - (mStretchState.mSel0, mStretchState.mSel1); + ( mStretchState.mBeat0.first, mStretchState.mBeat1.first ); mStretchState.mStretching = true; // Full refresh since the label area may need to indicate @@ -2784,65 +2799,62 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, return; } - NoteTrack *pNt = (NoteTrack *) pTrack; - double moveto = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge)); + NoteTrack *pNt = static_cast< NoteTrack * >( pTrack ); + + double moveto = + std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge)); + + auto t1 = mViewInfo->selectedRegion.t1(); + auto t0 = mViewInfo->selectedRegion.t0(); + double dur, left_dur, right_dur; // check to make sure tempo is not higher than 20 beats per second // (In principle, tempo can be higher, but not infinity.) - double minPeriod = 0.05; // minimum beat period - double qBeat0, qBeat1; - pNt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); // get beat - pNt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); + const double minPeriod = 0.05; // minimum beat period - // We could be moving 3 things: left edge, right edge, a point between + // make sure target duration is not too short + // Take quick exit if so, without changing the selection. switch (mStretchState.mMode) { case stretchLeft: { - // make sure target duration is not too short - double dur = mViewInfo->selectedRegion.t1() - moveto; - if (dur < mStretchState.mRightBeats * minPeriod) { - dur = mStretchState.mRightBeats * minPeriod; - moveto = mViewInfo->selectedRegion.t1() - dur; - } - if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { - pNt->SetOffset(pNt->GetOffset() + moveto - mStretchState.mSel0); - mViewInfo->selectedRegion.setT0(moveto); - } + dur = t1 - moveto; + if (dur < mStretchState.mRightBeats * minPeriod) + return; + pNt->StretchRegion + ( mStretchState.mBeat0, mStretchState.mBeat1, dur ); + pNt->Offset( moveto - t0 ); + mStretchState.mBeat0.first = moveto; + mViewInfo->selectedRegion.setT0(moveto); break; } case stretchRight: { - // make sure target duration is not too short - double dur = moveto - mViewInfo->selectedRegion.t0(); - if (dur < mStretchState.mLeftBeats * minPeriod) { - dur = mStretchState.mLeftBeats * minPeriod; - moveto = mStretchState.mSel0 + dur; - } - if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { - mViewInfo->selectedRegion.setT1(moveto); - } + dur = moveto - t0; + if (dur < mStretchState.mLeftBeats * minPeriod) + return; + pNt->StretchRegion + ( mStretchState.mBeat0, mStretchState.mBeat1, dur ); + mViewInfo->selectedRegion.setT1(moveto); + mStretchState.mBeat1.first = moveto; break; } case stretchCenter: { - // make sure both left and right target durations are not too short - double left_dur = moveto - mViewInfo->selectedRegion.t0(); - double right_dur = mViewInfo->selectedRegion.t1() - moveto; - double centerBeat; - pNt->NearestBeatTime(mSelStart, ¢erBeat); - if (left_dur < mStretchState.mLeftBeats * minPeriod) { - left_dur = mStretchState.mLeftBeats * minPeriod; - moveto = mStretchState.mSel0 + left_dur; - } - if (right_dur < mStretchState.mRightBeats * minPeriod) { - right_dur = mStretchState.mRightBeats * minPeriod; - moveto = mStretchState.mSel1 - right_dur; - } - pNt->StretchRegion(mStretchState.mStart, mStretchState.mSel1, right_dur); - pNt->StretchRegion(mStretchState.mSel0, mStretchState.mStart, left_dur); + left_dur = moveto - t0; + right_dur = t1 - moveto; + + if (left_dur < mStretchState.mLeftBeats * minPeriod || + right_dur < mStretchState.mRightBeats * minPeriod) + return; + pNt->StretchRegion + ( mStretchState.mBeatCenter, mStretchState.mBeat1, right_dur ); + pNt->StretchRegion + ( mStretchState.mBeat0, mStretchState.mBeatCenter, left_dur ); + mStretchState.mBeatCenter.first = moveto; break; } default: wxASSERT(false); break; } + Refresh(false); } #endif diff --git a/src/TrackPanel.h b/src/TrackPanel.h index e006789a6..89304d25f 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -326,11 +326,13 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { }; struct StretchState { StretchEnum mMode { stretchCenter }; // remembers what to drag + + using QuantizedTimeAndBeat = std::pair< double, double >; + bool mStretching {}; // true between mouse down and mouse up - double mStart {}; // time of initial mouse position, quantized - // to the nearest beat - double mSel0 {}; // initial sel0 (left) quantized to nearest beat - double mSel1 {}; // initial sel1 (left) quantized to nearest beat + QuantizedTimeAndBeat mBeatCenter { 0, 0 }; + QuantizedTimeAndBeat mBeat0 { 0, 0 }; + QuantizedTimeAndBeat mBeat1 { 0, 0 }; double mLeftBeats {}; // how many beats from left to cursor double mRightBeats {}; // how many beats from cursor to right } mStretchState; From 82ce041c753ba7be301ca5b1cfe6207b669c6b2c Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Thu, 11 May 2017 14:53:03 -0400 Subject: [PATCH 4/6] Preview cursor for Stretch can take left, right, or center form --- src/TrackPanel.cpp | 109 +++++++++++++++++++++++++++++---------------- src/TrackPanel.h | 9 +++- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index e57110578..6b4759948 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -1359,7 +1359,9 @@ bool TrackPanel::SetCursorByActivity( ) return true; #ifdef USE_MIDI case IsStretching: - SetCursor( unsafe ? *mDisabledCursor : *mStretchCursor); + SetCursor( unsafe + ? *mDisabledCursor + : *ChooseStretchCursor( mStretchState.mMode ) ); return true; #endif default: @@ -1627,13 +1629,14 @@ void TrackPanel::SetCursorAndTipWhenSelectTool( Track * t, switch( boundary) { case SBNone: case SBLeft: - case SBRight: - if ( HitTestStretch(t, rect, event)) { + case SBRight: { + if ( auto stretchMode = HitTestStretch( t, rect, event ) ) { tip = _("Click and drag to stretch within selected region."); - *ppCursor = mStretchCursor.get(); + *ppCursor = ChooseStretchCursor( stretchMode ); return; } break; + } default: break; } @@ -2070,7 +2073,7 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, #ifdef USE_MIDI mStretchState = StretchState{}; - bool stretch = HitTestStretch(pTrack, rect, event); + auto stretch = HitTestStretch( pTrack, rect, event, &mStretchState ); #endif bool bShiftDown = event.ShiftDown(); @@ -2238,12 +2241,6 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, const auto nt = static_cast(pTrack); // find nearest beat to sel0, sel1 double minPeriod = 0.05; // minimum beat period - mStretchState.mBeatCenter = { 0, 0 }; - - mStretchState.mBeat0 = - nt->NearestBeatTime( mViewInfo->selectedRegion.t0() ); - mStretchState.mBeat1 = - nt->NearestBeatTime( mViewInfo->selectedRegion.t1() ); // If there is not (almost) a beat to stretch that is slower // than 20 beats per second, don't stretch @@ -2256,49 +2253,30 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event, if (startNewSelection) { // mouse is not at an edge, but after // quantization, we could be indicating the selection edge - mSelStartValid = true; - mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x)); - mStretchState.mBeatCenter = nt->NearestBeatTime( mSelStart ); - if ( within( mStretchState.mBeat0.second, - mStretchState.mBeatCenter.second, 0.1 ) ) { + if ( stretch == stretchLeft ) { mListener->TP_DisplayStatusMessage( _("Click and drag to stretch selected region.")); SetCursor(*mStretchLeftCursor); // mStretchMode = stretchLeft; - mSelStart = mViewInfo->selectedRegion.t1(); - // condition that implies stretchLeft startNewSelection = false; } - else if ( within( mStretchState.mBeat1.second, - mStretchState.mBeatCenter.second, 0.1 ) ) { + else if ( stretchRight ) { mListener->TP_DisplayStatusMessage( _("Click and drag to stretch selected region.")); SetCursor(*mStretchRightCursor); // mStretchMode = stretchRight; - mSelStart = mViewInfo->selectedRegion.t0(); - // condition that implies stretchRight startNewSelection = false; } } - if (startNewSelection) { - mStretchState.mMode = stretchCenter; + mStretchState.mMode = stretch; + if ( mStretchState.mMode == stretchCenter ) { mStretchState.mLeftBeats = mStretchState.mBeat1.second - mStretchState.mBeatCenter.second; mStretchState.mRightBeats = mStretchState.mBeatCenter.second - mStretchState.mBeat0.second; } - else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) { - // note that at this point, mSelStart is at the opposite - // end of the selection from the cursor. If the cursor is - // over sel0, then mSelStart is at sel1. - mStretchState.mMode = stretchLeft; - } - else { - mStretchState.mMode = stretchRight; - } - - if (mStretchState.mMode == stretchLeft) { + else if (mStretchState.mMode == stretchLeft) { mStretchState.mLeftBeats = 0; mStretchState.mRightBeats = mStretchState.mBeat1.second - mStretchState.mBeat0.second; @@ -2789,6 +2767,50 @@ void TrackPanel::ResetFreqSelectionPin(double hintFrequency, bool logF) #endif #ifdef USE_MIDI + +wxCursor *TrackPanel::ChooseStretchCursor( StretchEnum mode ) +{ + switch ( mode ) { + case stretchCenter: return mStretchCursor.get(); + case stretchLeft: return mStretchLeftCursor.get(); + case stretchRight: return mStretchRightCursor.get(); + default: return nullptr; + } +} + +auto TrackPanel::ChooseStretchMode + ( const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo, + const NoteTrack *nt, StretchState *pState ) -> StretchEnum +{ + // Assume x coordinate is in the selection and y is appropriate for stretch + // -- and then decide whether x is near enough to either edge or neither. + + Maybe< StretchState > state; + if ( !pState ) + state.create(), pState = state.get(); + + if ( nt ) { + pState->mBeat0 = + nt->NearestBeatTime( viewInfo.selectedRegion.t0() ); + pState->mBeat1 = + nt->NearestBeatTime( viewInfo.selectedRegion.t1() ); + + auto selStart = std::max(0.0, viewInfo.PositionToTime(event.m_x, rect.x)); + pState->mBeatCenter = nt->NearestBeatTime( selStart ); + + if ( within( pState->mBeat0.second, pState->mBeatCenter.second, 0.1 ) ) + return stretchLeft; + else if ( within( pState->mBeat1.second, pState->mBeatCenter.second, 0.1 ) ) + return stretchRight; + } + else { + pState->mBeat0 = pState->mBeat1 = pState->mBeatCenter = { 0, 0 }; + return stretchNone; + } + + return stretchCenter; +} + void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack) { @@ -6799,14 +6821,17 @@ int TrackPanel::DetermineToolToUse( ToolsToolBar * pTtb, const wxMouseEvent & ev #ifdef USE_MIDI -bool TrackPanel::HitTestStretch(Track *track, const wxRect &rect, const wxMouseEvent & event) +auto TrackPanel::HitTestStretch + ( const Track *track, const wxRect &rect, const wxMouseEvent & event, + StretchState *pState ) + -> StretchEnum { // later, we may want a different policy, but for now, stretch is // selected when the cursor is near the center of the track and // within the selection if (!track || !track->GetSelected() || track->GetKind() != Track::Note || IsUnsafe()) { - return false; + return stretchNone; } int center = rect.y + rect.height / 2; int distance = abs(event.m_y - center); @@ -6815,8 +6840,14 @@ bool TrackPanel::HitTestStretch(Track *track, const wxRect &rect, const wxMouseE wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), rect.x); // Something is wrong if right edge comes before left edge wxASSERT(!(rightSel < leftSel)); - return (leftSel <= event.m_x && event.m_x <= rightSel && - distance < yTolerance); + + if (leftSel <= event.m_x && event.m_x <= rightSel && + distance < yTolerance) + return ChooseStretchMode + ( event, rect, *mViewInfo, + static_cast< const NoteTrack * >( track ), pState ); + + return stretchNone; } #endif diff --git a/src/TrackPanel.h b/src/TrackPanel.h index 89304d25f..7b41fd4c9 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -320,6 +320,7 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { // part shrinks, keeping the leftmost and rightmost boundaries // fixed. enum StretchEnum { + stretchNone = 0, // false value! stretchLeft, stretchCenter, stretchRight @@ -337,7 +338,13 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { double mRightBeats {}; // how many beats from cursor to right } mStretchState; - virtual bool HitTestStretch(Track *track, const wxRect &rect, const wxMouseEvent & event); + virtual StretchEnum HitTestStretch + ( const Track *track, const wxRect &rect, const wxMouseEvent & event, + StretchState *pState = nullptr ); + wxCursor *ChooseStretchCursor( StretchEnum mode ); + static StretchEnum ChooseStretchMode + ( const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo, + const NoteTrack *nt, StretchState *pState = nullptr ); virtual void Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack); #endif From 54636c65fa02a1584e44f0db659f7d708950721f Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Thu, 11 May 2017 17:04:53 -0400 Subject: [PATCH 5/6] Allow NoteTrack to stretch left of zero --- src/TrackPanel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 6b4759948..f470be76c 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -2795,7 +2795,7 @@ auto TrackPanel::ChooseStretchMode pState->mBeat1 = nt->NearestBeatTime( viewInfo.selectedRegion.t1() ); - auto selStart = std::max(0.0, viewInfo.PositionToTime(event.m_x, rect.x)); + auto selStart = viewInfo.PositionToTime(event.m_x, rect.x); pState->mBeatCenter = nt->NearestBeatTime( selStart ); if ( within( pState->mBeat0.second, pState->mBeatCenter.second, 0.1 ) ) From 646c66f78fc7ef6e3b1120cc4102f982e96f4116 Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Fri, 12 May 2017 17:41:57 -0400 Subject: [PATCH 6/6] Sync lock adjustment for NoteTrack stretch... ... It applies only at button-up, not in a previewing fashion during the drag. Realizing that preview is more work than I want to do right now. --- src/TrackPanel.cpp | 33 ++++++++++++++++++++++++++++++++- src/TrackPanel.h | 2 ++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index f470be76c..1e663f134 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -1840,6 +1840,34 @@ void TrackPanel::HandleSelect(wxMouseEvent & event) SelectionHandleClick(event, t, rect); } else if (event.LeftUp() || event.RightUp()) { mSnapManager.reset(); + + bool left; + if ( GetProject()->IsSyncLocked() && + ( ( left = mStretchState.mMode == stretchLeft ) || + mStretchState.mMode == stretchRight ) ) { + auto pNt = static_cast< NoteTrack * >( mCapturedTrack ); + SyncLockedTracksIterator syncIter( GetTracks() ); + for ( auto track = syncIter.StartWith(pNt); track != nullptr; + track = syncIter.Next() ) { + if ( track != pNt ) { + if ( left ) { + auto origT0 = mStretchState.mOrigT0; + auto diff = mViewInfo->selectedRegion.t0() - origT0; + if ( diff > 0) + track->SyncLockAdjust( origT0 + diff, origT0 ); + else + track->SyncLockAdjust( origT0, origT0 - diff ); + track->Offset( diff ); + } + else { + auto origT1 = mStretchState.mOrigT1; + auto diff = mViewInfo->selectedRegion.t1() - origT1; + track->SyncLockAdjust( origT1, origT1 + diff ); + } + } + } + } + if ( mStretchState.mStretching ) { MakeParentPushState(_("Stretch Note Track"), _("Stretch"), UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE); @@ -2792,8 +2820,10 @@ auto TrackPanel::ChooseStretchMode if ( nt ) { pState->mBeat0 = nt->NearestBeatTime( viewInfo.selectedRegion.t0() ); + pState->mOrigT0 = pState->mBeat0.first; pState->mBeat1 = nt->NearestBeatTime( viewInfo.selectedRegion.t1() ); + pState->mOrigT1 = pState->mBeat1.first; auto selStart = viewInfo.PositionToTime(event.m_x, rect.x); pState->mBeatCenter = nt->NearestBeatTime( selStart ); @@ -2843,8 +2873,8 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, return; pNt->StretchRegion ( mStretchState.mBeat0, mStretchState.mBeat1, dur ); - pNt->Offset( moveto - t0 ); mStretchState.mBeat0.first = moveto; + pNt->Offset( moveto - t0 ); mViewInfo->selectedRegion.setT0(moveto); break; } @@ -2854,6 +2884,7 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge, return; pNt->StretchRegion ( mStretchState.mBeat0, mStretchState.mBeat1, dur ); + mViewInfo->selectedRegion.setT1(moveto); mStretchState.mBeat1.first = moveto; break; diff --git a/src/TrackPanel.h b/src/TrackPanel.h index 7b41fd4c9..c87809f9e 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -331,6 +331,8 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { using QuantizedTimeAndBeat = std::pair< double, double >; bool mStretching {}; // true between mouse down and mouse up + double mOrigT0 {}; + double mOrigT1 {}; QuantizedTimeAndBeat mBeatCenter { 0, 0 }; QuantizedTimeAndBeat mBeat0 { 0, 0 }; QuantizedTimeAndBeat mBeat1 { 0, 0 };