1 /*!********************************************************************
2 *
3  Audacity: A Digital Audio Editor
4 
5  WaveClipTrimHandle.cpp
6 
7  Vitaly Sverchinsky
8 
9  **********************************************************************/
10 
11 #include "WaveClipTrimHandle.h"
12 #include "ProjectAudioIO.h"
13 #include "RefreshCode.h"
14 
15 
16 #include "../../../../TrackArtist.h"
17 #include "../../../../Snap.h"
18 #include "../../../../TrackPanelDrawingContext.h"
19 #include "../../../images/Cursors.h"
20 #include "WaveClip.h"
21 #include "WaveTrack.h"
22 #include "WaveTrackView.h"
23 #include "HitTestResult.h"
24 #include "TrackPanelMouseEvent.h"
25 #include "ViewInfo.h"
26 #include "ProjectHistory.h"
27 #include "UndoManager.h"
28 
29 namespace {
30 
FindClipsInChannels(double start,double end,WaveTrack * track)31     std::vector<std::shared_ptr<WaveClip>> FindClipsInChannels(double start, double end, WaveTrack* track) {
32         std::vector<std::shared_ptr<WaveClip>> result;
33         for (auto channel : TrackList::Channels(track))
34         {
35             for (auto& clip : channel->GetClips())
36             {
37                 if (clip->GetPlayStartTime() == start && clip->GetPlayEndTime() == end)
38                     result.push_back(clip);
39             }
40         }
41         return result;
42     }
43 }
44 
45 WaveClipTrimHandle::ClipTrimPolicy::~ClipTrimPolicy() = default;
46 
47 class WaveClipTrimHandle::AdjustBorder final : public WaveClipTrimHandle::ClipTrimPolicy
48 {
49    std::shared_ptr<WaveTrack> mTrack;
50    std::vector<std::shared_ptr<WaveClip>> mClips;
51    double mInitialBorderPosition{};
52    int mDragStartX{ };
53    std::pair<double, double> mRange;
54    bool mAdjustingLeftBorder;
55 
56    std::unique_ptr<SnapManager> mSnapManager;
57    SnapResults mSnap;
58 
TrimTo(double t)59    void TrimTo(double t)
60    {
61       t = std::clamp(t, mRange.first, mRange.second);
62       if (mAdjustingLeftBorder)
63       {
64          for (auto& clip : mClips)
65             clip->TrimLeftTo(t);
66       }
67       else
68       {
69          for (auto& clip : mClips)
70             clip->TrimRightTo(t);
71       }
72    }
73 
74    //Search for a good snap points among all tracks, including
75    //the one to which adjusted clip belongs to, but not counting
76    //the borders of adjusted clip
FindSnapPoints(const WaveTrack * currentTrack,WaveClip * adjustedClip,const std::pair<double,double> range)77    static SnapPointArray FindSnapPoints(
78       const WaveTrack* currentTrack,
79       WaveClip* adjustedClip,
80       const std::pair<double, double> range)
81    {
82       SnapPointArray result;
83 
84       auto addSnapPoint = [&](double t, const Track* track)
85       {
86          if(t > range.second || t < range.first)
87             return;
88 
89          for(const auto& snapPoint : result)
90             if(snapPoint.t == t)
91                return;
92          result.emplace_back(t, track);
93       };
94 
95       if(const auto trackList = currentTrack->GetOwner())
96       {
97          for(const auto track : trackList->Any())
98          {
99             const auto isSameTrack = (track == currentTrack) ||
100                (track->GetLinkType() == Track::LinkType::Aligned && *trackList->FindLeader(currentTrack) == track) ||
101                (currentTrack->GetLinkType() == Track::LinkType::Aligned && *trackList->FindLeader(track) == currentTrack);
102             for(const auto& interval : track->GetIntervals())
103             {
104                if(isSameTrack)
105                {
106                   auto waveTrackIntervalData = dynamic_cast<WaveTrack::IntervalData*>(interval.Extra());
107                   if(waveTrackIntervalData->GetClip().get() == adjustedClip)
108                   //exclude boundaries of the adjusted clip
109                      continue;
110                }
111                addSnapPoint(interval.Start(), track);
112                if(interval.Start() != interval.End())
113                   addSnapPoint(interval.End(), track);
114             }
115          }
116       }
117       return result;
118    }
119 
120 public:
AdjustBorder(const std::shared_ptr<WaveTrack> & track,const std::shared_ptr<WaveClip> & clip,bool leftBorder,const ZoomInfo & zoomInfo)121    AdjustBorder(
122       const std::shared_ptr<WaveTrack>& track,
123       const std::shared_ptr<WaveClip>& clip,
124       bool leftBorder,
125       const ZoomInfo& zoomInfo)
126       : mTrack(track),
127         mAdjustingLeftBorder(leftBorder)
128    {
129       auto clips = track->GetClips();
130 
131       wxASSERT(std::find(clips.begin(), clips.end(), clip) != clips.end());
132 
133       if (track->IsAlignedWithLeader() || track->GetLinkType() == Track::LinkType::Aligned)
134          //find clips in other channels which are also should be trimmed
135          mClips = FindClipsInChannels(clip->GetPlayStartTime(), clip->GetPlayEndTime(), track.get());
136       else
137          mClips.push_back(clip);
138 
139       if (mAdjustingLeftBorder)
140       {
141          auto left = clip->GetSequenceStartTime();
142          for (const auto& other : clips)
143             if (other->GetPlayStartTime() < clip->GetPlayStartTime() && other->GetPlayEndTime() > left)
144                left = other->GetPlayEndTime();
145          //not less than 1 sample length
146          mRange = std::make_pair(left, clip->GetPlayEndTime() - 1.0 / clip->GetRate());
147 
148          mInitialBorderPosition = mClips[0]->GetPlayStartTime();
149       }
150       else
151       {
152          auto right = clip->GetSequenceEndTime();
153          for (const auto& other : clips)
154             if (other->GetPlayStartTime() > clip->GetPlayStartTime() && other->GetPlayStartTime() < right)
155                right = other->GetPlayStartTime();
156          //not less than 1 sample length
157          mRange = std::make_pair(clip->GetPlayStartTime() + 1.0 / clip->GetRate(), right);
158 
159          mInitialBorderPosition = mClips[0]->GetPlayEndTime();
160       }
161 
162       if(const auto trackList = track->GetOwner())
163       {
164          mSnapManager = std::make_unique<SnapManager>(
165             *trackList->GetOwner(),
166             FindSnapPoints(track.get(), clip.get(), mRange),
167             zoomInfo);
168       }
169    }
170 
Init(const TrackPanelMouseEvent & event)171    bool Init(const TrackPanelMouseEvent& event) override
172    {
173       if (event.event.LeftDown())
174       {
175          mDragStartX = event.event.GetX();
176          return true;
177       }
178       return false;
179    }
180 
Trim(const TrackPanelMouseEvent & event,AudacityProject & project)181    UIHandle::Result Trim(const TrackPanelMouseEvent& event, AudacityProject& project) override
182    {
183       const auto eventX = event.event.GetX();
184       const auto dx = eventX - mDragStartX;
185 
186       const auto& viewInfo = ViewInfo::Get(project);
187 
188       const auto eventT = viewInfo.PositionToTime(viewInfo.TimeToPosition(mInitialBorderPosition, event.rect.x) + dx, event.rect.x);
189 
190       const auto offset = sampleCount(floor((eventT - mInitialBorderPosition) * mClips[0]->GetRate())).as_double() / mClips[0]->GetRate();
191       const auto t = std::clamp(mInitialBorderPosition + offset, mRange.first, mRange.second);
192       const auto wasSnapped = mSnap.Snapped();
193       if(mSnapManager)
194          mSnap = mSnapManager->Snap(mTrack.get(), t, !mAdjustingLeftBorder);
195       if(mSnap.Snapped())
196       {
197          if(mSnap.outTime >= mRange.first && mSnap.outTime <= mRange.second)
198          {
199             //Make sure that outTime belongs to the adjustment range after snapping
200             TrimTo(mSnap.outTime);
201             return RefreshCode::RefreshAll;
202          }
203          else
204          {
205             //Otherwise snapping cannot be performed
206             mSnap = {};
207             TrimTo(t);
208          }
209       }
210       else
211          TrimTo(t);
212       //If there was a snap line, make sure it is removed
213       //from the screen by redrawing whole TrackPanel
214       return wasSnapped ? RefreshCode::RefreshAll : RefreshCode::RefreshCell;
215    }
216 
Finish(AudacityProject & project)217    void Finish(AudacityProject& project) override
218    {
219       if (mClips[0]->GetPlayStartTime() != mInitialBorderPosition)
220       {
221          if (mAdjustingLeftBorder)
222          {
223             auto dt = std::abs(mClips[0]->GetPlayStartTime() - mInitialBorderPosition);
224             ProjectHistory::Get(project).PushState(XO("Clip-Trim-Left"),
225                 XO("Moved by %.02f").Format(dt), UndoPush::CONSOLIDATE);
226          }
227          else
228          {
229             auto dt = std::abs(mInitialBorderPosition - mClips[0]->GetPlayEndTime());
230             ProjectHistory::Get(project).PushState(XO("Clip-Trim-Right"),
231                 XO("Moved by %.02f").Format(dt), UndoPush::CONSOLIDATE);
232          }
233       }
234    }
235 
Cancel()236    void Cancel() override
237    {
238       TrimTo(mInitialBorderPosition);
239    }
240 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)241    void Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass) override
242    {
243       if(iPass == TrackArtist::PassSnapping && mSnap.Snapped())
244       {
245          auto &dc = context.dc;
246          SnapManager::Draw(&dc, rect.x + mSnap.outCoord, -1);
247       }
248    }
249 
DrawingArea(TrackPanelDrawingContext &,const wxRect & rect,const wxRect & panelRect,unsigned iPass)250    wxRect DrawingArea(TrackPanelDrawingContext&, const wxRect& rect, const wxRect& panelRect, unsigned iPass) override
251    {
252       if(iPass == TrackArtist::PassSnapping)
253         return MaximizeHeight(rect, panelRect);
254       return rect;
255    }
256 };
257 
258 class WaveClipTrimHandle::AdjustBetweenBorders final : public WaveClipTrimHandle::ClipTrimPolicy
259 {
260    std::pair<double, double> mRange;
261    std::vector<std::shared_ptr<WaveClip>> mLeftClips;
262    std::vector<std::shared_ptr<WaveClip>> mRightClips;
263    double mInitialBorderPosition{};
264    int mDragStartX{ };
265 
TrimTo(double t)266    void TrimTo(double t)
267    {
268       t = std::clamp(t, mRange.first, mRange.second);
269 
270       for (auto& clip : mLeftClips)
271          clip->TrimRightTo(t);
272       for (auto& clip : mRightClips)
273          clip->TrimLeftTo(t);
274    }
275 
276 public:
AdjustBetweenBorders(WaveTrack * track,std::shared_ptr<WaveClip> & leftClip,std::shared_ptr<WaveClip> & rightClip)277    AdjustBetweenBorders(
278       WaveTrack* track,
279       std::shared_ptr<WaveClip>& leftClip,
280       std::shared_ptr<WaveClip>& rightClip)
281    {
282       auto clips = track->GetClips();
283 
284       wxASSERT(std::find(clips.begin(), clips.end(), leftClip) != clips.end());
285       wxASSERT(std::find(clips.begin(), clips.end(), rightClip) != clips.end());
286 
287       if (track->IsAlignedWithLeader() || track->GetLinkType() == Track::LinkType::Aligned)
288       {
289          //find clips in other channels which are also should be trimmed
290          mLeftClips = FindClipsInChannels(leftClip->GetPlayStartTime(), leftClip->GetPlayEndTime(), track);
291          mRightClips = FindClipsInChannels(rightClip->GetPlayStartTime(), rightClip->GetPlayEndTime(), track);
292       }
293       else
294       {
295          mLeftClips.push_back(leftClip);
296          mRightClips.push_back(rightClip);
297       }
298 
299       mRange = std::make_pair(
300          //not less than 1 sample length
301          mLeftClips[0]->GetPlayStartTime() + 1.0 / mLeftClips[0]->GetRate(),
302          mRightClips[0]->GetPlayEndTime() - 1.0 / mRightClips[0]->GetRate()
303       );
304       mInitialBorderPosition = mRightClips[0]->GetPlayStartTime();
305    }
306 
Init(const TrackPanelMouseEvent & event)307    bool Init(const TrackPanelMouseEvent& event) override
308    {
309       if (event.event.LeftDown())
310       {
311          mDragStartX = event.event.GetX();
312          return true;
313       }
314       return false;
315    }
316 
Trim(const TrackPanelMouseEvent & event,AudacityProject & project)317    UIHandle::Result Trim(const TrackPanelMouseEvent& event, AudacityProject& project) override
318    {
319       const auto newX = event.event.GetX();
320       const auto dx = newX - mDragStartX;
321 
322       auto& viewInfo = ViewInfo::Get(project);
323 
324       auto eventT = viewInfo.PositionToTime(viewInfo.TimeToPosition(mInitialBorderPosition, event.rect.x) + dx, event.rect.x);
325       auto offset = sampleCount(
326          floor(
327             (eventT - mInitialBorderPosition) * mLeftClips[0]->GetRate()
328          )
329       ).as_double() / mLeftClips[0]->GetRate();
330 
331       TrimTo(mInitialBorderPosition + offset);
332 
333       return RefreshCode::RefreshCell;
334    }
335 
Finish(AudacityProject & project)336    void Finish(AudacityProject& project) override
337    {
338       if (mRightClips[0]->GetPlayStartTime() != mInitialBorderPosition)
339       {
340          auto dt = std::abs(mRightClips[0]->GetPlayStartTime() - mInitialBorderPosition);
341          ProjectHistory::Get(project).PushState(XO("Clip-Trim-Between"),
342                XO("Moved by %.02f").Format(dt), UndoPush::CONSOLIDATE);
343       }
344    }
345 
Cancel()346    void Cancel() override
347    {
348       TrimTo(mInitialBorderPosition);
349    }
350 };
351 
352 
HitPreview(const AudacityProject *,bool unsafe)353 HitTestPreview WaveClipTrimHandle::HitPreview(const AudacityProject*, bool unsafe)
354 {
355     static auto disabledCursor =
356         ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
357     static auto slideCursor =
358         MakeCursor(wxCURSOR_SIZEWE, TimeCursorXpm, 16, 16);
359     auto message = XO("Click and drag to move clip boundary in time");
360 
361     return {
362        message,
363        (unsafe
364         ? &*disabledCursor
365         : &*slideCursor)
366     };
367 }
368 
369 
WaveClipTrimHandle(std::unique_ptr<ClipTrimPolicy> & clipTrimPolicy)370 WaveClipTrimHandle::WaveClipTrimHandle(std::unique_ptr<ClipTrimPolicy>& clipTrimPolicy)
371    : mClipTrimPolicy{ std::move(clipTrimPolicy) }
372 {
373 
374 }
375 
Draw(TrackPanelDrawingContext &,const wxRect &,unsigned)376 void WaveClipTrimHandle::ClipTrimPolicy::Draw(TrackPanelDrawingContext&, const wxRect&, unsigned) { }
377 
DrawingArea(TrackPanelDrawingContext &,const wxRect & rect,const wxRect &,unsigned)378 wxRect WaveClipTrimHandle::ClipTrimPolicy::DrawingArea(TrackPanelDrawingContext&, const wxRect& rect, const wxRect&, unsigned)
379 {
380    return rect;
381 }
382 
HitAnywhere(std::weak_ptr<WaveClipTrimHandle> & holder,const std::shared_ptr<WaveTrack> & waveTrack,const AudacityProject * pProject,const TrackPanelMouseState & state)383 UIHandlePtr WaveClipTrimHandle::HitAnywhere(
384    std::weak_ptr<WaveClipTrimHandle>& holder,
385    const std::shared_ptr<WaveTrack>& waveTrack,
386    const AudacityProject* pProject,
387    const TrackPanelMouseState& state)
388 {
389     const auto rect = state.rect;
390 
391     auto px = state.state.m_x;
392 
393     auto& zoomInfo = ViewInfo::Get(*pProject);
394 
395     std::shared_ptr<WaveClip> leftClip;
396     std::shared_ptr<WaveClip> rightClip;
397 
398     //Test left and right boundaries of each clip
399     //to determine which type of trimming should be applied
400     //and input for the policy
401     for (auto& clip : waveTrack->GetClips())
402     {
403         if (!WaveTrackView::ClipDetailsVisible(*clip, zoomInfo, rect))
404            continue;
405 
406         auto clipRect = ClipParameters::GetClipRect(*clip.get(), zoomInfo, rect);
407 
408         //double the hit testing area in case if clip are close to each other
409         if (std::abs(px - clipRect.GetLeft()) <= BoundaryThreshold * 2)
410            rightClip = clip;
411         else if (std::abs(px - clipRect.GetRight()) <= BoundaryThreshold * 2)
412            leftClip = clip;
413     }
414 
415     std::unique_ptr<ClipTrimPolicy> clipTrimPolicy;
416     if (leftClip && rightClip)
417     {
418        //between adjacent clips
419        if(ClipParameters::GetClipRect(*leftClip, zoomInfo, rect).GetRight() > px)
420          clipTrimPolicy = std::make_unique<AdjustBorder>(waveTrack, leftClip, false, zoomInfo);//right border
421        else
422          clipTrimPolicy = std::make_unique<AdjustBorder>(waveTrack, rightClip, true, zoomInfo);//left border
423     }
424     else
425     {
426        auto clip = leftClip ? leftClip : rightClip;
427        if (clip)
428        {
429           //single clip case, determine the border,
430           //hit testing area differs from one
431           //used for general case
432           auto clipRect = ClipParameters::GetClipRect(*clip.get(), zoomInfo, rect);
433           if (std::abs(px - clipRect.GetLeft()) <= BoundaryThreshold)
434              clipTrimPolicy = std::make_unique<AdjustBorder>(waveTrack, clip, true, zoomInfo);
435           else if (std::abs(px - clipRect.GetRight()) <= BoundaryThreshold)
436              clipTrimPolicy = std::make_unique<AdjustBorder>(waveTrack, clip, false, zoomInfo);
437        }
438     }
439 
440     if(clipTrimPolicy)
441       return AssignUIHandlePtr(
442          holder,
443          std::make_shared<WaveClipTrimHandle>(clipTrimPolicy)
444       );
445     return { };
446 }
447 
HitTest(std::weak_ptr<WaveClipTrimHandle> & holder,WaveTrackView & view,const AudacityProject * pProject,const TrackPanelMouseState & state)448 UIHandlePtr WaveClipTrimHandle::HitTest(std::weak_ptr<WaveClipTrimHandle>& holder,
449     WaveTrackView& view, const AudacityProject* pProject,
450     const TrackPanelMouseState& state)
451 {
452     auto waveTrack = std::dynamic_pointer_cast<WaveTrack>(view.FindTrack());
453     //For multichannel tracks, show trim handle only for the leader track
454     if (!waveTrack->IsLeader() && waveTrack->IsAlignedWithLeader())
455         return {};
456 
457     std::vector<UIHandlePtr> results;
458 
459     const auto rect = state.rect;
460 
461     auto px = state.state.m_x;
462     auto py = state.state.m_y;
463 
464     if (py >= rect.GetTop() &&
465         py <= (rect.GetTop() + static_cast<int>(rect.GetHeight() * 0.3)))
466     {
467         return HitAnywhere(holder, waveTrack, pProject, state);
468     }
469     return {};
470 }
471 
472 
473 
Preview(const TrackPanelMouseState & mouseState,AudacityProject * pProject)474 HitTestPreview WaveClipTrimHandle::Preview(const TrackPanelMouseState& mouseState, AudacityProject* pProject)
475 {
476     const bool unsafe = ProjectAudioIO::Get(*pProject).IsAudioActive();
477     return HitPreview(pProject, unsafe);
478 }
479 
Click(const TrackPanelMouseEvent & event,AudacityProject * pProject)480 UIHandle::Result WaveClipTrimHandle::Click
481 (const TrackPanelMouseEvent& event, AudacityProject* pProject)
482 {
483    if (!ProjectAudioIO::Get(*pProject).IsAudioActive())
484    {
485       if (mClipTrimPolicy->Init(event))
486          return RefreshCode::RefreshNone;
487    }
488    return RefreshCode::Cancelled;
489 }
490 
Drag(const TrackPanelMouseEvent & event,AudacityProject * project)491 UIHandle::Result WaveClipTrimHandle::Drag
492 (const TrackPanelMouseEvent& event, AudacityProject* project)
493 {
494    return mClipTrimPolicy->Trim(event, *project);
495 }
496 
Release(const TrackPanelMouseEvent & event,AudacityProject * project,wxWindow * pParent)497 UIHandle::Result WaveClipTrimHandle::Release
498 (const TrackPanelMouseEvent& event, AudacityProject* project,
499     wxWindow* pParent)
500 {
501    mClipTrimPolicy->Finish(*project);
502    return RefreshCode::RefreshAll;
503 }
504 
Cancel(AudacityProject * pProject)505 UIHandle::Result WaveClipTrimHandle::Cancel(AudacityProject* pProject)
506 {
507    mClipTrimPolicy->Cancel();
508    return RefreshCode::RefreshAll;
509 }
510 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)511 void WaveClipTrimHandle::Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass)
512 {
513    mClipTrimPolicy->Draw(context, rect, iPass);
514 }
515 
DrawingArea(TrackPanelDrawingContext & context,const wxRect & rect,const wxRect & panelRect,unsigned iPass)516 wxRect WaveClipTrimHandle::DrawingArea(TrackPanelDrawingContext& context, const wxRect& rect,
517    const wxRect& panelRect, unsigned iPass)
518 {
519    return mClipTrimPolicy->DrawingArea(context, rect, panelRect, iPass);
520 }
521