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