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