1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-11-14 17:14:07 +01:00

TrackPanel no longer implements the selection tool or MIDI stretch...

This one's big!

Also restores the effect of ctrl-click on label track.

Also adds ESC key handling for the Stretch.
This commit is contained in:
Paul Licameli
2015-09-08 21:15:35 -04:00
committed by Paul Licameli
parent efdb9889b1
commit 770b3b52ef
16 changed files with 2161 additions and 2087 deletions

View File

@@ -15,8 +15,12 @@ Paul Licameli split from TrackPanel.cpp
#include "LabelGlyphHandle.h"
#include "LabelTextHandle.h"
#include "../../ui/SelectHandle.h"
#include "../../../HitTestResult.h"
#include "../../../Project.h"
#include "../../../TrackPanelMouseEvent.h"
#include "../../../toolbars/ToolsToolBar.h"
HitTestResult LabelTrack::HitTest
(const TrackPanelMouseEvent &evt,
@@ -37,6 +41,12 @@ HitTestResult LabelTrack::HitTest
// Missed glyph, try text box
// This hit test does not define its own messages or cursor
HitTestResult defaultResult = Track::HitTest(evt, pProject);
if (!defaultResult.handle) {
// In case of multi tool, default to selection.
const ToolsToolBar *const pTtb = pProject->GetToolsToolBar();
if (pTtb->IsDown(multiTool))
defaultResult = SelectHandle::HitTest(evt, pProject, this);
}
result = LabelTextHandle::HitTest(event, this);
if (result.handle)
// Use any cursor or status message change from catchall,

View File

@@ -17,12 +17,49 @@ Paul Licameli split from TrackPanel.cpp
#include "NoteTrackVRulerControls.h"
#include "../../../../HitTestResult.h"
#include "../../../../Project.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../ui/SelectHandle.h"
#include "StretchHandle.h"
#include "../../../../toolbars/ToolsToolBar.h"
HitTestResult NoteTrack::HitTest
(const TrackPanelMouseEvent &event,
const AudacityProject *pProject)
{
return Track::HitTest(event, pProject);
const ToolsToolBar *const pTtb = pProject->GetToolsToolBar();
// If the cursor is in the hot zone for stretching, that takes precedence
// over selection, but it didn't take precedence over other hits.
// That was the old logic, and maybe I tried too hard to preserve it just so.
// PRL.
// Eligible for stretch?
HitTestResult result1;
#ifdef USE_MIDI
StretchHandle::StretchState state;
result1 = StretchHandle::HitTest( event, pProject, this, state );
#endif
// But some other non-select tool like zoom may take priority.
HitTestResult result = Track::HitTest(event, pProject);
if (result.preview.cursor &&
!(result1.preview.cursor && pTtb->GetCurrentTool() == selectTool))
return result;
if (pTtb->IsDown(multiTool)) {
// Default to selection
if (!result1.preview.cursor &&
NULL != (result =
SelectHandle::HitTest(event, pProject, this)).preview.cursor)
return result;
}
// Do stretch!
if (result1.preview.cursor)
return result1;
return result;
}
TrackControls *NoteTrack::GetControls()

View File

@@ -0,0 +1,339 @@
/**********************************************************************
Audacity: A Digital Audio Editor
StretchHandle.cpp
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#include "../../../../Audacity.h"
#ifdef USE_MIDI
#include "StretchHandle.h"
#include "../../../../HitTestResult.h"
#include "../../../../NoteTrack.h"
#include "../../../../Project.h"
#include "../../../../RefreshCode.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../UndoManager.h"
#include "../../../../../images/Cursors.h"
#include <algorithm>
StretchHandle::StretchHandle()
{
}
StretchHandle &StretchHandle::Instance()
{
static StretchHandle instance;
return instance;
}
HitTestPreview StretchHandle::HitPreview( StretchEnum stretchMode, bool unsafe )
{
static auto disabledCursor =
::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
static auto stretchLeftCursor =
::MakeCursor(wxCURSOR_BULLSEYE, StretchLeftCursorXpm, 16, 16);
static auto stretchRightCursor =
::MakeCursor(wxCURSOR_BULLSEYE, StretchRightCursorXpm, 16, 16);
static auto stretchCursor =
::MakeCursor(wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16);
if (unsafe) {
return { _(""), &*disabledCursor };
}
else {
wxCursor *pCursor = NULL;
switch (stretchMode) {
default:
wxASSERT(false);
case stretchLeft:
pCursor = &*stretchLeftCursor; break;
case stretchCenter:
pCursor = &*stretchCursor; break;
case stretchRight:
pCursor = &*stretchRightCursor; break;
}
return {
_("Click and drag to stretch selected region."),
pCursor
};
}
}
HitTestResult StretchHandle::HitTest
( const TrackPanelMouseEvent &evt, const AudacityProject *pProject,
NoteTrack *pTrack, StretchState &stretchState)
{
const wxMouseEvent &event = evt.event;
// 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
const ViewInfo &viewInfo = pProject->GetViewInfo();
const bool unsafe = pProject->IsAudioActive();
if (!pTrack || !pTrack->GetSelected() || pTrack->GetKind() != Track::Note)
return {};
const wxRect &rect = evt.rect;
int center = rect.y + rect.height / 2;
int distance = abs(event.m_y - center);
const int yTolerance = 10;
wxInt64 leftSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t0(), rect.x);
wxInt64 rightSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t1(), rect.x);
// Something is wrong if right edge comes before left edge
wxASSERT(!(rightSel < leftSel));
if (!(leftSel <= event.m_x && event.m_x <= rightSel &&
distance < yTolerance))
return {};
// find nearest beat to sel0, sel1
static const double minPeriod = 0.05; // minimum beat period
stretchState.mBeatCenter = { 0, 0 };
auto t0 = viewInfo.selectedRegion.t0();
stretchState.mBeat0 = pTrack->NearestBeatTime( t0 );
stretchState.mOrigSel0Quantized = stretchState.mBeat0.first;
auto t1 = viewInfo.selectedRegion.t1();
stretchState.mBeat1 = pTrack->NearestBeatTime( t1 );
stretchState.mOrigSel1Quantized = stretchState.mBeat1.first;
// If there is not (almost) a beat to stretch that is slower
// than 20 beats per second, don't stretch
if ( within( stretchState.mBeat0.second,
stretchState.mBeat1.second, 0.9 ) ||
( stretchState.mBeat1.first - stretchState.mBeat0.first ) /
( stretchState.mBeat1.second - stretchState.mBeat0.second )
< minPeriod )
return {};
auto selStart = viewInfo.PositionToTime( event.m_x, rect.x );
stretchState.mBeatCenter = pTrack->NearestBeatTime( selStart );
bool startNewSelection = true;
if ( within( stretchState.mBeat0.second,
stretchState.mBeatCenter.second, 0.1 ) ) {
stretchState.mMode = stretchLeft;
stretchState.mLeftBeats = 0;
stretchState.mRightBeats =
stretchState.mBeat1.second - stretchState.mBeat0.second;
}
else if ( within( stretchState.mBeat1.second,
stretchState.mBeatCenter.second, 0.1 ) ) {
stretchState.mMode = stretchRight;
stretchState.mLeftBeats =
stretchState.mBeat1.second - stretchState.mBeat0.second;
stretchState.mRightBeats = 0;
}
else {
stretchState.mMode = stretchCenter;
stretchState.mLeftBeats =
stretchState.mBeat1.second - stretchState.mBeatCenter.second;
stretchState.mRightBeats =
stretchState.mBeatCenter.second - stretchState.mBeat0.second;
}
return {
HitPreview( stretchState.mMode, unsafe ),
&Instance()
};
}
StretchHandle::~StretchHandle()
{
}
UIHandle::Result StretchHandle::Click
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
{
using namespace RefreshCode;
const bool unsafe = pProject->IsAudioActive();
const wxMouseEvent &event = evt.event;
if (unsafe ||
event.LeftDClick() ||
!event.LeftDown() ||
evt.pCell == NULL)
return Cancelled;
mLeftEdge = evt.rect.GetLeft();
mpTrack = static_cast<NoteTrack*>(evt.pCell);
ViewInfo &viewInfo = pProject->GetViewInfo();
// We must have hit if we got here, but repeat some
// calculations that set members
HitTest( evt, pProject, mpTrack, mStretchState );
viewInfo.selectedRegion.setTimes
( mStretchState.mBeat0.first, mStretchState.mBeat1.first );
// Full refresh since the label area may need to indicate
// newly selected tracks. (I'm really not sure if the label area
// needs to be refreshed or how to just refresh non-label areas.-RBD)
return RefreshAll | UpdateSelection;
}
UIHandle::Result StretchHandle::Drag
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
{
using namespace RefreshCode;
const bool unsafe = pProject->IsAudioActive();
if (unsafe) {
this->Cancel(pProject);
return RefreshAll | Cancelled;
}
const wxMouseEvent &event = evt.event;
const int x = event.m_x;
Track *clickedTrack =
static_cast<CommonTrackPanelCell*>(evt.pCell)->FindTrack();
if (clickedTrack == NULL && mpTrack != NULL)
clickedTrack = mpTrack;
Stretch(pProject, x, mLeftEdge, clickedTrack);
return RefreshAll;
}
HitTestPreview StretchHandle::Preview
(const TrackPanelMouseEvent &, const AudacityProject *pProject)
{
const bool unsafe = pProject->IsAudioActive();
return HitPreview( mStretchState.mMode, unsafe );
}
UIHandle::Result StretchHandle::Release
(const TrackPanelMouseEvent &, AudacityProject *pProject,
wxWindow *)
{
using namespace RefreshCode;
const bool unsafe = pProject->IsAudioActive();
if (unsafe) {
this->Cancel(pProject);
return RefreshAll | Cancelled;
}
bool left;
ViewInfo &viewInfo = pProject->GetViewInfo();
if ( pProject->IsSyncLocked() &&
( ( left = mStretchState.mMode == stretchLeft ) ||
mStretchState.mMode == stretchRight ) ) {
SyncLockedTracksIterator syncIter( pProject->GetTracks() );
for ( auto track = syncIter.StartWith( mpTrack ); track != nullptr;
track = syncIter.Next() ) {
if ( track != mpTrack ) {
if ( left ) {
auto origT0 = mStretchState.mOrigSel0Quantized;
auto diff = viewInfo.selectedRegion.t0() - origT0;
if ( diff > 0)
track->SyncLockAdjust( origT0 + diff, origT0 );
else
track->SyncLockAdjust( origT0, origT0 - diff );
track->Offset( diff );
}
else {
auto origT1 = mStretchState.mOrigSel1Quantized;
auto diff = viewInfo.selectedRegion.t1() - origT1;
track->SyncLockAdjust( origT1, origT1 + diff );
}
}
}
}
/* i18n-hint: (noun) The track that is used for MIDI notes which can be
dragged to change their duration.*/
pProject->PushState(_("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"),
UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE);
return RefreshAll;
}
UIHandle::Result StretchHandle::Cancel(AudacityProject *pProject)
{
pProject->RollbackState();
return RefreshCode::RefreshNone;
}
void StretchHandle::Stretch(AudacityProject *pProject, int mouseXCoordinate, int trackLeftEdge,
Track *pTrack)
{
ViewInfo &viewInfo = pProject->GetViewInfo();
TrackList *const trackList = pProject->GetTracks();
if (pTrack == NULL && mpTrack != NULL)
pTrack = mpTrack;
if (!pTrack || pTrack->GetKind() != Track::Note) {
return;
}
NoteTrack *pNt = static_cast<NoteTrack *>(pTrack);
double moveto =
std::max(0.0, viewInfo.PositionToTime(mouseXCoordinate, trackLeftEdge));
auto t1 = viewInfo.selectedRegion.t1();
auto t0 = viewInfo.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
// make sure target duration is not too short
// Take quick exit if so, without changing the selection.
switch ( mStretchState.mMode ) {
case stretchLeft: {
dur = t1 - moveto;
if (dur < mStretchState.mRightBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
pNt->Offset( moveto - t0 );
mStretchState.mBeat0.first = moveto;
viewInfo.selectedRegion.setT0(moveto);
break;
}
case stretchRight: {
dur = moveto - t0;
if (dur < mStretchState.mLeftBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
viewInfo.selectedRegion.setT1(moveto);
mStretchState.mBeat1.first = moveto;
viewInfo.selectedRegion.setT0(moveto);
break;
}
case stretchCenter: {
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;
}
}
#endif

View File

@@ -0,0 +1,98 @@
/**********************************************************************
Audacity: A Digital Audio Editor
StretchHandle.h
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#ifndef __AUDACITY_STRETCH_HANDLE__
#define __AUDACITY_STRETCH_HANDLE__
#include "../../../../UIHandle.h"
#include "../../../../MemoryX.h"
class Alg_seq;
struct HitTestResult;
class NoteTrack;
class Track;
class ViewInfo;
class wxCursor;
class StretchHandle : public UIHandle
{
public:
enum StretchEnum {
stretchNone = 0, // false value!
stretchLeft,
stretchCenter,
stretchRight
};
// Stretching applies to a selected region after quantizing the
// region to beat boundaries (subbeat stretching is not supported,
// but maybe it should be enabled with shift or ctrl or something)
// Stretching can drag the left boundary (the right stays fixed),
// the right boundary (the left stays fixed), or the center (splits
// the selection into two parts: when left part grows, the right
// part shrinks, keeping the leftmost and rightmost boundaries
// fixed.
struct StretchState {
StretchEnum mMode { stretchCenter }; // remembers what to drag
using QuantizedTimeAndBeat = std::pair< double, double >;
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
double mOrigSel0Quantized { -1 }, mOrigSel1Quantized { -1 };
};
private:
StretchHandle();
StretchHandle(const StretchHandle&);
StretchHandle &operator=(const StretchHandle&);
static StretchHandle& Instance();
static HitTestPreview HitPreview(StretchEnum stretchMode, bool unsafe);
public:
static HitTestResult HitTest
( const TrackPanelMouseEvent &event, const AudacityProject *pProject,
NoteTrack *pTrack, StretchState &state );
virtual ~StretchHandle();
virtual Result Click
(const TrackPanelMouseEvent &event, AudacityProject *pProject);
virtual Result Drag
(const TrackPanelMouseEvent &event, AudacityProject *pProject);
virtual HitTestPreview Preview
(const TrackPanelMouseEvent &event, const AudacityProject *pProject);
virtual Result Release
(const TrackPanelMouseEvent &event, AudacityProject *pProject,
wxWindow *pParent);
virtual Result Cancel(AudacityProject *pProject);
bool StopsOnKeystroke() override { return true; }
private:
void Stretch
(AudacityProject *pProject, int mouseXCoordinate, int trackLeftEdge, Track *pTrack);
NoteTrack *mpTrack{};
int mLeftEdge{ -1 };
StretchState mStretchState{};
};
#endif

View File

@@ -18,6 +18,7 @@ Paul Licameli split from TrackPanel.cpp
#include "../../../../toolbars/ToolsToolBar.h"
#include "CutlineHandle.h"
#include "../../../ui/SelectHandle.h"
#include "../../../ui/EnvelopeHandle.h"
#include "SampleHandle.h"
#include "../../../ui/TimeShiftHandle.h"
@@ -26,6 +27,11 @@ HitTestResult WaveTrack::HitTest
(const TrackPanelMouseEvent &event,
const AudacityProject *pProject)
{
// FIXME: Should similar logic apply to NoteTrack (#if defined(USE_MIDI)) ?
// From here on the order in which we hit test determines
// which tool takes priority in the rare cases where it
// could be more than one.
// This hit was always tested first no matter which tool:
HitTestResult result = CutlineHandle::HitTest(event.event, event.rect, pProject, this);
if (result.preview.cursor)
@@ -51,6 +57,10 @@ HitTestResult WaveTrack::HitTest
else if (NULL != (result =
SampleHandle::HitTest(event.event, event.rect, pProject, this)).preview.cursor)
;
else if (NULL != (result =
SelectHandle::HitTest(event, pProject, this)).preview.cursor)
// default of all other hit tests
;
}
return result;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
/**********************************************************************
Audacity: A Digital Audio Editor
SelectHandle.h
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#ifndef __AUDACITY_SELECT_HANDLE__
#define __AUDACITY_SELECT_HANDLE__
#include "../../UIHandle.h"
#include "../../SelectedRegion.h"
#include "../../MemoryX.h"
#include <vector>
#include <wx/event.h>
#include <wx/gdicmn.h>
struct HitTestResult;
class SelectionStateChanger;
class SnapManager;
class SpectrumAnalyst;
class Track;
class ViewInfo;
class WaveTrack;
class SelectHandle : public wxEvtHandler, public UIHandle
{
SelectHandle();
SelectHandle(const SelectHandle&);
SelectHandle &operator=(const SelectHandle&);
public:
static SelectHandle& Instance();
// This always hits, but details of the hit vary with mouse position and
// key state.
static HitTestResult HitTest
(const TrackPanelMouseEvent &event, const AudacityProject *pProject, const Track *pTrack);
virtual ~SelectHandle();
virtual Result Click
(const TrackPanelMouseEvent &event, AudacityProject *pProject);
virtual Result Drag
(const TrackPanelMouseEvent &event, AudacityProject *pProject);
virtual HitTestPreview Preview
(const TrackPanelMouseEvent &event, const AudacityProject *pProject);
virtual Result Release
(const TrackPanelMouseEvent &event, AudacityProject *pProject,
wxWindow *pParent);
virtual Result Cancel(AudacityProject*);
virtual void DrawExtras
(DrawingPass pass,
wxDC * dc, const wxRegion &updateRegion, const wxRect &panelRect);
void OnProjectChange(AudacityProject *pProject) override;
// Receives timer event notifications, to implement auto-scroll
void OnTimer(wxCommandEvent &event);
private:
void Connect(AudacityProject *pProject);
void Disconnect();
void StartSelection
(AudacityProject *pProject, int mouseXCoordinate, int trackLeftEdge);
void AdjustSelection
(ViewInfo &viewInfo, int mouseXCoordinate, int trackLeftEdge,
Track *pTrack);
void StartFreqSelection
(ViewInfo &viewInfo, int mouseYCoordinate, int trackTopEdge,
int trackHeight, Track *pTrack);
void AdjustFreqSelection
(ViewInfo &viewInfo, int mouseYCoordinate, int trackTopEdge,
int trackHeight);
void HandleCenterFrequencyClick
(const ViewInfo &viewInfo, bool shiftDown,
const WaveTrack *pTrack, double value);
void StartSnappingFreqSelection
(const ViewInfo &viewInfo, const WaveTrack *pTrack);
void MoveSnappingFreqSelection
(AudacityProject *pProject, ViewInfo &viewInfo, int mouseYCoordinate,
int trackTopEdge,
int trackHeight, Track *pTrack);
public:
// This is needed to implement a command assignable to keystrokes
void SnapCenterOnce
(ViewInfo &viewInfo, const WaveTrack *pTrack, bool up);
private:
//void ResetFreqSelectionPin
// (const ViewInfo &viewInfo, double hintFrequency, bool logF);
Track *mpTrack;
wxRect mRect;
SelectedRegion mInitialSelection;
// Handles snapping the selection boundaries or track boundaries to
// line up with existing tracks or labels. mSnapLeft and mSnapRight
// are the horizontal index of pixels to display user feedback
// guidelines so the user knows when such snapping is taking place.
std::unique_ptr<SnapManager> mSnapManager;
wxInt64 mSnapLeft;
wxInt64 mSnapRight;
bool mSelStartValid;
double mSelStart;
int mSelectionBoundary;
enum eFreqSelMode {
FREQ_SEL_INVALID,
FREQ_SEL_SNAPPING_CENTER,
FREQ_SEL_PINNED_CENTER,
FREQ_SEL_DRAG_CENTER,
FREQ_SEL_FREE,
FREQ_SEL_TOP_FREE,
FREQ_SEL_BOTTOM_FREE,
} mFreqSelMode;
const WaveTrack *mFreqSelTrack {};
// Following holds:
// the center for FREQ_SEL_PINNED_CENTER,
// the ratio of top to center (== center to bottom) for FREQ_SEL_DRAG_CENTER,
// a frequency boundary for FREQ_SEL_FREE, FREQ_SEL_TOP_FREE, or
// FREQ_SEL_BOTTOM_FREE,
// and is ignored otherwise.
double mFreqSelPin;
std::unique_ptr<SpectrumAnalyst> mFrequencySnapper;
int mMostRecentX, mMostRecentY;
bool mAutoScrolling;
AudacityProject *mConnectedProject;
std::unique_ptr<SelectionStateChanger> mSelectionStateChanger;
};
#endif

View File

@@ -17,6 +17,7 @@ Paul Licameli split from TrackPanel.cpp
#include "../../Project.h"
#include "../../toolbars/ToolsToolBar.h"
#include "../ui/SelectHandle.h"
#include "EnvelopeHandle.h"
#include "../playabletrack/wavetrack/ui/SampleHandle.h"
#include "ZoomHandle.h"
@@ -40,10 +41,10 @@ HitTestResult Track::HitTest
return ZoomHandle::HitAnywhere(event.event, pProject);
case slideTool:
return TimeShiftHandle::HitAnywhere(pProject);
case selectTool:
return SelectHandle::HitTest(event, pProject, this);
default:
// cases not yet implemented
// fallthru
;
}