Audacity 3.2.0
LabelGlyphHandle.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5LabelGlyphHandle.cpp
6
7Paul Licameli split from TrackPanel.cpp
8
9**********************************************************************/
10
11
12#include "LabelGlyphHandle.h"
13
14#include "LabelTrackView.h"
15#include "../../../HitTestResult.h"
16#include "../../../LabelTrack.h"
17#include "ProjectHistory.h"
18#include "../../../RefreshCode.h"
19#include "../../../TrackPanelMouseEvent.h"
20#include "UndoManager.h"
21#include "ViewInfo.h"
22#include "../../../SelectionState.h"
23#include "../../../ProjectAudioIO.h"
24#include "../../../../images/Cursors.h"
25#include "../../../tracks/ui/TimeShiftHandle.h"
26
27#include <wx/cursor.h>
28#include <wx/translation.h>
29
30LabelTrackHit::LabelTrackHit( const std::shared_ptr<LabelTrack> &pLT )
31 : mpLT{ pLT }
32{
33 pLT->Bind(
34 EVT_LABELTRACK_PERMUTED, &LabelTrackHit::OnLabelPermuted, this );
35}
36
38{
39 // Must do this because this sink isn't wxEvtHandler
40 mpLT->Unbind(
41 EVT_LABELTRACK_PERMUTED, &LabelTrackHit::OnLabelPermuted, this );
42}
43
45{
46 e.Skip();
47 if ( e.mpTrack.lock() != mpLT )
48 return;
49
50 auto former = e.mFormerPosition;
51 auto present = e.mPresentPosition;
52
53 auto update = [=]( int &index ){
54 if ( index == former )
55 index = present;
56 else if ( former < index && index <= present )
57 -- index;
58 else if ( former > index && index >= present )
59 ++ index;
60 };
61
62 update( mMouseOverLabelLeft );
63 update( mMouseOverLabelRight );
64 update( mMouseOverLabel );
65}
66
68(const std::shared_ptr<LabelTrack> &pLT,
69 const wxRect &rect, const std::shared_ptr<LabelTrackHit> &pHit)
70 : mpHit{ pHit }
71 , mpLT{ pLT }
72 , mRect{ rect }
73{
74}
75
77{
79}
80
82(const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState)
83{
84 if (oldState.mpHit->mEdge != newState.mpHit->mEdge)
85 // pointer moves between the circle and the chevron
87 return 0;
88}
89
91(std::weak_ptr<LabelGlyphHandle> &holder,
92 const wxMouseState &state,
93 const std::shared_ptr<LabelTrack> &pLT, const wxRect &rect)
94{
95 // Allocate on heap because there are pointers to it when it is bound as
96 // an event sink, therefore it's not copyable; make it shared so
97 // LabelGlyphHandle can be copyable:
98 auto pHit = std::make_shared<LabelTrackHit>( pLT );
99
100 LabelTrackView::OverGlyph(*pLT, *pHit, state.m_x, state.m_y);
101
102 // IF edge!=0 THEN we've set the cursor and we're done.
103 // signal this by setting the tip.
104 if ( pHit->mEdge & 3 )
105 {
106 auto result = std::make_shared<LabelGlyphHandle>( pLT, rect, pHit );
107 result = AssignUIHandlePtr(holder, result);
108 return result;
109 }
110
111 return {};
112}
113
115{
116}
117
119(LabelTrackHit &hit, const wxMouseEvent & evt,
120 const wxRect & r, const ZoomInfo &zoomInfo,
122{
123 if (evt.ButtonDown())
124 {
125 //OverGlyph sets mMouseOverLabel to be the chosen label.
126 const auto pTrack = mpLT;
127 LabelTrackView::OverGlyph(*pTrack, hit, evt.m_x, evt.m_y);
128
129 hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) &&
130 ( hit.mEdge & 3 ) != 0;
131
132 if (hit.mIsAdjustingLabel)
133 {
134 auto& view = LabelTrackView::Get(*pTrack);
135 view.ResetTextSelection();
136
137 double t = 0.0;
138
139 // When we start dragging the label(s) we don't want them to jump.
140 // so we calculate the displacement of the mouse from the drag center
141 // and use that in subsequent dragging calculations. The mouse stays
142 // at the same relative displacement throughout dragging.
143
144 // However, if two label's edges are being dragged
145 // then the displacement is relative to the initial average
146 // position of them, and in that case there can be a jump of at most
147 // a few pixels to bring the two label boundaries to exactly the same
148 // position when we start dragging.
149
150 // Dragging of three label edges at the same time is not supported (yet).
151
152 const auto &mLabels = pTrack->GetLabels();
153 if( ( hit.mMouseOverLabelRight >= 0 ) &&
154 ( hit.mMouseOverLabelLeft >= 0 )
155 )
156 {
157 t = (mLabels[ hit.mMouseOverLabelRight ].getT1() +
158 mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f;
159
160 // If we're moving two edges of same label then it's a move
161 // (label is shrunk to zero and size of zero is preserved)
162 // If we're on a boundary between two different labels,
163 // then it's an adjust.
164 // In both cases the two points coalesce.
165 //
166 // NOTE: seems that it's not necessary that hitting the both
167 // left and right handles mean that we're dealing with a point,
168 // but the range will be turned into a point on click
169 bool isPointLabel = hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight;
170 // Except! We don't coalesce if both ends are from the same label and
171 // we have deliberately chosen to preserve length, by holding shift down.
172 if (!(isPointLabel && evt.ShiftDown()))
173 {
174 MayAdjustLabel(hit, hit.mMouseOverLabelLeft, -1, false, t);
175 MayAdjustLabel(hit, hit.mMouseOverLabelRight, 1, false, t);
176 wxASSERT(mLabels[hit.mMouseOverLabelRight].getT1() ==
177 mLabels[hit.mMouseOverLabelLeft].getT0());
178 }
179 }
180 else if( hit.mMouseOverLabelRight >=0)
181 {
182 t = mLabels[ hit.mMouseOverLabelRight ].getT1();
183 }
184 else if( hit.mMouseOverLabelLeft >=0)
185 {
186 t = mLabels[ hit.mMouseOverLabelLeft ].getT0();
187 }
188 else if (hit.mMouseOverLabel >= 0)
189 {
190 t = mLabels[hit.mMouseOverLabel].getT0();
191 }
192 mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x;
193 }
194 }
195}
196
198(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
199{
200 auto result = LabelDefaultClickHandle::Click( evt, pProject );
201
202 const wxMouseEvent &event = evt.event;
203 auto& selectionState = SelectionState::Get(*pProject);
204 auto& tracks = TrackList::Get(*pProject);
205
206 auto &viewInfo = ViewInfo::Get( *pProject );
208 *mpHit, event, mRect, viewInfo, viewInfo.selectedRegion);
209
210 if (! mpHit->mIsAdjustingLabel )
211 {
212 // The positive hit test should have ensured otherwise
213 //wxASSERT(false);
214 result |= RefreshCode::Cancelled;
215 }
216 else
217 // redraw the track.
218 result |= RefreshCode::RefreshCell;
219
220 return result;
221}
222
229( LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
230{
231 if( iLabel < 0 )
232 return;
233
234 const auto pTrack = mpLT;
235 const auto &mLabels = pTrack->GetLabels();
236 auto labelStruct = mLabels[ iLabel ];
237
238 // Adjust the requested edge.
239 bool flipped = labelStruct.AdjustEdge( iEdge, fNewTime );
240 // If the edges did not swap, then we are done.
241 if( ! flipped ) {
242 pTrack->SetLabel( iLabel, labelStruct );
243 return;
244 }
245
246 // If swapping's not allowed we must also move the edge
247 // we didn't move. Then we're done.
248 if( !bAllowSwapping )
249 {
250 labelStruct.AdjustEdge( -iEdge, fNewTime );
251 pTrack->SetLabel( iLabel, labelStruct );
252 return;
253 }
254
255 pTrack->SetLabel( iLabel, labelStruct );
256
257 // Swap our record of what we are dragging.
259}
260
261// If the index is for a real label, adjust its left and right boundary.
262void LabelGlyphHandle::MayMoveLabel( int iLabel, int iEdge, double fNewTime)
263{
264 if( iLabel < 0 )
265 return;
266
267 const auto pTrack = mpLT;
268 const auto &mLabels = pTrack->GetLabels();
269 auto labelStruct = mLabels[ iLabel ];
270 labelStruct.MoveLabel( iEdge, fNewTime );
271 pTrack->SetLabel( iLabel, labelStruct );
272}
273
274// Constrain function, as in processing/arduino.
275// returned value will be between min and max (inclusive).
276static int Constrain( int value, int min, int max )
277{
278 wxASSERT( min <= max );
279 int result=value;
280 if( result < min )
281 result=min;
282 if( result > max )
283 result=max;
284 return result;
285}
286
288(AudacityProject &project,
289 LabelTrackHit &hit, const wxMouseEvent & evt,
290 wxRect & r, const ZoomInfo &zoomInfo,
292{
293 const auto pTrack = mpLT;
294 const auto &mLabels = pTrack->GetLabels();
295 if(evt.LeftUp())
296 {
297 bool updated = false;
298 if( hit.mMouseOverLabelLeft >= 0 ) {
299 auto labelStruct = mLabels[ hit.mMouseOverLabelLeft ];
300 updated |= labelStruct.updated;
301 labelStruct.updated = false;
302 pTrack->SetLabel( hit.mMouseOverLabelLeft, labelStruct );
303 }
304 if( hit.mMouseOverLabelRight >= 0 ) {
305 auto labelStruct = mLabels[ hit.mMouseOverLabelRight ];
306 updated |= labelStruct.updated;
307 labelStruct.updated = false;
308 pTrack->SetLabel( hit.mMouseOverLabelRight, labelStruct );
309 }
310
311 if (hit.mMouseOverLabel >= 0)
312 {
313 auto labelStruct = mLabels[hit.mMouseOverLabel];
314 if (!labelStruct.updated)
315 {
316 //happens on click over bar between handles (without moving a cursor)
317 newSel = labelStruct.selectedRegion;
318
319 // IF the user clicked a label, THEN select all other tracks by Label
320 // do nothing if at least one other track is selected
321 auto& selectionState = SelectionState::Get(project);
322 auto& tracks = TrackList::Get(project);
323
324 bool done = tracks.Selected().any_of(
325 [&](const Track* track) { return track != static_cast<Track*>(pTrack.get()); }
326 );
327
328 if (!done) {
329 //otherwise, select all tracks
330 for (auto t : tracks.Any())
331 selectionState.SelectTrack(*t, true, true);
332 }
333
334 // Do this after, for its effect on TrackPanel's memory of last selected
335 // track (which affects shift-click actions)
336 selectionState.SelectTrack(*pTrack.get(), true, true);
337
338 // PRL: bug1659 -- make selection change undo correctly
339 updated = !ProjectAudioIO::Get(project).IsAudioActive();
340
341 auto& view = LabelTrackView::Get(*pTrack);
342 view.SetNavigationIndex(hit.mMouseOverLabel);
343 }
344 else
345 {
346 labelStruct.updated = false;
347 pTrack->SetLabel(hit.mMouseOverLabel, labelStruct);
348 updated = true;
349 }
350 }
351
352 hit.mIsAdjustingLabel = false;
353 hit.mMouseOverLabelLeft = -1;
354 hit.mMouseOverLabelRight = -1;
355 hit.mMouseOverLabel = -1;
356 return updated;
357 }
358
359 if(evt.Dragging())
360 {
361 //If we are currently adjusting a label,
362 //just reset its value and redraw.
363 // LL: Constrain to inside track rectangle for now. Should be changed
364 // to allow scrolling while dragging labels
365 int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width);
366
367 double fNewX = zoomInfo.PositionToTime(x, 0);
368 // Moving the whole ranged label(s)
369 if (hit.mMouseOverLabel != -1)
370 {
371 if (evt.ShiftDown())
372 {
373 auto dt = fNewX - mLabels[hit.mMouseOverLabel].getT0();
374 for (auto i = 0, count = static_cast<int>(mLabels.size()); i < count; ++i)
375 MayMoveLabel(i, -1, mLabels[i].getT0() + dt);
376 }
377 else
378 MayMoveLabel(hit.mMouseOverLabel, -1, fNewX);
379 }
380 // If we're on the 'dot' and nowe're moving,
381 // Though shift-down inverts that.
382 // and if both edges the same, then we're always moving the label.
383 else if((hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight) || evt.ShiftDown())
384 {
385 MayMoveLabel( hit.mMouseOverLabelLeft, -1, fNewX );
386 MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX );
387 }
388 else
389 {
390 // If exactly one edge is selected we allow swapping
391 bool bAllowSwapping =
392 (hit.mMouseOverLabelLeft >= 0) !=
393 (hit.mMouseOverLabelRight >= 0);
394 MayAdjustLabel( hit, hit.mMouseOverLabelLeft, -1, bAllowSwapping, fNewX );
395 MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX );
396 }
397
398 const auto &view = LabelTrackView::Get( *pTrack );
399 auto navigationIndex = view.GetNavigationIndex(project);
400 if(navigationIndex != -1 &&
401 (navigationIndex == hit.mMouseOverLabel ||
402 navigationIndex == hit.mMouseOverLabelLeft ||
403 navigationIndex == hit.mMouseOverLabelRight))
404 {
405 //Set the selection region to be equal to
406 //the NEW size of the label.
407 newSel = mLabels[navigationIndex].selectedRegion;
408 }
409 pTrack->SortLabels();
410 }
411
412 return false;
413}
414
416(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
417{
418 auto result = LabelDefaultClickHandle::Drag( evt, pProject );
419
420 const wxMouseEvent &event = evt.event;
421 auto &viewInfo = ViewInfo::Get( *pProject );
423 *pProject, *mpHit, event, mRect, viewInfo, viewInfo.selectedRegion);
424
425 // Refresh all so that the change of selection is redrawn in all tracks
427}
428
431{
432 static wxCursor arrowCursor{ wxCURSOR_ARROW };
433 static auto handOpenCursor =
434 MakeCursor(wxCURSOR_HAND, RearrangeCursorXpm, 16, 16);
435 static auto handClosedCursor =
436 MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
437
438 if (mpHit->mMouseOverLabel != -1)
439 {
440 return {
441 XO("Drag label. Hold shift and drag to move all labels on the same track."),
442 mpHit->mIsAdjustingLabel ? &*handClosedCursor : &*handOpenCursor
443 };
444 }
445 else if ((mpHit->mEdge & 4) != 0)
446 return { XO("Drag one or more label boundaries."), &arrowCursor };
447 else
448 return { XO("Drag label boundary."), &arrowCursor };
449}
450
452(const TrackPanelMouseEvent &evt, AudacityProject *pProject,
453 wxWindow *pParent)
454{
455 auto result = LabelDefaultClickHandle::Release( evt, pProject, pParent );
456
457 const wxMouseEvent &event = evt.event;
458 auto &viewInfo = ViewInfo::Get( *pProject );
460 *pProject, *mpHit, event, mRect, viewInfo, viewInfo.selectedRegion)) {
461 ProjectHistory::Get( *pProject ).PushState(XO("Modified Label"),
462 XO("Label Edit"),
464 }
465
466 // Refresh all so that the change of selection is redrawn in all tracks
468}
469
471{
472 ProjectHistory::Get( *pProject ).RollbackState();
473 auto result = LabelDefaultClickHandle::Cancel( pProject );
474 return result | RefreshCode::RefreshAll;
475}
static auto handOpenCursor
std::shared_ptr< UIHandle > UIHandlePtr
Definition: CellularPanel.h:28
int min(int a, int b)
#define XO(s)
Definition: Internat.h:31
static int Constrain(int value, int min, int max)
std::unique_ptr< wxCursor > MakeCursor(int WXUNUSED(CursorId), const char *const pXpm[36], int HotX, int HotY)
Definition: TrackPanel.cpp:185
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:151
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:89
Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
Result Cancel(AudacityProject *pProject) override
Result Cancel(AudacityProject *pProject) override
void Enter(bool forward, AudacityProject *) override
std::shared_ptr< LabelTrackHit > mpHit
Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
void MayAdjustLabel(LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
bool HandleGlyphDragRelease(AudacityProject &project, LabelTrackHit &hit, const wxMouseEvent &evt, wxRect &r, const ZoomInfo &zoomInfo, NotifyingSelectedRegion &newSel)
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
std::shared_ptr< LabelTrack > mpLT
static UIHandlePtr HitTest(std::weak_ptr< LabelGlyphHandle > &holder, const wxMouseState &state, const std::shared_ptr< LabelTrack > &pLT, const wxRect &rect)
static UIHandle::Result NeedChangeHighlight(const LabelGlyphHandle &oldState, const LabelGlyphHandle &newState)
void MayMoveLabel(int iLabel, int iEdge, double fNewTime)
void HandleGlyphClick(LabelTrackHit &hit, const wxMouseEvent &evt, const wxRect &r, const ZoomInfo &zoomInfo, NotifyingSelectedRegion &newSel)
LabelGlyphHandle(const std::shared_ptr< LabelTrack > &pLT, const wxRect &rect, const std::shared_ptr< LabelTrackHit > &pHit)
HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
int mxMouseDisplacement
Displacement of mouse cursor from the centre being dragged.
static LabelTrackView & Get(LabelTrack &)
static void OverGlyph(const LabelTrack &track, LabelTrackHit &hit, int x, int y)
bool IsAudioActive() const
static ProjectAudioIO & Get(AudacityProject &project)
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
static SelectionState & Get(AudacityProject &project)
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:225
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:486
Result mChangeHighlight
Definition: UIHandle.h:139
unsigned Result
Definition: UIHandle.h:38
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
double PositionToTime(wxInt64 position, wxInt64 origin=0, bool ignoreFisheye=false) const
Definition: ZoomInfo.cpp:41
wxInt64 TimeToPosition(double time, wxInt64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ZoomInfo.cpp:51
void swap(std::unique_ptr< Alg_seq > &a, std::unique_ptr< Alg_seq > &b)
Definition: NoteTrack.cpp:753
const std::weak_ptr< Track > mpTrack
Definition: LabelTrack.h:225
int mMouseOverLabelRight
Keeps track of which left label the mouse is currently over.
LabelTrackHit(const std::shared_ptr< LabelTrack > &pLT)
int mMouseOverLabelLeft
Keeps track of which (ranged) label the mouse is currently over.
std::shared_ptr< LabelTrack > mpLT
void OnLabelPermuted(LabelTrackEvent &e)
bool mIsAdjustingLabel
Keeps track of which right label the mouse is currently over.