2015-07-08 21:17:43 +00:00
|
|
|
/**********************************************************************
|
|
|
|
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
|
|
|
|
TimeShiftHandle.cpp
|
|
|
|
|
|
|
|
Paul Licameli split from TrackPanel.cpp
|
|
|
|
|
|
|
|
**********************************************************************/
|
|
|
|
|
2021-05-09 15:16:56 +00:00
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
#include "TimeShiftHandle.h"
|
2018-11-11 02:40:37 +00:00
|
|
|
|
2019-06-18 04:00:35 +00:00
|
|
|
#include "TrackView.h"
|
2015-07-08 21:17:43 +00:00
|
|
|
#include "../../AColor.h"
|
|
|
|
#include "../../HitTestResult.h"
|
2019-05-29 15:37:47 +00:00
|
|
|
#include "../../ProjectAudioIO.h"
|
2019-06-06 13:55:34 +00:00
|
|
|
#include "../../ProjectHistory.h"
|
2019-05-29 15:31:40 +00:00
|
|
|
#include "../../ProjectSettings.h"
|
2015-07-08 21:17:43 +00:00
|
|
|
#include "../../RefreshCode.h"
|
2020-09-09 16:44:46 +00:00
|
|
|
#include "../../Snap.h"
|
2020-09-16 16:51:28 +00:00
|
|
|
#include "../../Track.h"
|
2019-06-21 19:10:11 +00:00
|
|
|
#include "../../TrackArtist.h"
|
|
|
|
#include "../../TrackPanelDrawingContext.h"
|
2015-07-08 21:17:43 +00:00
|
|
|
#include "../../TrackPanelMouseEvent.h"
|
|
|
|
#include "../../UndoManager.h"
|
2019-04-28 10:49:47 +00:00
|
|
|
#include "../../ViewInfo.h"
|
2015-07-08 21:17:43 +00:00
|
|
|
#include "../../../images/Cursors.h"
|
|
|
|
|
Changed lifetime management of UIHandle objects, no singletons...
... Rather, construct them during hit tests (also capturing more state sooner
rather than at Click time, and adding some accessors for later use)
This also fixes bug 1677 by other means and avoids similar problems.
A cell may be implemented to re-use a previously hit handle object, not yet
clicked, in a later hit test, by remembering a weak pointer, but TrackPanel
holds the strong pointers that determine when the object is destroyed.
And the objects will surely be destroyed after drag-release, or ESC key.
For now they are also destroyed whenever not dragging, and hit-testing is
re-invoked; that will be changed later, so that the re-use mentioned above
becomes effective, but still they will be destroyed when the pointer moves
from one cell to another.
2017-07-05 20:45:55 +00:00
|
|
|
TimeShiftHandle::TimeShiftHandle
|
|
|
|
( const std::shared_ptr<Track> &pTrack, bool gripHit )
|
2020-09-12 19:48:53 +00:00
|
|
|
: mGripHit{ gripHit }
|
2017-06-22 13:11:29 +00:00
|
|
|
{
|
2020-09-12 19:48:53 +00:00
|
|
|
mClipMoveState.mCapturedTrack = pTrack;
|
2017-07-12 04:28:49 +00:00
|
|
|
}
|
|
|
|
|
2019-05-21 17:36:29 +00:00
|
|
|
void TimeShiftHandle::Enter(bool, AudacityProject *)
|
2017-07-12 04:28:49 +00:00
|
|
|
{
|
2017-06-22 13:11:29 +00:00
|
|
|
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
|
|
|
|
mChangeHighlight = RefreshCode::RefreshCell;
|
|
|
|
#endif
|
|
|
|
}
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
HitTestPreview TimeShiftHandle::HitPreview
|
2017-12-08 11:26:09 +00:00
|
|
|
(const AudacityProject *WXUNUSED(pProject), bool unsafe)
|
2015-07-08 21:17:43 +00:00
|
|
|
{
|
|
|
|
static auto disabledCursor =
|
|
|
|
::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
|
|
|
|
static auto slideCursor =
|
|
|
|
MakeCursor(wxCURSOR_SIZEWE, TimeCursorXpm, 16, 16);
|
2017-07-17 15:39:32 +00:00
|
|
|
// TODO: Should it say "track or clip" ? Non-wave tracks can move, or clips in a wave track.
|
|
|
|
// TODO: mention effects of shift (move all clips of selected wave track) and ctrl (move vertically only) ?
|
|
|
|
// -- but not all of that is available in multi tool.
|
2019-12-08 21:24:20 +00:00
|
|
|
auto message = XO("Click and drag to move a track in time");
|
2017-07-17 15:39:32 +00:00
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
return {
|
2017-07-17 15:39:32 +00:00
|
|
|
message,
|
2015-07-08 21:17:43 +00:00
|
|
|
(unsafe
|
|
|
|
? &*disabledCursor
|
|
|
|
: &*slideCursor)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-07-04 04:48:09 +00:00
|
|
|
UIHandlePtr TimeShiftHandle::HitAnywhere
|
Changed lifetime management of UIHandle objects, no singletons...
... Rather, construct them during hit tests (also capturing more state sooner
rather than at Click time, and adding some accessors for later use)
This also fixes bug 1677 by other means and avoids similar problems.
A cell may be implemented to re-use a previously hit handle object, not yet
clicked, in a later hit test, by remembering a weak pointer, but TrackPanel
holds the strong pointers that determine when the object is destroyed.
And the objects will surely be destroyed after drag-release, or ESC key.
For now they are also destroyed whenever not dragging, and hit-testing is
re-invoked; that will be changed later, so that the re-use mentioned above
becomes effective, but still they will be destroyed when the pointer moves
from one cell to another.
2017-07-05 20:45:55 +00:00
|
|
|
(std::weak_ptr<TimeShiftHandle> &holder,
|
|
|
|
const std::shared_ptr<Track> &pTrack, bool gripHit)
|
2015-07-08 21:17:43 +00:00
|
|
|
{
|
Changed lifetime management of UIHandle objects, no singletons...
... Rather, construct them during hit tests (also capturing more state sooner
rather than at Click time, and adding some accessors for later use)
This also fixes bug 1677 by other means and avoids similar problems.
A cell may be implemented to re-use a previously hit handle object, not yet
clicked, in a later hit test, by remembering a weak pointer, but TrackPanel
holds the strong pointers that determine when the object is destroyed.
And the objects will surely be destroyed after drag-release, or ESC key.
For now they are also destroyed whenever not dragging, and hit-testing is
re-invoked; that will be changed later, so that the re-use mentioned above
becomes effective, but still they will be destroyed when the pointer moves
from one cell to another.
2017-07-05 20:45:55 +00:00
|
|
|
auto result = std::make_shared<TimeShiftHandle>( pTrack, gripHit );
|
|
|
|
result = AssignUIHandlePtr(holder, result);
|
2017-07-04 04:48:09 +00:00
|
|
|
return result;
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
|
2017-07-04 04:48:09 +00:00
|
|
|
UIHandlePtr TimeShiftHandle::HitTest
|
Changed lifetime management of UIHandle objects, no singletons...
... Rather, construct them during hit tests (also capturing more state sooner
rather than at Click time, and adding some accessors for later use)
This also fixes bug 1677 by other means and avoids similar problems.
A cell may be implemented to re-use a previously hit handle object, not yet
clicked, in a later hit test, by remembering a weak pointer, but TrackPanel
holds the strong pointers that determine when the object is destroyed.
And the objects will surely be destroyed after drag-release, or ESC key.
For now they are also destroyed whenever not dragging, and hit-testing is
re-invoked; that will be changed later, so that the re-use mentioned above
becomes effective, but still they will be destroyed when the pointer moves
from one cell to another.
2017-07-05 20:45:55 +00:00
|
|
|
(std::weak_ptr<TimeShiftHandle> &holder,
|
|
|
|
const wxMouseState &state, const wxRect &rect,
|
|
|
|
const std::shared_ptr<Track> &pTrack)
|
2015-07-08 21:17:43 +00:00
|
|
|
{
|
|
|
|
/// method that tells us if the mouse event landed on a
|
|
|
|
/// time-slider that allows us to time shift the sequence.
|
|
|
|
/// (Those are the two "grips" drawn at left and right edges for multi tool mode.)
|
|
|
|
|
|
|
|
// Perhaps we should delegate this to TrackArtist as only TrackArtist
|
|
|
|
// knows what the real sizes are??
|
|
|
|
|
|
|
|
// The drag Handle width includes border, width and a little extra margin.
|
|
|
|
const int adjustedDragHandleWidth = 14;
|
|
|
|
// The hotspot for the cursor isn't at its centre. Adjust for this.
|
|
|
|
const int hotspotOffset = 5;
|
|
|
|
|
|
|
|
// We are doing an approximate test here - is the mouse in the right or left border?
|
2017-06-29 03:21:20 +00:00
|
|
|
if (!(state.m_x + hotspotOffset < rect.x + adjustedDragHandleWidth ||
|
|
|
|
state.m_x + hotspotOffset >= rect.x + rect.width - adjustedDragHandleWidth))
|
2015-07-08 21:17:43 +00:00
|
|
|
return {};
|
|
|
|
|
2017-07-04 04:48:09 +00:00
|
|
|
return HitAnywhere( holder, pTrack, true );
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TimeShiftHandle::~TimeShiftHandle()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2020-09-12 22:57:59 +00:00
|
|
|
void ClipMoveState::DoHorizontalOffset( double offset )
|
|
|
|
{
|
|
|
|
if ( !shifters.empty() ) {
|
|
|
|
for ( auto &pair : shifters )
|
|
|
|
pair.second->DoHorizontalOffset( offset );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
for (auto channel : TrackList::Channels( mCapturedTrack.get() ))
|
|
|
|
channel->Offset( offset );
|
2018-09-22 18:36:52 +00:00
|
|
|
}
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
|
|
|
|
2020-09-29 12:54:22 +00:00
|
|
|
TrackShifter::TrackShifter() = default;
|
|
|
|
|
2020-09-09 09:10:23 +00:00
|
|
|
TrackShifter::~TrackShifter() = default;
|
|
|
|
|
2020-09-11 15:05:07 +00:00
|
|
|
void TrackShifter::UnfixIntervals(
|
|
|
|
std::function< bool( const TrackInterval& ) > pred )
|
|
|
|
{
|
|
|
|
for ( auto iter = mFixed.begin(); iter != mFixed.end(); ) {
|
|
|
|
if ( pred( *iter) ) {
|
|
|
|
mMoving.push_back( std::move( *iter ) );
|
|
|
|
iter = mFixed.erase( iter );
|
2020-09-12 22:57:59 +00:00
|
|
|
mAllFixed = false;
|
2020-09-11 15:05:07 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
++iter;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TrackShifter::UnfixAll()
|
|
|
|
{
|
|
|
|
std::move( mFixed.begin(), mFixed.end(), std::back_inserter(mMoving) );
|
|
|
|
mFixed = Intervals{};
|
2020-09-12 22:57:59 +00:00
|
|
|
mAllFixed = false;
|
2020-09-11 15:05:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-11 15:44:55 +00:00
|
|
|
void TrackShifter::SelectInterval( const TrackInterval & )
|
|
|
|
{
|
|
|
|
UnfixAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
void TrackShifter::CommonSelectInterval(const TrackInterval &interval)
|
|
|
|
{
|
|
|
|
UnfixIntervals( [&](auto &myInterval){
|
|
|
|
return !(interval.End() < myInterval.Start() ||
|
|
|
|
myInterval.End() < interval.Start());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-14 02:59:24 +00:00
|
|
|
double TrackShifter::HintOffsetLarger(double desiredOffset)
|
|
|
|
{
|
|
|
|
return desiredOffset;
|
|
|
|
}
|
|
|
|
|
2020-09-18 12:37:13 +00:00
|
|
|
double TrackShifter::QuantizeOffset(double desiredOffset)
|
|
|
|
{
|
|
|
|
return desiredOffset;
|
|
|
|
}
|
|
|
|
|
2020-09-18 12:40:54 +00:00
|
|
|
double TrackShifter::AdjustOffsetSmaller(double desiredOffset)
|
|
|
|
{
|
|
|
|
return desiredOffset;
|
|
|
|
}
|
|
|
|
|
2020-09-16 12:10:13 +00:00
|
|
|
bool TrackShifter::MayMigrateTo(Track &)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TrackShifter::CommonMayMigrateTo(Track &otherTrack)
|
|
|
|
{
|
|
|
|
auto &track = GetTrack();
|
|
|
|
|
|
|
|
// Both tracks need to be owned to decide this
|
|
|
|
auto pMyList = track.GetOwner().get();
|
|
|
|
auto pOtherList = otherTrack.GetOwner().get();
|
|
|
|
if (pMyList && pOtherList) {
|
|
|
|
|
|
|
|
// Can migrate to another track of the same kind...
|
|
|
|
if ( otherTrack.SameKindAs( track ) ) {
|
|
|
|
|
|
|
|
// ... with the same number of channels ...
|
|
|
|
auto myChannels = TrackList::Channels( &track );
|
|
|
|
auto otherChannels = TrackList::Channels( &otherTrack );
|
|
|
|
if (myChannels.size() == otherChannels.size()) {
|
|
|
|
|
|
|
|
// ... and where this track and the other have corresponding
|
|
|
|
// positions
|
|
|
|
return myChannels.size() == 1 ||
|
|
|
|
std::distance(myChannels.first, pMyList->Find(&track)) ==
|
|
|
|
std::distance(otherChannels.first, pOtherList->Find(&otherTrack));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-09-16 06:30:32 +00:00
|
|
|
auto TrackShifter::Detach() -> Intervals
|
|
|
|
{
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2020-09-18 13:42:15 +00:00
|
|
|
bool TrackShifter::AdjustFit(
|
|
|
|
const Track &, const Intervals&, double &, double)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-09-16 06:30:32 +00:00
|
|
|
bool TrackShifter::Attach( Intervals )
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TrackShifter::FinishMigration()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-09-12 22:57:59 +00:00
|
|
|
void TrackShifter::DoHorizontalOffset( double offset )
|
|
|
|
{
|
|
|
|
if (!AllFixed())
|
|
|
|
GetTrack().Offset( offset );
|
|
|
|
}
|
|
|
|
|
2020-10-07 12:27:09 +00:00
|
|
|
double TrackShifter::AdjustT0(double t0) const
|
|
|
|
{
|
|
|
|
return t0;
|
|
|
|
}
|
|
|
|
|
2020-09-11 15:05:07 +00:00
|
|
|
void TrackShifter::InitIntervals()
|
|
|
|
{
|
|
|
|
mMoving.clear();
|
|
|
|
mFixed = GetTrack().GetIntervals();
|
|
|
|
}
|
|
|
|
|
2020-09-09 09:10:23 +00:00
|
|
|
CoarseTrackShifter::CoarseTrackShifter( Track &track )
|
|
|
|
: mpTrack{ track.SharedPointer() }
|
2020-09-11 15:05:07 +00:00
|
|
|
{
|
|
|
|
InitIntervals();
|
|
|
|
}
|
2020-09-09 09:10:23 +00:00
|
|
|
|
|
|
|
CoarseTrackShifter::~CoarseTrackShifter() = default;
|
|
|
|
|
2020-09-20 02:04:34 +00:00
|
|
|
auto CoarseTrackShifter::HitTest(
|
|
|
|
double, const ViewInfo&, HitTestParams* ) -> HitTestResult
|
2020-09-10 03:50:17 +00:00
|
|
|
{
|
|
|
|
return HitTestResult::Track;
|
|
|
|
}
|
|
|
|
|
2020-09-11 15:44:55 +00:00
|
|
|
bool CoarseTrackShifter::SyncLocks()
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-09-09 09:10:23 +00:00
|
|
|
template<> auto MakeTrackShifter::Implementation() -> Function {
|
2020-09-20 01:13:45 +00:00
|
|
|
return [](Track &track, AudacityProject&) {
|
2020-09-09 09:10:23 +00:00
|
|
|
return std::make_unique<CoarseTrackShifter>(track);
|
|
|
|
};
|
|
|
|
}
|
2020-10-17 23:45:11 +00:00
|
|
|
static MakeTrackShifter registerMakeTrackShifter;
|
2020-09-09 09:10:23 +00:00
|
|
|
|
2020-09-12 16:49:02 +00:00
|
|
|
void ClipMoveState::Init(
|
2020-09-20 01:13:45 +00:00
|
|
|
AudacityProject &project,
|
2020-09-12 16:49:02 +00:00
|
|
|
Track &capturedTrack,
|
2020-09-20 03:56:12 +00:00
|
|
|
TrackShifter::HitTestResult hitTestResult,
|
2020-09-12 16:49:02 +00:00
|
|
|
std::unique_ptr<TrackShifter> pHit,
|
|
|
|
double clickTime,
|
|
|
|
const ViewInfo &viewInfo,
|
|
|
|
TrackList &trackList, bool syncLocked )
|
2017-06-02 01:54:22 +00:00
|
|
|
{
|
2020-09-12 16:49:02 +00:00
|
|
|
shifters.clear();
|
|
|
|
|
|
|
|
auto &state = *this;
|
2020-09-12 19:48:53 +00:00
|
|
|
state.mCapturedTrack = capturedTrack.SharedPointer();
|
2020-09-12 16:49:02 +00:00
|
|
|
|
2020-09-20 03:56:12 +00:00
|
|
|
switch (hitTestResult) {
|
|
|
|
case TrackShifter::HitTestResult::Miss:
|
|
|
|
wxASSERT(false);
|
|
|
|
pHit.reset();
|
|
|
|
break;
|
|
|
|
case TrackShifter::HitTestResult::Track:
|
|
|
|
pHit.reset();
|
|
|
|
break;
|
|
|
|
case TrackShifter::HitTestResult::Intervals:
|
|
|
|
break;
|
|
|
|
case TrackShifter::HitTestResult::Selection:
|
|
|
|
state.movingSelection = true;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2020-09-12 16:49:02 +00:00
|
|
|
|
|
|
|
if (!pHit)
|
|
|
|
return;
|
|
|
|
|
|
|
|
state.shifters[&capturedTrack] = std::move( pHit );
|
|
|
|
|
|
|
|
// Collect TrackShifters for the rest of the tracks
|
|
|
|
for ( auto track : trackList.Any() ) {
|
|
|
|
auto &pShifter = state.shifters[track];
|
|
|
|
if (!pShifter)
|
2020-09-20 01:13:45 +00:00
|
|
|
pShifter = MakeTrackShifter::Call( *track, project );
|
2020-09-12 16:49:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( state.movingSelection ) {
|
2020-09-11 15:44:55 +00:00
|
|
|
// All selected tracks may move some intervals
|
|
|
|
const TrackInterval interval{
|
|
|
|
viewInfo.selectedRegion.t0(),
|
|
|
|
viewInfo.selectedRegion.t1()
|
|
|
|
};
|
|
|
|
for ( const auto &pair : state.shifters ) {
|
|
|
|
auto &shifter = *pair.second;
|
|
|
|
auto &track = shifter.GetTrack();
|
2020-09-20 03:56:12 +00:00
|
|
|
if (&track == &capturedTrack)
|
|
|
|
// Don't change the choice of intervals made by HitTest
|
|
|
|
continue;
|
2020-09-11 15:44:55 +00:00
|
|
|
if ( track.IsSelected() )
|
|
|
|
shifter.SelectInterval( interval );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Move intervals only of the chosen channel group
|
|
|
|
for ( auto channel : TrackList::Channels( &capturedTrack ) ) {
|
|
|
|
auto &shifter = *state.shifters[channel];
|
2020-09-20 03:56:12 +00:00
|
|
|
if ( channel != &capturedTrack )
|
|
|
|
shifter.SelectInterval(TrackInterval{clickTime, clickTime});
|
2020-09-11 15:44:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sync lock propagation of unfixing of intervals
|
|
|
|
if ( syncLocked ) {
|
|
|
|
bool change = true;
|
|
|
|
while( change ) {
|
|
|
|
change = false;
|
|
|
|
|
2020-09-21 15:28:54 +00:00
|
|
|
// Iterate over all unfixed intervals in all tracks
|
|
|
|
// that do propagation and are in sync lock groups ...
|
2020-09-11 15:44:55 +00:00
|
|
|
for ( auto &pair : state.shifters ) {
|
|
|
|
auto &shifter = *pair.second.get();
|
|
|
|
if (!shifter.SyncLocks())
|
|
|
|
continue;
|
|
|
|
auto &track = shifter.GetTrack();
|
2020-09-21 15:28:54 +00:00
|
|
|
auto group = TrackList::SyncLockGroup(&track);
|
|
|
|
if ( group.size() <= 1 )
|
|
|
|
continue;
|
|
|
|
|
2020-09-11 15:44:55 +00:00
|
|
|
auto &intervals = shifter.MovingIntervals();
|
|
|
|
for (auto &interval : intervals) {
|
|
|
|
|
2020-09-21 15:28:54 +00:00
|
|
|
// ...and tell all other tracks in the sync lock group
|
|
|
|
// to select that interval...
|
|
|
|
for ( auto pTrack2 : group ) {
|
|
|
|
if (pTrack2 == &track)
|
2020-09-11 15:44:55 +00:00
|
|
|
continue;
|
2020-09-21 15:28:54 +00:00
|
|
|
|
|
|
|
auto &shifter2 = *shifters[pTrack2];
|
2020-09-11 15:44:55 +00:00
|
|
|
auto size = shifter2.MovingIntervals().size();
|
|
|
|
shifter2.SelectInterval( interval );
|
|
|
|
change = change ||
|
|
|
|
(shifter2.SyncLocks() &&
|
|
|
|
size != shifter2.MovingIntervals().size());
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ... and repeat if any other interval became unfixed in a
|
|
|
|
// shifter that propagates
|
|
|
|
}
|
|
|
|
}
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
|
|
|
|
2020-09-14 09:33:40 +00:00
|
|
|
const TrackInterval *ClipMoveState::CapturedInterval() const
|
|
|
|
{
|
|
|
|
auto pTrack = mCapturedTrack.get();
|
|
|
|
if ( pTrack ) {
|
|
|
|
auto iter = shifters.find( pTrack );
|
|
|
|
if ( iter != shifters.end() ) {
|
|
|
|
auto &pShifter = iter->second;
|
|
|
|
if ( pShifter ) {
|
|
|
|
auto &intervals = pShifter->MovingIntervals();
|
|
|
|
if ( !intervals.empty() )
|
|
|
|
return &intervals[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-09-18 12:40:54 +00:00
|
|
|
double ClipMoveState::DoSlideHorizontal( double desiredSlideAmount )
|
2017-06-02 01:54:22 +00:00
|
|
|
{
|
2020-09-12 16:49:02 +00:00
|
|
|
auto &state = *this;
|
2020-09-12 19:48:53 +00:00
|
|
|
auto &capturedTrack = *state.mCapturedTrack;
|
2020-09-12 16:49:02 +00:00
|
|
|
|
2018-09-22 18:36:52 +00:00
|
|
|
// Given a signed slide distance, move clips, but subject to constraint of
|
|
|
|
// non-overlapping with other clips, so the distance may be adjusted toward
|
|
|
|
// zero.
|
2020-09-18 12:40:54 +00:00
|
|
|
if ( !state.shifters.empty() ) {
|
|
|
|
double initialAllowed = 0;
|
2017-06-02 01:54:22 +00:00
|
|
|
do { // loop to compute allowed, does not actually move anything yet
|
2020-09-18 12:40:54 +00:00
|
|
|
initialAllowed = desiredSlideAmount;
|
|
|
|
|
|
|
|
for (auto &pair : shifters) {
|
|
|
|
auto newAmount = pair.second->AdjustOffsetSmaller( desiredSlideAmount );
|
|
|
|
if ( desiredSlideAmount != newAmount ) {
|
|
|
|
if ( newAmount * desiredSlideAmount < 0 ||
|
|
|
|
fabs(newAmount) > fabs(desiredSlideAmount) ) {
|
|
|
|
wxASSERT( false ); // AdjustOffsetSmaller didn't honor postcondition!
|
|
|
|
newAmount = 0; // Be sure the loop progresses to termination!
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
2020-09-18 12:40:54 +00:00
|
|
|
desiredSlideAmount = newAmount;
|
|
|
|
state.snapLeft = state.snapRight = -1; // see bug 1067
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
2020-09-18 12:40:54 +00:00
|
|
|
if (newAmount == 0)
|
|
|
|
break;
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
2020-09-18 12:40:54 +00:00
|
|
|
} while ( desiredSlideAmount != initialAllowed );
|
2017-06-02 01:54:22 +00:00
|
|
|
}
|
2020-09-12 22:57:59 +00:00
|
|
|
|
|
|
|
// Whether moving intervals or a whole track,
|
|
|
|
// finally, here is where clips are moved
|
|
|
|
if ( desiredSlideAmount != 0.0 )
|
|
|
|
state.DoHorizontalOffset( desiredSlideAmount );
|
2020-09-12 16:49:02 +00:00
|
|
|
|
2020-09-18 12:40:54 +00:00
|
|
|
return (state.hSlideAmount = desiredSlideAmount);
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
|
2020-09-09 12:09:39 +00:00
|
|
|
namespace {
|
|
|
|
SnapPointArray FindCandidates(
|
2020-09-10 00:27:26 +00:00
|
|
|
const TrackList &tracks, const ClipMoveState::ShifterMap &shifters )
|
2020-09-09 12:09:39 +00:00
|
|
|
{
|
2020-09-10 00:27:26 +00:00
|
|
|
// Compare with the other function FindCandidates in Snap
|
|
|
|
// Make the snap manager more selective than it would be if just constructed
|
|
|
|
// from the track list
|
2020-09-09 12:09:39 +00:00
|
|
|
SnapPointArray candidates;
|
2020-09-10 00:27:26 +00:00
|
|
|
for ( const auto &pair : shifters ) {
|
|
|
|
auto &shifter = pair.second;
|
|
|
|
auto &track = shifter->GetTrack();
|
|
|
|
for (const auto &interval : shifter->FixedIntervals() ) {
|
|
|
|
candidates.emplace_back( interval.Start(), &track );
|
|
|
|
if ( interval.Start() != interval.End() )
|
|
|
|
candidates.emplace_back( interval.End(), &track );
|
2020-09-09 12:09:39 +00:00
|
|
|
}
|
2020-09-10 00:27:26 +00:00
|
|
|
}
|
2020-09-09 12:09:39 +00:00
|
|
|
return candidates;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
UIHandle::Result TimeShiftHandle::Click
|
|
|
|
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
|
|
|
|
{
|
2017-06-18 04:32:30 +00:00
|
|
|
using namespace RefreshCode;
|
2019-06-02 17:30:56 +00:00
|
|
|
const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
|
2017-06-18 04:32:30 +00:00
|
|
|
if ( unsafe )
|
|
|
|
return Cancelled;
|
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
const wxMouseEvent &event = evt.event;
|
|
|
|
const wxRect &rect = evt.rect;
|
2019-04-28 10:49:47 +00:00
|
|
|
auto &viewInfo = ViewInfo::Get( *pProject );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2019-06-18 04:00:35 +00:00
|
|
|
const auto pView = std::static_pointer_cast<TrackView>(evt.pCell);
|
|
|
|
const auto pTrack = pView ? pView->FindTrack().get() : nullptr;
|
2018-08-12 19:44:08 +00:00
|
|
|
if (!pTrack)
|
|
|
|
return RefreshCode::Cancelled;
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2019-05-06 23:00:10 +00:00
|
|
|
auto &trackList = TrackList::Get( *pProject );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
mClipMoveState.clear();
|
|
|
|
mDidSlideVertically = false;
|
|
|
|
|
2019-06-13 05:53:45 +00:00
|
|
|
const bool multiToolModeActive =
|
|
|
|
(ToolCodes::multiTool == ProjectSettings::Get( *pProject ).GetTool());
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
const double clickTime =
|
|
|
|
viewInfo.PositionToTime(event.m_x, rect.x);
|
|
|
|
|
2020-09-20 01:13:45 +00:00
|
|
|
auto pShifter = MakeTrackShifter::Call( *pTrack, *pProject );
|
2020-09-09 09:10:23 +00:00
|
|
|
|
2020-09-20 03:56:12 +00:00
|
|
|
auto hitTestResult = TrackShifter::HitTestResult::Track;
|
2020-09-10 03:50:17 +00:00
|
|
|
if (!event.ShiftDown()) {
|
2020-09-19 18:38:25 +00:00
|
|
|
TrackShifter::HitTestParams params{
|
|
|
|
rect, event.m_x, event.m_y
|
|
|
|
};
|
2020-09-20 03:56:12 +00:00
|
|
|
hitTestResult = pShifter->HitTest( clickTime, viewInfo, ¶ms );
|
|
|
|
switch( hitTestResult ) {
|
2020-09-10 03:50:17 +00:00
|
|
|
case TrackShifter::HitTestResult::Miss:
|
|
|
|
return Cancelled;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2020-09-20 03:56:12 +00:00
|
|
|
// just do shifting of one whole track
|
2020-09-10 03:50:17 +00:00
|
|
|
}
|
2020-09-09 09:10:23 +00:00
|
|
|
|
2020-09-20 01:13:45 +00:00
|
|
|
mClipMoveState.Init( *pProject, *pTrack,
|
2020-09-20 03:56:12 +00:00
|
|
|
hitTestResult,
|
|
|
|
std::move( pShifter ),
|
2020-09-12 16:49:02 +00:00
|
|
|
clickTime,
|
2020-09-09 09:10:23 +00:00
|
|
|
|
2020-09-12 16:49:02 +00:00
|
|
|
viewInfo, trackList,
|
|
|
|
ProjectSettings::Get( *pProject ).IsSyncLocked() );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
mSlideUpDownOnly = event.CmdDown() && !multiToolModeActive;
|
|
|
|
mRect = rect;
|
2018-09-26 18:50:56 +00:00
|
|
|
mClipMoveState.mMouseClickX = event.m_x;
|
2020-09-09 11:43:57 +00:00
|
|
|
mSnapManager =
|
2020-09-09 12:09:39 +00:00
|
|
|
std::make_shared<SnapManager>(*trackList.GetOwner(),
|
2020-09-10 00:27:26 +00:00
|
|
|
FindCandidates( trackList, mClipMoveState.shifters ),
|
2020-09-09 12:09:39 +00:00
|
|
|
viewInfo,
|
|
|
|
true, // don't snap to time
|
|
|
|
kPixelTolerance);
|
2015-07-08 21:17:43 +00:00
|
|
|
mClipMoveState.snapLeft = -1;
|
|
|
|
mClipMoveState.snapRight = -1;
|
2020-09-14 09:33:40 +00:00
|
|
|
auto pInterval = mClipMoveState.CapturedInterval();
|
|
|
|
mSnapPreferRightEdge = pInterval &&
|
|
|
|
(fabs(clickTime - pInterval->End()) <
|
|
|
|
fabs(clickTime - pInterval->Start()));
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
return RefreshNone;
|
|
|
|
}
|
|
|
|
|
2018-09-26 18:06:13 +00:00
|
|
|
namespace {
|
2018-09-26 18:50:56 +00:00
|
|
|
double FindDesiredSlideAmount(
|
|
|
|
const ViewInfo &viewInfo, wxCoord xx, const wxMouseEvent &event,
|
|
|
|
SnapManager *pSnapManager,
|
|
|
|
bool slideUpDownOnly, bool snapPreferRightEdge,
|
|
|
|
ClipMoveState &state,
|
2020-09-18 12:37:13 +00:00
|
|
|
Track &track )
|
2018-09-26 18:50:56 +00:00
|
|
|
{
|
2020-09-18 12:37:13 +00:00
|
|
|
auto &capturedTrack = *state.mCapturedTrack;
|
2018-09-26 18:50:56 +00:00
|
|
|
if (slideUpDownOnly)
|
|
|
|
return 0.0;
|
|
|
|
else {
|
|
|
|
double desiredSlideAmount =
|
|
|
|
viewInfo.PositionToTime(event.m_x) -
|
|
|
|
viewInfo.PositionToTime(state.mMouseClickX);
|
|
|
|
double clipLeft = 0, clipRight = 0;
|
|
|
|
|
2020-09-18 12:37:13 +00:00
|
|
|
if (!state.shifters.empty())
|
|
|
|
desiredSlideAmount =
|
|
|
|
state.shifters[ &track ]->QuantizeOffset( desiredSlideAmount );
|
2018-09-26 18:50:56 +00:00
|
|
|
|
|
|
|
// Adjust desiredSlideAmount using SnapManager
|
|
|
|
if (pSnapManager) {
|
2020-09-14 09:33:40 +00:00
|
|
|
auto pInterval = state.CapturedInterval();
|
|
|
|
if (pInterval) {
|
|
|
|
clipLeft = pInterval->Start() + desiredSlideAmount;
|
|
|
|
clipRight = pInterval->End() + desiredSlideAmount;
|
2018-09-26 18:50:56 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
clipLeft = capturedTrack.GetStartTime() + desiredSlideAmount;
|
|
|
|
clipRight = capturedTrack.GetEndTime() + desiredSlideAmount;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto results =
|
|
|
|
pSnapManager->Snap(&capturedTrack, clipLeft, false);
|
|
|
|
auto newClipLeft = results.outTime;
|
|
|
|
results =
|
|
|
|
pSnapManager->Snap(&capturedTrack, clipRight, false);
|
|
|
|
auto newClipRight = results.outTime;
|
|
|
|
|
|
|
|
// Only one of them is allowed to snap
|
|
|
|
if (newClipLeft != clipLeft && newClipRight != clipRight) {
|
|
|
|
// Un-snap the un-preferred edge
|
|
|
|
if (snapPreferRightEdge)
|
|
|
|
newClipLeft = clipLeft;
|
|
|
|
else
|
|
|
|
newClipRight = clipRight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take whichever one snapped (if any) and compute the NEW desiredSlideAmount
|
|
|
|
state.snapLeft = -1;
|
|
|
|
state.snapRight = -1;
|
|
|
|
if (newClipLeft != clipLeft) {
|
|
|
|
const double difference = (newClipLeft - clipLeft);
|
|
|
|
desiredSlideAmount += difference;
|
|
|
|
state.snapLeft =
|
|
|
|
viewInfo.TimeToPosition(newClipLeft, xx);
|
|
|
|
}
|
|
|
|
else if (newClipRight != clipRight) {
|
|
|
|
const double difference = (newClipRight - clipRight);
|
|
|
|
desiredSlideAmount += difference;
|
|
|
|
state.snapRight =
|
|
|
|
viewInfo.TimeToPosition(newClipRight, xx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return desiredSlideAmount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-16 12:10:13 +00:00
|
|
|
using Correspondence = std::unordered_map< Track*, Track* >;
|
|
|
|
|
2018-09-26 23:14:14 +00:00
|
|
|
bool FindCorrespondence(
|
2020-09-16 12:10:13 +00:00
|
|
|
Correspondence &correspondence,
|
2020-09-21 14:54:19 +00:00
|
|
|
TrackList &trackList, Track &capturedTrack, Track &track,
|
2018-09-26 23:14:14 +00:00
|
|
|
ClipMoveState &state)
|
|
|
|
{
|
2020-09-21 14:54:19 +00:00
|
|
|
// Accumulate new pairs for the correspondence, and merge them
|
|
|
|
// into the given correspondence only on success
|
|
|
|
Correspondence newPairs;
|
|
|
|
|
2020-09-16 12:10:13 +00:00
|
|
|
auto sameType = [&]( auto pTrack ){
|
2020-09-21 14:54:19 +00:00
|
|
|
return capturedTrack.SameKindAs( *pTrack );
|
2020-09-16 12:10:13 +00:00
|
|
|
};
|
|
|
|
if (!sameType(&track))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// All tracks of the same kind as the captured track
|
|
|
|
auto range = trackList.Any() + sameType;
|
|
|
|
|
|
|
|
// Find how far this track would shift down among those (signed)
|
|
|
|
const auto myPosition =
|
2020-09-21 14:54:19 +00:00
|
|
|
std::distance( range.first, trackList.Find( &capturedTrack ) );
|
2020-09-16 12:10:13 +00:00
|
|
|
const auto otherPosition =
|
|
|
|
std::distance( range.first, trackList.Find( &track ) );
|
|
|
|
auto diff = otherPosition - myPosition;
|
|
|
|
|
|
|
|
// Point to destination track
|
|
|
|
auto iter = range.first.advance( diff > 0 ? diff : 0 );
|
|
|
|
|
|
|
|
for (auto pTrack : range) {
|
|
|
|
auto &pShifter = state.shifters[pTrack];
|
|
|
|
if ( !pShifter->MovingIntervals().empty() ) {
|
|
|
|
// One of the interesting tracks
|
|
|
|
|
|
|
|
auto pOther = *iter;
|
|
|
|
if ( diff < 0 || !pOther )
|
|
|
|
// No corresponding track
|
2017-01-03 22:07:12 +00:00
|
|
|
return false;
|
|
|
|
|
2020-09-16 12:10:13 +00:00
|
|
|
if ( !pShifter->MayMigrateTo(*pOther) )
|
|
|
|
// Rejected for other reason
|
2018-09-26 23:14:14 +00:00
|
|
|
return false;
|
2017-01-03 22:07:12 +00:00
|
|
|
|
2020-09-21 14:54:19 +00:00
|
|
|
if ( correspondence.count(pTrack) )
|
|
|
|
// Don't overwrite the given correspondence
|
|
|
|
return false;
|
|
|
|
|
|
|
|
newPairs[ pTrack ] = pOther;
|
2018-09-26 23:14:14 +00:00
|
|
|
}
|
2020-09-16 12:10:13 +00:00
|
|
|
|
|
|
|
if ( diff < 0 )
|
|
|
|
++diff; // Still consuming initial tracks
|
|
|
|
else
|
|
|
|
++iter; // Safe to increment TrackIter even at end of range
|
2018-09-26 23:14:14 +00:00
|
|
|
}
|
2020-09-16 12:10:13 +00:00
|
|
|
|
2020-09-21 14:54:19 +00:00
|
|
|
// Success
|
|
|
|
if (correspondence.empty())
|
|
|
|
correspondence.swap(newPairs);
|
|
|
|
else
|
|
|
|
std::copy( newPairs.begin(), newPairs.end(),
|
|
|
|
std::inserter( correspondence, correspondence.end() ) );
|
2018-09-26 23:14:14 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-09-18 13:42:15 +00:00
|
|
|
using DetachedIntervals =
|
|
|
|
std::unordered_map<Track*, TrackShifter::Intervals>;
|
|
|
|
|
2018-09-26 23:27:07 +00:00
|
|
|
bool CheckFit(
|
2020-09-18 13:42:15 +00:00
|
|
|
ClipMoveState &state, const Correspondence &correspondence,
|
|
|
|
const DetachedIntervals &intervals,
|
2018-09-26 23:27:07 +00:00
|
|
|
double tolerance, double &desiredSlideAmount )
|
|
|
|
{
|
|
|
|
bool ok = true;
|
|
|
|
double firstTolerance = tolerance;
|
2020-09-18 13:42:15 +00:00
|
|
|
|
2018-09-26 23:27:07 +00:00
|
|
|
// The desiredSlideAmount may change and the tolerance may get used up.
|
|
|
|
for ( unsigned iPass = 0; iPass < 2 && ok; ++iPass ) {
|
2020-09-18 13:42:15 +00:00
|
|
|
for ( auto &pair : state.shifters ) {
|
|
|
|
auto *pSrcTrack = pair.first;
|
|
|
|
auto iter = correspondence.find( pSrcTrack );
|
|
|
|
if ( iter != correspondence.end() )
|
|
|
|
if( auto *pOtherTrack = iter->second )
|
|
|
|
if ( !(ok = pair.second->AdjustFit(
|
|
|
|
*pOtherTrack, intervals.at(pSrcTrack),
|
|
|
|
desiredSlideAmount /*in,out*/, tolerance)) )
|
|
|
|
break;
|
2018-09-26 23:27:07 +00:00
|
|
|
}
|
2020-09-18 13:42:15 +00:00
|
|
|
|
2018-09-26 23:27:07 +00:00
|
|
|
// If it fits ok, desiredSlideAmount could have been updated to get
|
|
|
|
// the clip to fit.
|
|
|
|
// Check again, in the new position, this time with zero tolerance.
|
|
|
|
if (firstTolerance == 0)
|
|
|
|
break;
|
|
|
|
else
|
|
|
|
tolerance = 0.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ok;
|
|
|
|
}
|
|
|
|
|
2020-09-16 06:30:32 +00:00
|
|
|
[[noreturn]] void MigrationFailure() {
|
|
|
|
// Tracks may be in an inconsistent state; throw to the application
|
|
|
|
// handler which restores consistency from undo history
|
|
|
|
throw SimpleMessageBoxException{
|
|
|
|
XO("Could not shift between tracks")};
|
|
|
|
}
|
|
|
|
|
2018-09-26 18:06:13 +00:00
|
|
|
struct TemporaryClipRemover {
|
|
|
|
TemporaryClipRemover( ClipMoveState &clipMoveState )
|
|
|
|
: state( clipMoveState )
|
|
|
|
{
|
|
|
|
// Pluck the moving clips out of their tracks
|
2020-09-16 06:30:32 +00:00
|
|
|
for (auto &pair : state.shifters)
|
|
|
|
detached[pair.first] = pair.second->Detach();
|
2018-09-26 18:06:13 +00:00
|
|
|
}
|
|
|
|
|
2020-09-16 06:30:32 +00:00
|
|
|
void Reinsert(
|
2020-09-20 22:51:58 +00:00
|
|
|
std::unordered_map< Track*, Track* > *pCorrespondence )
|
2018-09-26 18:06:13 +00:00
|
|
|
{
|
2020-09-16 06:30:32 +00:00
|
|
|
for (auto &pair : detached) {
|
|
|
|
auto pTrack = pair.first;
|
2020-09-20 22:51:58 +00:00
|
|
|
if (pCorrespondence && pCorrespondence->count(pTrack))
|
|
|
|
pTrack = (*pCorrespondence)[pTrack];
|
2020-09-16 06:30:32 +00:00
|
|
|
auto &pShifter = state.shifters[pTrack];
|
|
|
|
if (!pShifter->Attach( std::move( pair.second ) ))
|
|
|
|
MigrationFailure();
|
|
|
|
}
|
2018-09-26 18:06:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ClipMoveState &state;
|
2020-09-18 13:42:15 +00:00
|
|
|
DetachedIntervals detached;
|
2018-09-26 18:06:13 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-09-26 23:43:48 +00:00
|
|
|
bool TimeShiftHandle::DoSlideVertical
|
|
|
|
( ViewInfo &viewInfo, wxCoord xx,
|
2020-09-16 12:01:09 +00:00
|
|
|
ClipMoveState &state, TrackList &trackList,
|
2018-09-26 23:43:48 +00:00
|
|
|
Track &dstTrack, double &desiredSlideAmount )
|
|
|
|
{
|
2020-09-16 12:10:13 +00:00
|
|
|
Correspondence correspondence;
|
2020-09-21 14:54:19 +00:00
|
|
|
|
|
|
|
// See if captured track corresponds to another
|
|
|
|
auto &capturedTrack = *state.mCapturedTrack;
|
|
|
|
if (!FindCorrespondence(
|
|
|
|
correspondence, trackList, capturedTrack, dstTrack, state ))
|
2018-09-26 23:43:48 +00:00
|
|
|
return false;
|
|
|
|
|
2020-09-21 15:03:57 +00:00
|
|
|
// Try to extend the correpondence
|
|
|
|
auto tryExtend = [&](bool forward){
|
|
|
|
auto begin = trackList.begin(), end = trackList.end();
|
|
|
|
auto pCaptured = trackList.Find( &capturedTrack );
|
|
|
|
auto pDst = trackList.Find( &dstTrack );
|
|
|
|
// Scan for more correspondences
|
|
|
|
while ( true ) {
|
|
|
|
// Remember that TrackIter wraps circularly to the end iterator when
|
|
|
|
// decrementing it
|
|
|
|
|
|
|
|
// First move to a track with moving intervals and
|
|
|
|
// without a correspondent
|
|
|
|
do
|
|
|
|
forward ? ++pCaptured : --pCaptured;
|
|
|
|
while ( pCaptured != end &&
|
|
|
|
( correspondence.count(*pCaptured) || state.shifters[*pCaptured]->MovingIntervals().empty() ) );
|
|
|
|
if ( pCaptured == end )
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Change the choice of possible correspondent track too
|
|
|
|
do
|
|
|
|
forward ? ++pDst : --pDst;
|
|
|
|
while ( pDst != end && correspondence.count(*pDst) );
|
|
|
|
if ( pDst == end )
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Make correspondence if we can
|
|
|
|
if (!FindCorrespondence(
|
|
|
|
correspondence, trackList, **pCaptured, **pDst, state ))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Try extension, backward first, then forward
|
|
|
|
// (anticipating the case of dragging a label that is under a clip)
|
|
|
|
tryExtend(false);
|
|
|
|
tryExtend(true);
|
|
|
|
|
2018-09-26 23:43:48 +00:00
|
|
|
// Having passed that test, remove clips temporarily from their
|
|
|
|
// tracks, so moving clips don't interfere with each other
|
|
|
|
// when we call CanInsertClip()
|
|
|
|
TemporaryClipRemover remover{ state };
|
|
|
|
|
|
|
|
// Now check that the move is possible
|
|
|
|
double slide = desiredSlideAmount; // remember amount requested.
|
|
|
|
// The test for tolerance will need review with FishEye!
|
|
|
|
// The tolerance is supposed to be the time for one pixel,
|
|
|
|
// i.e. one pixel tolerance at current zoom.
|
|
|
|
double tolerance =
|
|
|
|
viewInfo.PositionToTime(xx + 1) - viewInfo.PositionToTime(xx);
|
2020-09-18 13:42:15 +00:00
|
|
|
bool ok = CheckFit( state, correspondence, remover.detached,
|
|
|
|
tolerance, desiredSlideAmount /*in,out*/ );
|
2018-09-26 23:43:48 +00:00
|
|
|
|
|
|
|
if (!ok) {
|
|
|
|
// Failure, even with using tolerance.
|
2020-09-20 22:51:58 +00:00
|
|
|
remover.Reinsert( nullptr );
|
2018-09-26 23:43:48 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the offset permanent; start from a "clean slate"
|
|
|
|
state.mMouseClickX = xx;
|
2020-09-20 22:51:58 +00:00
|
|
|
remover.Reinsert( &correspondence );
|
2018-09-26 23:43:48 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
UIHandle::Result TimeShiftHandle::Drag
|
|
|
|
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
|
|
|
|
{
|
2018-09-22 18:36:52 +00:00
|
|
|
using namespace RefreshCode;
|
2019-06-02 17:30:56 +00:00
|
|
|
const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
|
2018-09-22 18:36:52 +00:00
|
|
|
if (unsafe) {
|
|
|
|
this->Cancel(pProject);
|
|
|
|
return RefreshAll | Cancelled;
|
|
|
|
}
|
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
const wxMouseEvent &event = evt.event;
|
2019-04-28 10:49:47 +00:00
|
|
|
auto &viewInfo = ViewInfo::Get( *pProject );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2019-06-18 04:00:35 +00:00
|
|
|
TrackView *trackView = dynamic_cast<TrackView*>(evt.pCell.get());
|
|
|
|
Track *track = trackView ? trackView->FindTrack().get() : nullptr;
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
// Uncommenting this permits drag to continue to work even over the controls area
|
|
|
|
/*
|
2018-09-22 18:36:52 +00:00
|
|
|
track = static_cast<CommonTrackPanelCell*>(evt.pCell)->FindTrack().get();
|
2015-07-08 21:17:43 +00:00
|
|
|
*/
|
|
|
|
|
2017-06-27 17:40:36 +00:00
|
|
|
if (!track) {
|
2015-07-08 21:17:43 +00:00
|
|
|
// Allow sliding if the pointer is not over any track, but only if x is
|
|
|
|
// within the bounds of the tracks area.
|
|
|
|
if (event.m_x >= mRect.GetX() &&
|
|
|
|
event.m_x < mRect.GetX() + mRect.GetWidth())
|
2020-09-12 19:48:53 +00:00
|
|
|
track = mClipMoveState.mCapturedTrack.get();
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
|
Changed lifetime management of UIHandle objects, no singletons...
... Rather, construct them during hit tests (also capturing more state sooner
rather than at Click time, and adding some accessors for later use)
This also fixes bug 1677 by other means and avoids similar problems.
A cell may be implemented to re-use a previously hit handle object, not yet
clicked, in a later hit test, by remembering a weak pointer, but TrackPanel
holds the strong pointers that determine when the object is destroyed.
And the objects will surely be destroyed after drag-release, or ESC key.
For now they are also destroyed whenever not dragging, and hit-testing is
re-invoked; that will be changed later, so that the re-use mentioned above
becomes effective, but still they will be destroyed when the pointer moves
from one cell to another.
2017-07-05 20:45:55 +00:00
|
|
|
// May need a shared_ptr to reassign mCapturedTrack below
|
2019-03-23 17:23:46 +00:00
|
|
|
auto pTrack = Track::SharedPointer( track );
|
2015-07-08 21:17:43 +00:00
|
|
|
if (!pTrack)
|
|
|
|
return RefreshCode::RefreshNone;
|
|
|
|
|
2017-06-27 17:40:36 +00:00
|
|
|
|
2019-05-06 23:00:10 +00:00
|
|
|
auto &trackList = TrackList::Get( *pProject );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2020-09-12 19:48:53 +00:00
|
|
|
// GM: slide now implementing snap-to
|
2015-07-08 21:17:43 +00:00
|
|
|
// samples functionality based on sample rate.
|
|
|
|
|
|
|
|
// Start by undoing the current slide amount; everything
|
|
|
|
// happens relative to the original horizontal position of
|
|
|
|
// each clip...
|
2020-09-12 22:57:59 +00:00
|
|
|
mClipMoveState.DoHorizontalOffset( -mClipMoveState.hSlideAmount );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2020-09-12 16:49:02 +00:00
|
|
|
if ( mClipMoveState.movingSelection ) {
|
2015-07-08 21:17:43 +00:00
|
|
|
// Slide the selection, too
|
|
|
|
viewInfo.selectedRegion.move( -mClipMoveState.hSlideAmount );
|
|
|
|
}
|
|
|
|
mClipMoveState.hSlideAmount = 0.0;
|
|
|
|
|
2018-09-26 18:50:56 +00:00
|
|
|
double desiredSlideAmount =
|
|
|
|
FindDesiredSlideAmount( viewInfo, mRect.x, event, mSnapManager.get(),
|
|
|
|
mSlideUpDownOnly, mSnapPreferRightEdge, mClipMoveState,
|
2020-09-18 12:37:13 +00:00
|
|
|
*pTrack );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
// Scroll during vertical drag.
|
|
|
|
// If the mouse is over a track that isn't the captured track,
|
|
|
|
// decide which tracks the captured clips should go to.
|
2020-09-16 12:01:09 +00:00
|
|
|
// EnsureVisible(pTrack); //vvv Gale says this has problems on Linux, per bug 393 thread. Revert for 2.0.2.
|
|
|
|
bool slidVertically = (
|
2020-09-12 19:48:53 +00:00
|
|
|
pTrack != mClipMoveState.mCapturedTrack
|
2017-04-25 13:24:49 +00:00
|
|
|
/* && !mCapturedClipIsSelection*/
|
2020-09-16 16:51:28 +00:00
|
|
|
&& DoSlideVertical( viewInfo, event.m_x, mClipMoveState,
|
|
|
|
trackList, *pTrack, desiredSlideAmount ) );
|
|
|
|
if (slidVertically)
|
|
|
|
{
|
|
|
|
mClipMoveState.mCapturedTrack = pTrack;
|
|
|
|
mDidSlideVertically = true;
|
|
|
|
}
|
2017-04-25 13:24:49 +00:00
|
|
|
|
2015-07-08 21:17:43 +00:00
|
|
|
if (desiredSlideAmount == 0.0)
|
|
|
|
return RefreshAll;
|
|
|
|
|
2020-09-14 02:59:24 +00:00
|
|
|
// Note that mouse dragging doesn't use TrackShifter::HintOffsetLarger()
|
|
|
|
|
2020-09-18 12:40:54 +00:00
|
|
|
mClipMoveState.DoSlideHorizontal( desiredSlideAmount );
|
2015-07-08 21:17:43 +00:00
|
|
|
|
2020-09-12 16:49:02 +00:00
|
|
|
if (mClipMoveState.movingSelection) {
|
2015-07-08 21:17:43 +00:00
|
|
|
// Slide the selection, too
|
|
|
|
viewInfo.selectedRegion.move( mClipMoveState.hSlideAmount );
|
|
|
|
}
|
|
|
|
|
|
|
|
if (slidVertically) {
|
|
|
|
// NEW origin
|
|
|
|
mClipMoveState.hSlideAmount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return RefreshAll;
|
|
|
|
}
|
|
|
|
|
|
|
|
HitTestPreview TimeShiftHandle::Preview
|
2020-01-04 14:40:33 +00:00
|
|
|
(const TrackPanelMouseState &, AudacityProject *pProject)
|
2015-07-08 21:17:43 +00:00
|
|
|
{
|
2017-06-18 04:32:30 +00:00
|
|
|
// After all that, it still may be unsafe to drag.
|
|
|
|
// Even if so, make an informative cursor change from default to "banned."
|
2019-06-02 17:30:56 +00:00
|
|
|
const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
|
2017-06-18 04:32:30 +00:00
|
|
|
return HitPreview(pProject, unsafe);
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
UIHandle::Result TimeShiftHandle::Release
|
|
|
|
(const TrackPanelMouseEvent &, AudacityProject *pProject,
|
|
|
|
wxWindow *)
|
|
|
|
{
|
|
|
|
using namespace RefreshCode;
|
2019-06-02 17:30:56 +00:00
|
|
|
const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
|
2015-07-08 21:17:43 +00:00
|
|
|
if (unsafe)
|
|
|
|
return this->Cancel(pProject);
|
|
|
|
|
|
|
|
Result result = RefreshNone;
|
|
|
|
|
|
|
|
// Do not draw yellow lines
|
|
|
|
if ( mClipMoveState.snapLeft != -1 || mClipMoveState.snapRight != -1) {
|
|
|
|
mClipMoveState.snapLeft = mClipMoveState.snapRight = -1;
|
|
|
|
result |= RefreshAll;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !mDidSlideVertically && mClipMoveState.hSlideAmount == 0 )
|
|
|
|
return result;
|
|
|
|
|
2020-09-16 06:30:32 +00:00
|
|
|
for ( auto &pair : mClipMoveState.shifters )
|
|
|
|
if ( !pair.second->FinishMigration() )
|
|
|
|
MigrationFailure();
|
|
|
|
|
2019-12-08 17:11:31 +00:00
|
|
|
TranslatableString msg;
|
2015-07-08 21:17:43 +00:00
|
|
|
bool consolidate;
|
|
|
|
if (mDidSlideVertically) {
|
2019-12-08 17:11:31 +00:00
|
|
|
msg = XO("Moved clips to another track");
|
2015-07-08 21:17:43 +00:00
|
|
|
consolidate = false;
|
|
|
|
}
|
|
|
|
else {
|
2019-12-08 17:11:31 +00:00
|
|
|
msg = ( mClipMoveState.hSlideAmount > 0
|
|
|
|
? XO("Time shifted tracks/clips right %.02f seconds")
|
|
|
|
: XO("Time shifted tracks/clips left %.02f seconds")
|
|
|
|
)
|
|
|
|
.Format( fabs( mClipMoveState.hSlideAmount ) );
|
2015-07-08 21:17:43 +00:00
|
|
|
consolidate = true;
|
|
|
|
}
|
2019-12-08 17:11:31 +00:00
|
|
|
ProjectHistory::Get( *pProject ).PushState(msg, XO("Time-Shift"),
|
2020-07-10 17:19:59 +00:00
|
|
|
consolidate ? (UndoPush::CONSOLIDATE) : (UndoPush::NONE));
|
2015-07-08 21:17:43 +00:00
|
|
|
|
|
|
|
return result | FixScrollbars;
|
|
|
|
}
|
|
|
|
|
|
|
|
UIHandle::Result TimeShiftHandle::Cancel(AudacityProject *pProject)
|
|
|
|
{
|
2019-06-06 13:55:34 +00:00
|
|
|
ProjectHistory::Get( *pProject ).RollbackState();
|
2015-07-08 21:17:43 +00:00
|
|
|
return RefreshCode::RefreshAll;
|
|
|
|
}
|
|
|
|
|
2019-06-21 19:10:11 +00:00
|
|
|
void TimeShiftHandle::Draw(
|
|
|
|
TrackPanelDrawingContext &context,
|
|
|
|
const wxRect &rect, unsigned iPass )
|
2015-07-08 21:17:43 +00:00
|
|
|
{
|
2019-06-21 19:10:11 +00:00
|
|
|
if ( iPass == TrackArtist::PassSnapping ) {
|
|
|
|
auto &dc = context.dc;
|
2015-07-08 21:17:43 +00:00
|
|
|
// Draw snap guidelines if we have any
|
2019-06-21 19:10:11 +00:00
|
|
|
if ( mSnapManager ) {
|
|
|
|
mSnapManager->Draw(
|
|
|
|
&dc, mClipMoveState.snapLeft, mClipMoveState.snapRight );
|
|
|
|
}
|
2015-07-08 21:17:43 +00:00
|
|
|
}
|
|
|
|
}
|
2019-06-21 19:10:11 +00:00
|
|
|
|
|
|
|
wxRect TimeShiftHandle::DrawingArea(
|
2019-12-28 17:25:01 +00:00
|
|
|
TrackPanelDrawingContext &,
|
2019-06-21 19:10:11 +00:00
|
|
|
const wxRect &rect, const wxRect &panelRect, unsigned iPass )
|
|
|
|
{
|
|
|
|
if ( iPass == TrackArtist::PassSnapping )
|
|
|
|
return MaximizeHeight( rect, panelRect );
|
|
|
|
else
|
|
|
|
return rect;
|
|
|
|
}
|