1 /*!********************************************************************
2 *
3 Audacity: A Digital Audio Editor
4
5 WaveTrackAffordanceControls.cpp
6
7 Vitaly Sverchinsky
8
9 **********************************************************************/
10
11 #include "WaveTrackAffordanceControls.h"
12
13 #include <wx/dc.h>
14 #include <wx/frame.h>
15
16 #include "AllThemeResources.h"
17 #include "../../../../commands/CommandContext.h"
18 #include "../../../../commands/CommandFlag.h"
19 #include "../../../../commands/CommandFunctors.h"
20 #include "../../../../commands/CommandManager.h"
21 #include "../../../../TrackPanelMouseEvent.h"
22 #include "../../../../TrackArtist.h"
23 #include "../../../../TrackPanelDrawingContext.h"
24 #include "../../../../TrackPanelResizeHandle.h"
25 #include "ViewInfo.h"
26 #include "../../../../WaveTrack.h"
27 #include "../../../../WaveClip.h"
28 #include "../../../../UndoManager.h"
29 #include "../../../../ShuttleGui.h"
30 #include "../../../../ProjectWindows.h"
31 #include "../../../../commands/AudacityCommand.h"
32
33 #include "../../../ui/TextEditHelper.h"
34 #include "../../../ui/SelectHandle.h"
35 #include "WaveTrackView.h"//need only ClipParameters
36 #include "WaveTrackAffordanceHandle.h"
37
38 #include "../../../../ProjectHistory.h"
39 #include "../../../../ProjectSettings.h"
40 #include "../../../../SelectionState.h"
41 #include "../../../../RefreshCode.h"
42 #include "Theme.h"
43 #include "../../../../../images/Cursors.h"
44 #include "../../../../HitTestResult.h"
45 #include "../../../../TrackPanel.h"
46 #include "../../../../TrackPanelAx.h"
47
48 #include "../WaveTrackUtils.h"
49
50 #include "WaveClipTrimHandle.h"
51
52
53
54 class SetWaveClipNameCommand : public AudacityCommand
55 {
56 public:
57 static const ComponentInterfaceSymbol Symbol;
58
GetSymbol()59 ComponentInterfaceSymbol GetSymbol() override
60 {
61 return Symbol;
62 }
PopulateOrExchange(ShuttleGui & S)63 void PopulateOrExchange(ShuttleGui& S) override
64 {
65 S.AddSpace(0, 5);
66
67 S.StartMultiColumn(2, wxALIGN_CENTER);
68 {
69 S.TieTextBox(XXO("Name:"), mName, 60);
70 }
71 S.EndMultiColumn();
72 }
73 public:
74 wxString mName;
75 };
76
77 const ComponentInterfaceSymbol SetWaveClipNameCommand::Symbol
78 { XO("Set Wave Clip Name") };
79
80 //Handle which is used to send mouse events to TextEditHelper
81 class WaveClipTitleEditHandle final : public UIHandle
82 {
83 std::shared_ptr<TextEditHelper> mHelper;
84 public:
85
WaveClipTitleEditHandle(const std::shared_ptr<TextEditHelper> & helper)86 WaveClipTitleEditHandle(const std::shared_ptr<TextEditHelper>& helper)
87 : mHelper(helper)
88 { }
89
~WaveClipTitleEditHandle()90 ~WaveClipTitleEditHandle()
91 {
92 }
93
Click(const TrackPanelMouseEvent & event,AudacityProject * project)94 Result Click(const TrackPanelMouseEvent& event, AudacityProject* project) override
95 {
96 if (mHelper->OnClick(event.event, project))
97 return RefreshCode::RefreshCell;
98 return RefreshCode::RefreshNone;
99 }
100
Drag(const TrackPanelMouseEvent & event,AudacityProject * project)101 Result Drag(const TrackPanelMouseEvent& event, AudacityProject* project) override
102 {
103 if (mHelper->OnDrag(event.event, project))
104 return RefreshCode::RefreshCell;
105 return RefreshCode::RefreshNone;
106 }
107
Preview(const TrackPanelMouseState & state,AudacityProject * pProject)108 HitTestPreview Preview(const TrackPanelMouseState& state, AudacityProject* pProject) override
109 {
110 static auto ibeamCursor =
111 ::MakeCursor(wxCURSOR_IBEAM, IBeamCursorXpm, 17, 16);
112 return {
113 XO("Click and drag to select text"),
114 ibeamCursor.get()
115 };
116 }
117
Release(const TrackPanelMouseEvent & event,AudacityProject * project,wxWindow *)118 Result Release(const TrackPanelMouseEvent& event, AudacityProject* project, wxWindow*) override
119 {
120 if (mHelper->OnRelease(event.event, project))
121 return RefreshCode::RefreshCell;
122 return RefreshCode::RefreshNone;
123 }
124
Cancel(AudacityProject * project)125 Result Cancel(AudacityProject* project) override
126 {
127 if (mHelper)
128 {
129 mHelper->Cancel(project);
130 mHelper.reset();
131 }
132 return RefreshCode::RefreshAll;
133 }
134 };
135
WaveTrackAffordanceControls(const std::shared_ptr<Track> & pTrack)136 WaveTrackAffordanceControls::WaveTrackAffordanceControls(const std::shared_ptr<Track>& pTrack)
137 : CommonTrackCell(pTrack), mClipNameFont(wxFont(wxFontInfo()))
138 {
139 if (auto trackList = pTrack->GetOwner())
140 {
141 trackList->Bind(EVT_TRACKLIST_SELECTION_CHANGE,
142 &WaveTrackAffordanceControls::OnTrackChanged,
143 this);
144 }
145 }
146
HitTest(const TrackPanelMouseState & state,const AudacityProject * pProject)147 std::vector<UIHandlePtr> WaveTrackAffordanceControls::HitTest(const TrackPanelMouseState& state, const AudacityProject* pProject)
148 {
149 std::vector<UIHandlePtr> results;
150
151 auto px = state.state.m_x;
152 auto py = state.state.m_y;
153
154 const auto rect = state.rect;
155
156 auto track = std::static_pointer_cast<WaveTrack>(FindTrack());
157
158 {
159 auto handle = WaveClipTrimHandle::HitAnywhere(
160 mClipTrimHandle,
161 track,
162 pProject,
163 state);
164
165 if (handle)
166 results.push_back(handle);
167 }
168
169 auto trackList = track->GetOwner();
170 if ((std::abs(rect.GetTop() - py) <= WaveTrackView::kChannelSeparatorThickness / 2)
171 && trackList
172 && !track->IsLeader())
173 {
174 //given that track is not a leader there always should be
175 //another track before this one
176 auto prev = std::prev(trackList->Find(track.get()));
177 results.push_back(
178 AssignUIHandlePtr(
179 mResizeHandle,
180 std::make_shared<TrackPanelResizeHandle>((*prev)->shared_from_this(), py)
181 )
182 );
183 }
184
185 if (mTextEditHelper && mTextEditHelper->GetBBox().Contains(px, py))
186 {
187 results.push_back(
188 AssignUIHandlePtr(
189 mTitleEditHandle,
190 std::make_shared<WaveClipTitleEditHandle>(mTextEditHelper)
191 )
192 );
193 }
194
195 auto editClipLock = mEditedClip.lock();
196 const auto waveTrack = std::static_pointer_cast<WaveTrack>(track->SubstitutePendingChangedTrack());
197 auto& zoomInfo = ViewInfo::Get(*pProject);
198 for (const auto& clip : waveTrack->GetClips())
199 {
200 if (clip == editClipLock)
201 continue;
202
203 if (WaveTrackView::HitTest(*clip, zoomInfo, state.rect, {px, py}))
204 {
205 results.push_back(
206 AssignUIHandlePtr(
207 mAffordanceHandle,
208 std::make_shared<WaveTrackAffordanceHandle>(track, clip)
209 )
210 );
211 mFocusClip = clip;
212 break;
213 }
214 }
215
216 const auto& settings = ProjectSettings::Get(*pProject);
217 const auto currentTool = settings.GetTool();
218 if (currentTool == ToolCodes::multiTool || currentTool == ToolCodes::selectTool)
219 {
220 results.push_back(
221 SelectHandle::HitTest(
222 mSelectHandle, state, pProject, std::static_pointer_cast<TrackView>(track->GetTrackView())
223 )
224 );
225 }
226
227 return results;
228 }
229
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)230 void WaveTrackAffordanceControls::Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass)
231 {
232 if (iPass == TrackArtist::PassBackground) {
233 auto track = FindTrack();
234 const auto artist = TrackArtist::Get(context);
235
236 TrackArt::DrawBackgroundWithSelection(context, rect, track.get(), artist->blankSelectedBrush, artist->blankBrush);
237
238 const auto waveTrack = std::static_pointer_cast<WaveTrack>(track->SubstitutePendingChangedTrack());
239 const auto& zoomInfo = *artist->pZoomInfo;
240
241 {
242 wxDCClipper dcClipper(context.dc, rect);
243
244 context.dc.SetTextBackground(wxTransparentColor);
245 context.dc.SetTextForeground(theTheme.Colour(clrClipNameText));
246 context.dc.SetFont(mClipNameFont);
247
248 auto px = context.lastState.m_x;
249 auto py = context.lastState.m_y;
250
251 for (const auto& clip : waveTrack->GetClips())
252 {
253 auto affordanceRect
254 = ClipParameters::GetClipRect(*clip.get(), zoomInfo, rect);
255
256 if(!WaveTrackView::ClipDetailsVisible(*clip, zoomInfo, rect))
257 {
258 TrackArt::DrawClipFolded(context.dc, affordanceRect);
259 continue;
260 }
261
262 auto selected = GetSelectedClip().lock() == clip;
263 auto highlight = selected || affordanceRect.Contains(px, py);
264 if (mTextEditHelper && mEditedClip.lock() == clip)
265 {
266 TrackArt::DrawClipAffordance(context.dc, affordanceRect, wxEmptyString, highlight, selected);
267 mTextEditHelper->Draw(context.dc, TrackArt::GetAffordanceTitleRect(affordanceRect));
268 }
269 else
270 TrackArt::DrawClipAffordance(context.dc, affordanceRect, clip->GetName(), highlight, selected);
271
272 }
273 }
274
275 }
276 }
277
StartEditClipName(AudacityProject * project)278 bool WaveTrackAffordanceControls::StartEditClipName(AudacityProject* project)
279 {
280 if (auto lock = mFocusClip.lock())
281 {
282 auto clip = lock.get();
283
284 bool useDialog{ false };
285 gPrefs->Read(wxT("/GUI/DialogForNameNewLabel"), &useDialog, false);
286
287 if (useDialog)
288 {
289 SetWaveClipNameCommand Command;
290 auto oldName = clip->GetName();
291 Command.mName = oldName;
292 auto result = Command.PromptUser(&GetProjectFrame(*project));
293 if (result && Command.mName != oldName)
294 {
295 clip->SetName(Command.mName);
296 ProjectHistory::Get(*project).PushState(XO("Modified Clip Name"),
297 XO("Clip Name Edit"));
298 }
299 }
300 else
301 {
302 if (mTextEditHelper)
303 mTextEditHelper->Finish(project);
304
305 mEditedClip = lock;
306 mTextEditHelper = MakeTextEditHelper(clip->GetName());
307 }
308 return true;
309 }
310 return false;
311 }
312
GetSelectedClip() const313 std::weak_ptr<WaveClip> WaveTrackAffordanceControls::GetSelectedClip() const
314 {
315 if (auto handle = mAffordanceHandle.lock())
316 {
317 return handle->Clicked() ? mFocusClip : std::weak_ptr<WaveClip>();
318 }
319 return {};
320 }
321
322 namespace {
323
FindAffordance(WaveTrack & track)324 auto FindAffordance(WaveTrack &track)
325 {
326 auto &view = TrackView::Get( track );
327 auto pAffordance = view.GetAffordanceControls();
328 return std::dynamic_pointer_cast<WaveTrackAffordanceControls>(
329 pAffordance );
330 }
331
332 std::pair<WaveTrack *, WaveClip *>
SelectedClipOfFocusedTrack(AudacityProject & project)333 SelectedClipOfFocusedTrack(AudacityProject &project)
334 {
335 // Note that TrackFocus may change its state as a side effect, defining
336 // a track focus if there was none
337 if (auto pWaveTrack =
338 dynamic_cast<WaveTrack *>(TrackFocus::Get(project).Get())) {
339 for (auto pChannel : TrackList::Channels(pWaveTrack)) {
340 if (FindAffordance(*pChannel)) {
341 auto &viewInfo = ViewInfo::Get(project);
342 auto &clips = pChannel->GetClips();
343 auto begin = clips.begin(), end = clips.end(),
344 iter = WaveTrackUtils::SelectedClip(viewInfo, begin, end);
345 if (iter != end)
346 return { pChannel, iter->get() };
347 }
348 }
349 }
350 return { nullptr, nullptr };
351 }
352
353 // condition for enabling the command
SomeClipIsSelectedFlag()354 const ReservedCommandFlag &SomeClipIsSelectedFlag()
355 {
356 static ReservedCommandFlag flag{
357 [](const AudacityProject &project){
358 return nullptr !=
359 // const_cast isn't pretty but not harmful in this case
360 SelectedClipOfFocusedTrack(const_cast<AudacityProject&>(project))
361 .second;
362 }
363 };
364 return flag;
365 }
366
367 }
368
CaptureKey(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * pParent,AudacityProject * project)369 unsigned WaveTrackAffordanceControls::CaptureKey(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent, AudacityProject* project)
370 {
371 if (!mTextEditHelper
372 || !mTextEditHelper->CaptureKey(event.GetKeyCode(), event.GetModifiers()))
373 // Handle the event if it can be processed by the text editor (if any)
374 event.Skip();
375 return RefreshCode::RefreshNone;
376 }
377
378
KeyDown(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow *,AudacityProject * project)379 unsigned WaveTrackAffordanceControls::KeyDown(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow*, AudacityProject* project)
380 {
381 auto keyCode = event.GetKeyCode();
382
383 if (mTextEditHelper)
384 {
385 if (!mTextEditHelper->OnKeyDown(keyCode, event.GetModifiers(), project)
386 && !TextEditHelper::IsGoodEditKeyCode(keyCode))
387 event.Skip();
388
389 return RefreshCode::RefreshCell;
390 }
391 return RefreshCode::RefreshNone;
392 }
393
Char(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow * pParent,AudacityProject * project)394 unsigned WaveTrackAffordanceControls::Char(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent, AudacityProject* project)
395 {
396 if (mTextEditHelper && mTextEditHelper->OnChar(event.GetUnicodeKey(), project))
397 return RefreshCode::RefreshCell;
398 return RefreshCode::RefreshNone;
399 }
400
LoseFocus(AudacityProject *)401 unsigned WaveTrackAffordanceControls::LoseFocus(AudacityProject *)
402 {
403 return ExitTextEditing();
404 }
405
OnTextEditFinished(AudacityProject * project,const wxString & text)406 void WaveTrackAffordanceControls::OnTextEditFinished(AudacityProject* project, const wxString& text)
407 {
408 if (auto lock = mEditedClip.lock())
409 {
410 if (text != lock->GetName()) {
411 lock->SetName(text);
412
413 ProjectHistory::Get(*project).PushState(XO("Modified Clip Name"),
414 XO("Clip Name Edit"));
415 }
416 }
417 ResetClipNameEdit();
418 }
419
OnTextEditCancelled(AudacityProject * project)420 void WaveTrackAffordanceControls::OnTextEditCancelled(AudacityProject* project)
421 {
422 ResetClipNameEdit();
423 }
424
OnTextModified(AudacityProject * project,const wxString & text)425 void WaveTrackAffordanceControls::OnTextModified(AudacityProject* project, const wxString& text)
426 {
427 //Nothing to do
428 }
429
OnTextContextMenu(AudacityProject * project,const wxPoint & position)430 void WaveTrackAffordanceControls::OnTextContextMenu(AudacityProject* project, const wxPoint& position)
431 {
432 }
433
ResetClipNameEdit()434 void WaveTrackAffordanceControls::ResetClipNameEdit()
435 {
436 mTextEditHelper.reset();
437 mEditedClip.reset();
438 }
439
OnTrackChanged(TrackListEvent & evt)440 void WaveTrackAffordanceControls::OnTrackChanged(TrackListEvent& evt)
441 {
442 evt.Skip();
443 ExitTextEditing();
444 }
445
ExitTextEditing()446 unsigned WaveTrackAffordanceControls::ExitTextEditing()
447 {
448 using namespace RefreshCode;
449 if (mTextEditHelper)
450 {
451 if (auto trackList = FindTrack()->GetOwner())
452 {
453 mTextEditHelper->Finish(trackList->GetOwner());
454 }
455 ResetClipNameEdit();
456 return RefreshCell;
457 }
458 return RefreshNone;
459 }
460
461
StartEditNameOfMatchingClip(AudacityProject & project,std::function<bool (WaveClip &)> test)462 bool WaveTrackAffordanceControls::StartEditNameOfMatchingClip(
463 AudacityProject &project, std::function<bool(WaveClip&)> test )
464 {
465 //Attempts to invoke name editing if there is a selected clip
466 auto waveTrack = std::dynamic_pointer_cast<WaveTrack>(FindTrack());
467 if (!waveTrack)
468 return false;
469 auto clips = waveTrack->GetClips();
470
471 auto it = std::find_if(clips.begin(), clips.end(),
472 [&](auto pClip){ return pClip && test && test(*pClip); });
473 if (it != clips.end())
474 {
475 mFocusClip = *it;
476 return StartEditClipName(&project);
477 }
478 return false;
479 }
480
OnTextCopy(AudacityProject & project)481 bool WaveTrackAffordanceControls::OnTextCopy(AudacityProject& project)
482 {
483 if (mTextEditHelper)
484 {
485 mTextEditHelper->CopySelectedText(project);
486 return true;
487 }
488 return false;
489 }
490
OnTextCut(AudacityProject & project)491 bool WaveTrackAffordanceControls::OnTextCut(AudacityProject& project)
492 {
493 if (mTextEditHelper)
494 {
495 mTextEditHelper->CutSelectedText(project);
496 return true;
497 }
498 return false;
499 }
500
OnTextPaste(AudacityProject & project)501 bool WaveTrackAffordanceControls::OnTextPaste(AudacityProject& project)
502 {
503 if (mTextEditHelper)
504 {
505 mTextEditHelper->PasteSelectedText(project);
506 return true;
507 }
508 return false;
509 }
510
OnTextSelect(AudacityProject & project)511 bool WaveTrackAffordanceControls::OnTextSelect(AudacityProject& project)
512 {
513 if (mTextEditHelper)
514 {
515 mTextEditHelper->SelectAll();
516 return true;
517 }
518 return false;
519 }
520
OnAffordanceClick(const TrackPanelMouseEvent & event,AudacityProject * project)521 unsigned WaveTrackAffordanceControls::OnAffordanceClick(const TrackPanelMouseEvent& event, AudacityProject* project)
522 {
523 auto& viewInfo = ViewInfo::Get(*project);
524 if (mTextEditHelper)
525 {
526 if (auto lock = mEditedClip.lock())
527 {
528 auto affordanceRect = ClipParameters::GetClipRect(*lock.get(), viewInfo, event.rect);
529 if (!affordanceRect.Contains(event.event.GetPosition()))
530 return ExitTextEditing();
531 }
532 }
533 else if (auto lock = mFocusClip.lock())
534 {
535 if (event.event.LeftDClick())
536 {
537 auto affordanceRect = ClipParameters::GetClipRect(*lock.get(), viewInfo, event.rect);
538 if (affordanceRect.Contains(event.event.GetPosition()) &&
539 StartEditClipName(project))
540 {
541 event.event.Skip(false);
542 return RefreshCode::RefreshCell;
543 }
544 }
545 }
546 return RefreshCode::RefreshNone;
547 }
548
MakeTextEditHelper(const wxString & text)549 std::shared_ptr<TextEditHelper> WaveTrackAffordanceControls::MakeTextEditHelper(const wxString& text)
550 {
551 auto helper = std::make_shared<TextEditHelper>(shared_from_this(), text, mClipNameFont);
552 helper->SetTextColor(theTheme.Colour(clrClipNameText));
553 helper->SetTextSelectionColor(theTheme.Colour(clrClipNameTextSelection));
554 return helper;
555 }
556
557 // Register a menu item
558
559 namespace {
560
561 // Menu handler functions
562
563 struct Handler : CommandHandlerObject {
564
OnEditClipName__anon8225ad480411::Handler565 void OnEditClipName(const CommandContext &context)
566 {
567 auto &project = context.project;
568 const auto [pTrack, pClip] = SelectedClipOfFocusedTrack(project);
569 if (pTrack && pClip) {
570 if (auto pAffordance = FindAffordance(*pTrack)) {
571 pAffordance->StartEditNameOfMatchingClip(project,
572 [pClip = pClip](auto &clip){ return &clip == pClip; });
573 // Refresh so the cursor appears
574 TrackPanel::Get(project).RefreshTrack(pTrack);
575 }
576 }
577 }
578
579 };
580
581 #define FN(X) (& Handler :: X)
582
findCommandHandler(AudacityProject &)583 CommandHandlerObject &findCommandHandler(AudacityProject &) {
584 // Handler is not stateful. Doesn't need a factory registered with
585 // AudacityProject.
586 static Handler instance;
587 return instance;
588 };
589
590 using namespace MenuTable;
591
592 // Register menu items
593
594 AttachedItem sAttachment{ wxT("Edit/Other"),
595 ( FinderScope{ findCommandHandler },
596 Command( L"RenameClip", XXO("Rename Clip..."),
597 &Handler::OnEditClipName, SomeClipIsSelectedFlag(),
598 wxT("Ctrl+F2") ) )
599 };
600
601 }
602
603 #undef FN
604