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