1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 TrackSelectHandle.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "TrackSelectHandle.h"
13 
14 #include "TrackView.h"
15 #include "../../HitTestResult.h"
16 #include "Project.h"
17 #include "../../ProjectAudioIO.h"
18 #include "../../ProjectHistory.h"
19 #include "../../RefreshCode.h"
20 #include "../../SelectUtilities.h"
21 #include "../../TrackPanelMouseEvent.h"
22 #include "../../Track.h"
23 
24 #include <wx/cursor.h>
25 #include <wx/translation.h>
26 
27 #include "../../../images/Cursors.h"
28 
29 #if defined(__WXMAC__)
30 /* i18n-hint: Command names a modifier key on Macintosh keyboards */
31 #define CTRL_CLICK XO("Command+Click")
32 #else
33 /* i18n-hint: Ctrl names a modifier key on Windows or Linux keyboards */
34 #define CTRL_CLICK XO("Ctrl+Click")
35 #endif
36 
37 namespace {
Message(unsigned trackCount)38    TranslatableString Message(unsigned trackCount) {
39       if (trackCount > 1)
40          return XO(
41 // i18n-hint: %s is replaced by (translation of) 'Ctrl+Click' on windows, 'Command+Click' on Mac
42 "%s to select or deselect track. Drag up or down to change track order.")
43             .Format( CTRL_CLICK );
44       else
45          // i18n-hint: %s is replaced by (translation of) 'Ctrl+Click' on windows, 'Command+Click' on Mac
46          return XO("%s to select or deselect track.")
47             .Format( CTRL_CLICK );
48    }
49 }
50 
TrackSelectHandle(const std::shared_ptr<Track> & pTrack)51 TrackSelectHandle::TrackSelectHandle( const std::shared_ptr<Track> &pTrack )
52    : mpTrack( pTrack )
53 {}
54 
HitAnywhere(std::weak_ptr<TrackSelectHandle> & holder,const std::shared_ptr<Track> & pTrack)55 UIHandlePtr TrackSelectHandle::HitAnywhere
56 (std::weak_ptr<TrackSelectHandle> &holder,
57  const std::shared_ptr<Track> &pTrack)
58 {
59    auto result = std::make_shared<TrackSelectHandle>(pTrack);
60    result = AssignUIHandlePtr(holder, result);
61    return result;
62 }
63 
~TrackSelectHandle()64 TrackSelectHandle::~TrackSelectHandle()
65 {
66 }
67 
Click(const TrackPanelMouseEvent & evt,AudacityProject * pProject)68 UIHandle::Result TrackSelectHandle::Click
69 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
70 {
71    // If unsafe to drag, still, it does harmlessly change the selected track
72    // set on button down.
73 
74    using namespace RefreshCode;
75    Result result = RefreshNone;
76 
77    const wxMouseEvent &event = evt.event;
78 
79    // AS: If not a click, ignore the mouse event.
80    if (!event.ButtonDown() && !event.ButtonDClick())
81       return Cancelled;
82    if (!event.Button(wxMOUSE_BTN_LEFT))
83       return Cancelled;
84 
85    const auto pTrack = mpTrack;
86    if (!pTrack)
87       return Cancelled;
88    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
89 
90    // DM: If they weren't clicking on a particular part of a track label,
91    //  deselect other tracks and select this one.
92 
93    // JH: also, capture the current track for rearranging, so the user
94    //  can drag the track up or down to swap it with others
95    if (unsafe)
96       result |= Cancelled;
97    else {
98       mRearrangeCount = 0;
99       CalculateRearrangingThresholds(event, pProject);
100    }
101 
102    SelectUtilities::DoListSelection(*pProject,
103       pTrack.get(), event.ShiftDown(), event.ControlDown(), !unsafe);
104 
105    mClicked = true;
106    return result;
107 }
108 
Drag(const TrackPanelMouseEvent & evt,AudacityProject * pProject)109 UIHandle::Result TrackSelectHandle::Drag
110 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
111 {
112    using namespace RefreshCode;
113    Result result = RefreshNone;
114 
115    const wxMouseEvent &event = evt.event;
116 
117    auto &tracks = TrackList::Get( *pProject );
118 
119    // probably harmless during play?  However, we do disallow the click, so check this too.
120    bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
121    if (unsafe)
122       return result;
123 
124    if (event.m_y < mMoveUpThreshold || event.m_y < 0) {
125       tracks.MoveUp(mpTrack.get());
126       --mRearrangeCount;
127    }
128    else if ( event.m_y > mMoveDownThreshold
129       || event.m_y > evt.whole.GetHeight() ) {
130       tracks.MoveDown(mpTrack.get());
131       ++mRearrangeCount;
132    }
133    else
134       return result;
135 
136    // JH: if we moved up or down, recalculate the thresholds and make sure the
137    // track is fully on-screen.
138    CalculateRearrangingThresholds(event, pProject);
139 
140    result |= EnsureVisible | RefreshAll;
141    return result;
142 }
143 
Preview(const TrackPanelMouseState &,AudacityProject * project)144 HitTestPreview TrackSelectHandle::Preview
145 (const TrackPanelMouseState &, AudacityProject *project)
146 {
147    static auto disabledCursor =
148       ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
149    //static wxCursor rearrangeCursor{ wxCURSOR_HAND };
150    static auto rearrangingCursor =
151       ::MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
152    static wxCursor arrowCursor{ wxCURSOR_ARROW };
153 
154    //static auto hoverCursor =
155    //   ::MakeCursor(wxCURSOR_HAND, RearrangeCursorXpm, 16, 16);
156    //static auto clickedCursor =
157    //   ::MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
158 
159    const auto trackCount = TrackList::Get( *project ).Leaders().size();
160    auto message = Message(trackCount);
161    if (mClicked) {
162       const bool unsafe =
163          ProjectAudioIO::Get( *project ).IsAudioActive();
164       const bool canMove = TrackList::Get( *project ).Leaders().size() > 1;
165       return {
166          message,
167          (unsafe
168           ? &*disabledCursor
169           : canMove
170              ? &*rearrangingCursor
171              : &arrowCursor)
172          // , message // Stop showing the tooltip after the click
173       };
174    }
175    else {
176       // Only mouse-over
177       // Don't test safety, because the click to change selection is allowed
178       return {
179          message,
180          &arrowCursor,
181          message
182       };
183    }
184 }
185 
Release(const TrackPanelMouseEvent &,AudacityProject * project,wxWindow *)186 UIHandle::Result TrackSelectHandle::Release
187 (const TrackPanelMouseEvent &, AudacityProject *project, wxWindow *)
188 {
189    // If we're releasing, surely we are dragging a track?
190    wxASSERT( mpTrack );
191    if (mRearrangeCount != 0) {
192       ProjectHistory::Get( *project ).PushState(
193          /* i18n-hint: will substitute name of track for %s */
194          ( mRearrangeCount < 0 ? XO("Moved '%s' up") : XO("Moved '%s' down") )
195             .Format( mpTrack->GetName() ),
196          XO("Move Track"));
197    }
198    // Bug 1677
199    // Holding on to the reference to the track was causing it to be released far later
200    // than necessary, on shutdown, and so causing a crash as a dialog about cleaning
201    // out files could not show at that time.
202    mpTrack.reset();
203    // No need to redraw, that was done when drag moved the track
204    return RefreshCode::RefreshNone;
205 }
206 
Cancel(AudacityProject * pProject)207 UIHandle::Result TrackSelectHandle::Cancel(AudacityProject *pProject)
208 {
209    ProjectHistory::Get( *pProject ).RollbackState();
210    // Bug 1677
211    mpTrack.reset();
212    return RefreshCode::RefreshAll;
213 }
214 
215 /// Figure out how far the user must drag the mouse up or down
216 /// before the track will swap with the one above or below
CalculateRearrangingThresholds(const wxMouseEvent & event,AudacityProject * project)217 void TrackSelectHandle::CalculateRearrangingThresholds(
218    const wxMouseEvent & event, AudacityProject *project)
219 {
220    // JH: this will probably need to be tweaked a bit, I'm just
221    //   not sure what formula will have the best feel for the
222    //   user.
223 
224    auto &tracks = TrackList::Get( *project );
225 
226    if (tracks.CanMoveUp(mpTrack.get()))
227       mMoveUpThreshold =
228          event.m_y -
229             TrackView::GetChannelGroupHeight(
230                * -- tracks.FindLeader( mpTrack.get() ) );
231    else
232       mMoveUpThreshold = INT_MIN;
233 
234    if (tracks.CanMoveDown(mpTrack.get()))
235       mMoveDownThreshold =
236          event.m_y +
237             TrackView::GetChannelGroupHeight(
238                * ++ tracks.FindLeader( mpTrack.get() ) );
239    else
240       mMoveDownThreshold = INT_MAX;
241 }
242