Audacity 3.2.0
RealtimeEffectPanel.cpp
Go to the documentation of this file.
1/*!********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 @file RealtimeEffectPanel.cpp
6
7 @author Vitaly Sverchinsky
8
9**********************************************************************/
10
11#include "RealtimeEffectPanel.h"
12
13#include <wx/app.h>
14#include <wx/sizer.h>
15#include <wx/splitter.h>
16#include <wx/statbmp.h>
17#include <wx/stattext.h>
18#include <wx/menu.h>
19#include <wx/wupdlock.h>
20#include <wx/hyperlink.h>
21
22#include <wx/dcbuffer.h>
23
24#include "widgets/HelpSystem.h"
25#include "Theme.h"
26#include "AllThemeResources.h"
27#include "AudioIO.h"
28#include "Observer.h"
29#include "PluginManager.h"
30#include "Project.h"
31#include "ProjectHistory.h"
32#include "ProjectWindow.h"
33#include "ProjectWindows.h"
34#include "Track.h"
35#include "TrackPanelAx.h"
36#include "AColor.h"
37#include "WaveTrack.h"
38#include "effects/EffectUI.h"
43#include "UndoManager.h"
44#include "Prefs.h"
45#include "BasicUI.h"
46
47#if wxUSE_ACCESSIBILITY
49#endif
50
51namespace
52{
53 template <typename Visitor>
54 void VisitRealtimeEffectStateUIs(Track& track, Visitor&& visitor)
55 {
56 auto& effects = RealtimeEffectList::Get(track);
57 effects.Visit(
58 [visitor](auto& effectState, bool)
59 {
60 auto& ui = RealtimeEffectStateUI::Get(effectState);
61 visitor(ui);
62 });
63 }
64
66 {
68 track, [&](auto& ui) { ui.UpdateTrackData(track); });
69 }
70
72 {
74 track,
75 [&](auto& ui)
76 {
77 if (ui.IsShown())
78 {
79 ui.Hide(&project);
80 ui.Show(project);
81 }
82 });
83 }
84 //fwd
85 class RealtimeEffectControl;
86 PluginID ShowSelectEffectMenu(wxWindow* parent, RealtimeEffectControl* currentEffectControl = nullptr);
87
88 class DropHintLine : public wxWindow
89 {
90 public:
91 DropHintLine(wxWindow *parent,
92 wxWindowID id,
93 const wxPoint& pos = wxDefaultPosition,
94 const wxSize& size = wxDefaultSize)
95 : wxWindow(parent, id, pos, size, wxNO_BORDER, wxEmptyString)
96 {
97 wxWindow::SetBackgroundStyle(wxBG_STYLE_PAINT);
98 Bind(wxEVT_PAINT, &DropHintLine::OnPaint, this);
99 }
100
101 bool AcceptsFocus() const override { return false; }
102
103 private:
104 void OnPaint(wxPaintEvent&)
105 {
106 wxBufferedPaintDC dc(this);
107 const auto rect = wxRect(GetSize());
108
109 dc.SetPen(*wxTRANSPARENT_PEN);
110 dc.SetBrush(GetBackgroundColour());
111 dc.DrawRectangle(rect);
112 }
113 };
114
115 //Event generated by MovableControl when item is picked,
116 //dragged or dropped, extends wxCommandEvent interface
117 //with "source" and "target" indices which denote the
118 //initial and final element positions inside wxSizer (if present)
119 class MovableControlEvent final : public wxCommandEvent
120 {
121 int mSourceIndex{-1};
122 int mTargetIndex{-1};
123 public:
124 MovableControlEvent(wxEventType eventType, int winid = 0)
125 : wxCommandEvent(eventType, winid) { }
126
127 void SetSourceIndex(int index) noexcept { mSourceIndex = index; }
128 int GetSourceIndex() const noexcept { return mSourceIndex; }
129
130 void SetTargetIndex(int index) noexcept { mTargetIndex = index; }
131 int GetTargetIndex() const noexcept { return mTargetIndex; }
132
133 wxEvent* Clone() const override
134 {
135 return new MovableControlEvent(*this);
136 }
137 };
138
145 template<class WindowBase>
146 class ListNavigationEnabled : public wxNavigationEnabled<WindowBase>
147 {
148 public:
150 {
151 WindowBase::Bind(wxEVT_NAVIGATION_KEY, &ListNavigationEnabled::OnNavigationKeyEvent, this);
152 WindowBase::Bind(wxEVT_KEY_DOWN, &ListNavigationEnabled::OnKeyDown, this);
153 WindowBase::Bind(wxEVT_CHAR_HOOK, &ListNavigationEnabled::OnCharHook, this);
154 }
155
156 private:
157 void SetFocus() override
158 {
159 //Prevent attempt to search for a focusable child
160 WindowBase::SetFocus();
161 }
162
163 void OnCharHook(wxKeyEvent& evt)
164 {
165 //We want to restore focus to list item once arrow navigation is used
166 //on the child item, for this we need a char hook since key/navigation
167 //events are sent directly to the focused item
168 const auto keyCode = evt.GetKeyCode();
169 if((keyCode == WXK_DOWN || keyCode == WXK_UP) &&
170 !WindowBase::HasFocus() &&
171 WindowBase::IsDescendant(WindowBase::FindFocus()))
172 {
173 wxWindow::SetFocusFromKbd();
174 }
175 else
176 evt.Skip();
177 }
178
179 void OnKeyDown(wxKeyEvent& evt)
180 {
181 const auto keyCode = evt.GetKeyCode();
182 if(keyCode == WXK_TAB)
183 WindowBase::NavigateIn(wxNavigationKeyEvent::FromTab | (evt.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward));
184 else if(keyCode == WXK_DOWN)
185 WindowBase::Navigate(wxNavigationKeyEvent::IsForward);
186 else if(keyCode == WXK_UP)
187 WindowBase::Navigate(wxNavigationKeyEvent::IsBackward);
188 else
189 evt.Skip();
190 }
191
192 void OnNavigationKeyEvent(wxNavigationKeyEvent& evt)
193 {
194 if(evt.GetEventObject() == WindowBase::GetParent() && !evt.IsFromTab())
195 WindowBase::SetFocusFromKbd();
196 else if(evt.GetEventObject() == this && evt.GetCurrentFocus() == this && evt.IsFromTab())
197 {
198 //NavigateIn
199 wxPropagationDisabler disableProp(evt);
200 const auto isForward = evt.GetDirection();
201 const auto& children = WindowBase::GetChildren();
202 auto node = isForward ? children.GetFirst() : children.GetLast();
203 while(node)
204 {
205 auto child = node->GetData();
206 if(child->CanAcceptFocusFromKeyboard())
207 {
208 if(!child->GetEventHandler()->ProcessEvent(evt))
209 {
210 child->SetFocusFromKbd();
211 }
212 evt.Skip(false);
213 return;
214 }
215 node = isForward ? node->GetNext() : node->GetPrevious();
216 }
217 }
218 else
219 evt.Skip();
220 }
221
222 bool Destroy() override
223 {
224 if(WindowBase::IsDescendant(wxWindow::FindFocus()))
225 {
226 auto next = WindowBase::GetNextSibling();
227 if(next != nullptr && next->AcceptsFocus())
228 next->SetFocus();
229 else
230 {
231 auto prev = WindowBase::GetPrevSibling();
232 if(prev != nullptr && prev->AcceptsFocus())
233 prev->SetFocus();
234 }
235 }
236 return wxNavigationEnabled<WindowBase>::Destroy();
237 }
238
239 };
240
241 //Alias for ListNavigationEnabled<wxWindow> which provides wxWidgets-style ctor
243 {
244 public:
246
247 ListNavigationPanel(wxWindow* parent,
248 wxWindowID id,
249 const wxPoint& pos = wxDefaultPosition,
250 const wxSize& size = wxDefaultSize,
251 const wxString& name = wxPanelNameStr)
252 {
253 Create(parent, id, pos, size, name);
254 }
255
256 void Create(wxWindow* parent,
257 wxWindowID id,
258 const wxPoint& pos = wxDefaultPosition,
259 const wxSize& size = wxDefaultSize,
260 const wxString& name = wxPanelNameStr)
261 {
262 SetBackgroundStyle(wxBG_STYLE_PAINT);
263 ListNavigationEnabled<wxWindow>::Create(parent, id, pos, size, wxNO_BORDER | wxWANTS_CHARS, name);
264 Bind(wxEVT_PAINT, &ListNavigationPanel::OnPaint, this);
265 Bind(wxEVT_SET_FOCUS, &ListNavigationPanel::OnChangeFocus, this);
266 Bind(wxEVT_KILL_FOCUS, &ListNavigationPanel::OnChangeFocus, this);
267 }
268
269 void OnChangeFocus(wxFocusEvent& evt)
270 {
271 Refresh(false);
272 }
273
274 void OnPaint(wxPaintEvent& evt)
275 {
276 wxBufferedPaintDC dc(this);
277
278 dc.SetPen(*wxTRANSPARENT_PEN);
279 dc.SetBrush(GetBackgroundColour());
280 dc.Clear();
281
282 if(HasFocus())
283 AColor::DrawFocus(dc, GetClientRect().Deflate(3, 3));
284 }
285 };
286
287 class HyperLinkCtrlWrapper : public ListNavigationEnabled<wxHyperlinkCtrl>
288 {
289 public:
290 HyperLinkCtrlWrapper(wxWindow *parent,
291 wxWindowID id,
292 const wxString& label,
293 const wxString& url,
294 const wxPoint& pos = wxDefaultPosition,
295 const wxSize& size = wxDefaultSize,
296 long style = wxHL_DEFAULT_STYLE,
297 const wxString& name = wxHyperlinkCtrlNameStr)
298 {
299 Create(parent, id, label, url, pos, size, style, name);
300 }
301
302 void Create(wxWindow *parent,
303 wxWindowID id,
304 const wxString& label,
305 const wxString& url,
306 const wxPoint& pos = wxDefaultPosition,
307 const wxSize& size = wxDefaultSize,
308 long style = wxHL_DEFAULT_STYLE,
309 const wxString& name = wxHyperlinkCtrlNameStr)
310 {
311 ListNavigationEnabled<wxHyperlinkCtrl>::Create(parent, id, label, url, pos, size, style, name);
312 Bind(wxEVT_PAINT, &HyperLinkCtrlWrapper::OnPaint, this);
313 }
314
315 void OnPaint(wxPaintEvent& evt)
316 {
317 wxPaintDC dc(this);
318 dc.SetFont(GetFont());
319 dc.SetTextForeground(GetForegroundColour());
320 dc.SetTextBackground(GetBackgroundColour());
321
322 auto labelRect = GetLabelRect();
323
324 dc.DrawText(GetLabel(), labelRect.GetTopLeft());
325 if (HasFocus())
326 AColor::DrawFocus(dc, labelRect);
327 }
328 };
329
330 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_STARTED, MovableControlEvent);
331 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_POSITION, MovableControlEvent);
332 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_FINISHED, MovableControlEvent);
333
334 //Base class for the controls that can be moved with drag-and-drop
335 //action. Currently implementation is far from being generic and
336 //can work only in pair with wxBoxSizer with wxVERTICAL layout.
337 class MovableControl : public wxWindow
338 {
339 bool mDragging { false };
341
342 int mTargetIndex { -1 };
343 int mSourceIndex { -1 };
344 public:
345
346 MovableControl() = default;
347
348 MovableControl(wxWindow* parent,
349 wxWindowID id,
350 const wxPoint& pos = wxDefaultPosition,
351 const wxSize& size = wxDefaultSize,
352 long style = 0,
353 const wxString& name = wxPanelNameStr)
354 {
355 Create(parent, id, pos, size, style, name);
356 }
357
358 void Create(wxWindow* parent,
359 wxWindowID id,
360 const wxPoint& pos = wxDefaultPosition,
361 const wxSize& size = wxDefaultSize,
362 long style = 0,
363 const wxString& name = wxPanelNameStr)
364 {
365 wxWindow::Create(parent, id, pos, size, style, name);
366 Bind(wxEVT_LEFT_DOWN, &MovableControl::OnMouseDown, this);
367 Bind(wxEVT_LEFT_UP, &MovableControl::OnMouseUp, this);
368 Bind(wxEVT_MOTION, &MovableControl::OnMove, this);
369 Bind(wxEVT_KEY_DOWN, &MovableControl::OnKeyDown, this);
370 Bind(wxEVT_MOUSE_CAPTURE_LOST, &MovableControl::OnMouseCaptureLost, this);
371 }
372
373 void ProcessDragEvent(wxWindow* target, wxEventType eventType)
374 {
375 MovableControlEvent event(eventType);
376 event.SetSourceIndex(mSourceIndex);
377 event.SetTargetIndex(mTargetIndex);
378 event.SetEventObject(this);
379 target->GetEventHandler()->ProcessEvent(event);
380 }
381
383 {
384 auto parent = GetParent();
385 if(!parent)
386 return -1;
387
388 if(auto sizer = parent->GetSizer())
389 {
390 for(size_t i = 0, count = sizer->GetItemCount(); i < count; ++i)
391 {
392 if(sizer->GetItem(i)->GetWindow() == this)
393 return static_cast<int>(i);
394 }
395 }
396 return -1;
397 }
398
399 private:
400
401 void OnKeyDown(wxKeyEvent& evt)
402 {
403 const auto keyCode = evt.GetKeyCode();
404 if(evt.AltDown() && (keyCode == WXK_DOWN || keyCode == WXK_UP))
405 {
406#ifdef __WXOSX__
407 {//don't allow auto-repeats
408 static long lastEventTimestamp = 0;
409 if(lastEventTimestamp == evt.GetTimestamp())
410 return;//don't skip
411 lastEventTimestamp = evt.GetTimestamp();
412 }
413#endif
414 const auto sourceIndex = FindIndexInParent();
415 if(sourceIndex == -1)
416 {
417 evt.Skip();
418 return;
419 }
420
421 const auto targetIndex = std::clamp(
422 keyCode == WXK_DOWN ? sourceIndex + 1 : sourceIndex - 1,
423 0,
424 static_cast<int>(GetParent()->GetSizer()->GetItemCount()) - 1
425 );
426 if(sourceIndex != targetIndex)
427 {
428 mSourceIndex = sourceIndex;
429 mTargetIndex = targetIndex;
430 ProcessDragEvent(GetParent(), EVT_MOVABLE_CONTROL_DRAG_FINISHED);
431 }
432 }
433 else
434 evt.Skip();
435 }
436
437 void OnMouseCaptureLost(wxMouseCaptureLostEvent& event)
438 {
439 if(mDragging)
440 DragFinished();
441 }
442
444 {
445 if(auto parent = GetParent())
446 {
447 wxWindowUpdateLocker freeze(this);
448 ProcessDragEvent(parent, EVT_MOVABLE_CONTROL_DRAG_FINISHED);
449 }
450 mDragging = false;
451 }
452
453 void OnMouseDown(wxMouseEvent& evt)
454 {
455 if(mDragging)
456 {
457 DragFinished();
458 return;
459 }
460
461 mSourceIndex = mTargetIndex = FindIndexInParent();
462 if(mSourceIndex != -1)
463 {
464 CaptureMouse();
465 ProcessDragEvent(GetParent(), EVT_MOVABLE_CONTROL_DRAG_STARTED);
466
467 mInitialPosition = evt.GetPosition();
468 mDragging=true;
469 }
470 }
471
472 void OnMouseUp(wxMouseEvent& evt)
473 {
474 if(!mDragging)
475 return;
476
477 ReleaseMouse();
478
479 DragFinished();
480 }
481
482 void OnMove(wxMouseEvent& evt)
483 {
484 if(!mDragging)
485 return;
486
487 auto parent = GetParent();
488 if(!parent)
489 return;
490
491 wxPoint newPosition = wxGetMousePosition() - mInitialPosition;
492 Move(GetParent()->ScreenToClient(newPosition));
493
494 if(auto boxSizer = dynamic_cast<wxBoxSizer*>(parent->GetSizer()))
495 {
496 if(boxSizer->GetOrientation() == wxVERTICAL)
497 {
498 auto targetIndex = mSourceIndex;
499
500 //assuming that items are ordered from top to bottom (i > j <=> y(i) > y(j))
501 //compare wxSizerItem position with the current MovableControl position!
502 if(GetPosition().y < boxSizer->GetItem(mSourceIndex)->GetPosition().y)
503 {
504 //moving up
505 for(int i = 0; i < mSourceIndex; ++i)
506 {
507 const auto item = boxSizer->GetItem(i);
508
509 if(GetRect().GetTop() <= item->GetPosition().y + item->GetSize().y / 2)
510 {
511 targetIndex = i;
512 break;
513 }
514 }
515 }
516 else
517 {
518 //moving down
519 for(int i = static_cast<int>(boxSizer->GetItemCount()) - 1; i > mSourceIndex; --i)
520 {
521 const auto item = boxSizer->GetItem(i);
522 if(GetRect().GetBottom() >= item->GetPosition().y + item->GetSize().y / 2)
523 {
524 targetIndex = i;
525 break;
526 }
527 }
528 }
529
530 if(targetIndex != mTargetIndex)
531 {
532 mTargetIndex = targetIndex;
533 ProcessDragEvent(parent, EVT_MOVABLE_CONTROL_DRAG_POSITION);
534 }
535 }
536 }
537 }
538 };
539
540#if wxUSE_ACCESSIBILITY
541 class RealtimeEffectControlAx : public wxAccessible
542 {
543 public:
544 RealtimeEffectControlAx(wxWindow* win = nullptr) : wxAccessible(win) { }
545
546 wxAccStatus GetName(int childId, wxString* name) override
547 {
548 if(childId != wxACC_SELF)
549 return wxACC_NOT_IMPLEMENTED;
550
551 if(auto movable = wxDynamicCast(GetWindow(), MovableControl))
552 //i18n-hint: argument - position of the effect in the effect stack
553 *name = wxString::Format(_("Effect %d"), movable->FindIndexInParent() + 1);
554 return wxACC_OK;
555 }
556
557 wxAccStatus GetChildCount(int* childCount) override
558 {
559 const auto window = GetWindow();
560 *childCount = window->GetChildren().size();
561 return wxACC_OK;
562 }
563
564 wxAccStatus GetChild(int childId, wxAccessible** child) override
565 {
566 if(childId == wxACC_SELF)
567 *child = this;
568 else
569 {
570 const auto window = GetWindow();
571 const auto& children = window->GetChildren();
572 const auto childIndex = childId - 1;
573 if(childIndex < children.size())
574 *child = children[childIndex]->GetAccessible();
575 else
576 *child = nullptr;
577 }
578 return wxACC_OK;
579 }
580
581 wxAccStatus GetRole(int childId, wxAccRole* role) override
582 {
583 if(childId != wxACC_SELF)
584 return wxACC_NOT_IMPLEMENTED;
585
586 *role = wxROLE_SYSTEM_PANE;
587 return wxACC_OK;
588 }
589
590 wxAccStatus GetState(int childId, long* state) override
591 {
592 if(childId != wxACC_SELF)
593 return wxACC_NOT_IMPLEMENTED;
594
595 const auto window = GetWindow();
596 if(!window->IsEnabled())
597 *state = wxACC_STATE_SYSTEM_UNAVAILABLE;
598 else
599 {
600 *state = wxACC_STATE_SYSTEM_FOCUSABLE;
601 if(window->HasFocus())
602 *state |= wxACC_STATE_SYSTEM_FOCUSED;
603 }
604 return wxACC_OK;
605 }
606 };
607#endif
608
609 //UI control that represents individual effect from the effect list
610 class RealtimeEffectControl : public ListNavigationEnabled<MovableControl>
611 {
612 wxWeakRef<AudacityProject> mProject;
613 std::shared_ptr<Track> mTrack;
614 std::shared_ptr<RealtimeEffectState> mEffectState;
615 std::shared_ptr<EffectSettingsAccess> mSettingsAccess;
616
617 ThemedAButtonWrapper<AButton>* mChangeButton{nullptr};
618 AButton* mEnableButton{nullptr};
620
622
623 public:
625
626 RealtimeEffectControl(wxWindow* parent,
627 wxWindowID winid,
628 const wxPoint& pos = wxDefaultPosition,
629 const wxSize& size = wxDefaultSize)
630 {
631 Create(parent, winid, pos, size);
632 }
633
634 void Create(wxWindow* parent,
635 wxWindowID winid,
636 const wxPoint& pos = wxDefaultPosition,
637 const wxSize& size = wxDefaultSize)
638 {
639 //Prevents flickering and paint order issues
640 MovableControl::SetBackgroundStyle(wxBG_STYLE_PAINT);
641 MovableControl::Create(parent, winid, pos, size, wxNO_BORDER | wxWANTS_CHARS);
642
643 Bind(wxEVT_PAINT, &RealtimeEffectControl::OnPaint, this);
644 Bind(wxEVT_SET_FOCUS, &RealtimeEffectControl::OnFocusChange, this);
645 Bind(wxEVT_KILL_FOCUS, &RealtimeEffectControl::OnFocusChange, this);
646
647 auto sizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
648
649 //On/off button
650 auto enableButton = safenew ThemedAButtonWrapper<AButton>(this);
651 enableButton->SetTranslatableLabel(XO("Power"));
652 enableButton->SetImageIndices(0, bmpEffectOff, bmpEffectOff, bmpEffectOn, bmpEffectOn, bmpEffectOff);
653 enableButton->SetButtonToggles(true);
654 enableButton->SetBackgroundColorIndex(clrEffectListItemBackground);
655 mEnableButton = enableButton;
656
657 enableButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
658
659 mEffectState->SetActive(mEnableButton->IsDown());
660 });
661
662 //Central button with effect name, show settings
663 const auto optionsButton = safenew ThemedAButtonWrapper<AButton>(this, wxID_ANY);
664 optionsButton->SetImageIndices(0,
665 bmpHButtonNormal,
666 bmpHButtonHover,
667 bmpHButtonDown,
668 bmpHButtonHover,
669 bmpHButtonDisabled);
670 optionsButton->SetBackgroundColorIndex(clrEffectListItemBackground);
671 optionsButton->SetForegroundColorIndex(clrTrackPanelText);
672 optionsButton->SetButtonType(AButton::TextButton);
673 optionsButton->Bind(wxEVT_BUTTON, &RealtimeEffectControl::OnOptionsClicked, this);
674
675 //Remove/replace effect
676 auto changeButton = safenew ThemedAButtonWrapper<AButton>(this);
677 changeButton->SetImageIndices(0, bmpMoreNormal, bmpMoreHover, bmpMoreDown, bmpMoreHover, bmpMoreDisabled);
678 changeButton->SetBackgroundColorIndex(clrEffectListItemBackground);
679 changeButton->SetTranslatableLabel(XO("Replace effect"));
680 changeButton->Bind(wxEVT_BUTTON, &RealtimeEffectControl::OnChangeButtonClicked, this);
681
682 auto dragArea = safenew wxStaticBitmap(this, wxID_ANY, theTheme.Bitmap(bmpDragArea));
683 dragArea->Disable();
684 sizer->Add(dragArea, 0, wxLEFT | wxCENTER, 5);
685 sizer->Add(enableButton, 0, wxLEFT | wxCENTER, 5);
686 sizer->Add(optionsButton, 1, wxLEFT | wxCENTER, 5);
687 sizer->Add(changeButton, 0, wxLEFT | wxRIGHT | wxCENTER, 5);
688 mChangeButton = changeButton;
689 mOptionsButton = optionsButton;
690
691 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
692 vSizer->Add(sizer.release(), 0, wxUP | wxDOWN | wxEXPAND, 10);
693
694 SetSizer(vSizer.release());
695
696#if wxUSE_ACCESSIBILITY
697 SetAccessible(safenew RealtimeEffectControlAx(this));
698#endif
699 }
700
701 static const PluginDescriptor *GetPlugin(const PluginID &ID) {
702 auto desc = PluginManager::Get().GetPlugin(ID);
703 return desc;
704 }
705
708 {
709 const auto &ID = mEffectState->GetID();
710 const auto desc = GetPlugin(ID);
711 return desc
712 ? desc->GetSymbol().Msgid()
713 : XO("%s (missing)")
714 .Format(PluginManager::GetEffectNameFromID(ID).GET());
715 }
716
718 const std::shared_ptr<Track>& track,
719 const std::shared_ptr<RealtimeEffectState> &pState)
720 {
721 mProject = &project;
722 mTrack = track;
723 mEffectState = pState;
724
725 mSubscription = mEffectState->Subscribe([this](RealtimeEffectStateChange state) {
727 ? mEnableButton->PushDown()
728 : mEnableButton->PopUp();
729
730 if (mProject)
731 ProjectHistory::Get(*mProject).ModifyState(false);
732 });
733
735 if (pState) {
736 label = GetEffectName();
737 mSettingsAccess = pState->GetAccess();
738 }
739 else
740 mSettingsAccess.reset();
741 if (mEnableButton)
742 mSettingsAccess && mSettingsAccess->Get().extra.GetActive()
743 ? mEnableButton->PushDown()
744 : mEnableButton->PopUp();
745 if (mOptionsButton)
746 {
747 mOptionsButton->SetTranslatableLabel(label);
748 mOptionsButton->SetEnabled(pState && GetPlugin(pState->GetID()));
749 }
750 }
751
753 {
754 if(mProject == nullptr || mEffectState == nullptr)
755 return;
756
757 auto& ui = RealtimeEffectStateUI::Get(*mEffectState);
758 // Don't need autosave for the effect that is being removed
759 ui.Hide();
760
761 auto effectName = GetEffectName();
762 //After AudioIO::RemoveState call this will be destroyed
763 auto project = mProject.get();
764 auto trackName = mTrack->GetName();
765
766 AudioIO::Get()->RemoveState(*project, &*mTrack, mEffectState);
772 XO("Removed %s from %s").Format(effectName, trackName),
775 XO("Remove %s").Format(effectName)
776 );
777 }
778
779 void OnOptionsClicked(wxCommandEvent& event)
780 {
781 if(mProject == nullptr || mEffectState == nullptr)
782 return;//not initialized
783
784 const auto ID = mEffectState->GetID();
785 const auto effectPlugin = EffectManager::Get().GetEffect(ID);
786
787 if(effectPlugin == nullptr)
788 {
790 return;
791 }
792
793 auto& effectStateUI = RealtimeEffectStateUI::Get(*mEffectState);
794
795 effectStateUI.UpdateTrackData(*mTrack);
796 effectStateUI.Toggle( *mProject );
797 }
798
799 void OnChangeButtonClicked(wxCommandEvent& event)
800 {
801 if(!mTrack || mProject == nullptr)
802 return;
803 if(mEffectState == nullptr)
804 return;//not initialized
805
806 const auto effectID = ShowSelectEffectMenu(mChangeButton, this);
807 if(effectID.empty())
808 return;
809
810 auto &em = RealtimeEffectManager::Get(*mProject);
811 auto oIndex = em.FindState(&*mTrack, mEffectState);
812 if (!oIndex)
813 return;
814
815 auto oldName = GetEffectName();
816 auto &project = *mProject;
817 auto trackName = mTrack->GetName();
818 if (auto state = AudioIO::Get()
819 ->ReplaceState(project, &*mTrack, *oIndex, effectID)
820 ){
821 // Message subscription took care of updating the button text
822 // and destroyed `this`!
823 auto effect = state->GetEffect();
824 assert(effect); // postcondition of ReplaceState
826 /*i18n-hint: undo history,
827 first and second parameters - realtime effect names
828 */
829 XO("Replaced %s with %s")
830 .Format(oldName, effect->GetName()),
833 XO("Replace %s").Format(oldName));
834 }
835 }
836
837 void OnPaint(wxPaintEvent&)
838 {
839 wxBufferedPaintDC dc(this);
840 const auto rect = wxRect(GetSize());
841
842 dc.SetPen(*wxTRANSPARENT_PEN);
843 dc.SetBrush(GetBackgroundColour());
844 dc.DrawRectangle(rect);
845
846 dc.SetPen(theTheme.Colour(clrEffectListItemBorder));
847 dc.SetBrush(theTheme.Colour(clrEffectListItemBorder));
848 dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
849
850 if(HasFocus())
851 AColor::DrawFocus(dc, GetClientRect().Deflate(3, 3));
852 }
853
854 void OnFocusChange(wxFocusEvent& evt)
855 {
856 Refresh(false);
857 evt.Skip();
858 }
859 };
860
861 static wxString GetSafeVendor(const PluginDescriptor& descriptor)
862 {
863 if (descriptor.GetVendor().empty())
864 return XO("Unknown").Translation();
865
866 return descriptor.GetVendor();
867 }
868
869 PluginID ShowSelectEffectMenu(wxWindow* parent, RealtimeEffectControl* currentEffectControl)
870 {
871 wxMenu menu;
872
873 if(currentEffectControl != nullptr)
874 {
875 //no need to handle language change since menu creates it's own event loop
876 menu.Append(wxID_REMOVE, _("No Effect"));
877 menu.AppendSeparator();
878 }
879
880 auto& pluginManager = PluginManager::Get();
881
882 std::vector<const PluginDescriptor*> effects;
883 int selectedEffectIndex = -1;
884
885 auto compareEffects = [](const PluginDescriptor* a, const PluginDescriptor* b)
886 {
887 const auto vendorA = GetSafeVendor(*a);
888 const auto vendorB = GetSafeVendor(*b);
889
890 return vendorA < vendorB ||
891 (vendorA == vendorB &&
892 a->GetSymbol().Translation() < b->GetSymbol().Translation());
893 };
894
895 for(auto& effect : pluginManager.EffectsOfType(EffectTypeProcess))
896 {
897 if(!effect.IsEffectRealtime() || !effect.IsEnabled())
898 continue;
899 effects.push_back(&effect);
900 }
901
902 std::sort(effects.begin(), effects.end(), compareEffects);
903
904 wxString currentSubMenuName;
905 std::unique_ptr<wxMenu> currentSubMenu;
906
907 auto submenuEventHandler = [&](wxCommandEvent& event)
908 {
909 selectedEffectIndex = event.GetId() - wxID_HIGHEST;
910 };
911
912 for(int i = 0, count = effects.size(); i < count; ++i)
913 {
914 auto& effect = *effects[i];
915
916 const wxString vendor = GetSafeVendor(effect);
917
918 if(currentSubMenuName != vendor)
919 {
920 if(currentSubMenu)
921 {
922 currentSubMenu->Bind(wxEVT_MENU, submenuEventHandler);
923 menu.AppendSubMenu(currentSubMenu.release(), currentSubMenuName);
924 }
925 currentSubMenuName = vendor;
926 currentSubMenu = std::make_unique<wxMenu>();
927 }
928
929 const auto ID = wxID_HIGHEST + i;
930 currentSubMenu->Append(ID, effect.GetSymbol().Translation());
931 }
932 if(currentSubMenu)
933 {
934 currentSubMenu->Bind(wxEVT_MENU, submenuEventHandler);
935 menu.AppendSubMenu(currentSubMenu.release(), currentSubMenuName);
936 menu.AppendSeparator();
937 }
938 menu.Append(wxID_MORE, _("Get more effects..."));
939
940 menu.Bind(wxEVT_MENU, [&](wxCommandEvent& event)
941 {
942 if(event.GetId() == wxID_REMOVE)
943 currentEffectControl->RemoveFromList();
944 else if(event.GetId() == wxID_MORE)
945 OpenInDefaultBrowser("https://plugins.audacityteam.org/");
946 });
947
948 if(parent->PopupMenu(&menu, parent->GetClientRect().GetLeftBottom()) && selectedEffectIndex != -1)
949 return effects[selectedEffectIndex]->GetID();
950
951 return {};
952 }
953}
954
955class RealtimeEffectListWindow : public wxScrolledWindow
956{
957 wxWeakRef<AudacityProject> mProject;
958 std::shared_ptr<Track> mTrack;
960 wxStaticText* mAddEffectHint{nullptr};
961 wxWindow* mAddEffectTutorialLink{nullptr};
962 wxWindow* mEffectListContainer{nullptr};
963
965
966public:
967 RealtimeEffectListWindow(wxWindow *parent,
968 wxWindowID winid = wxID_ANY,
969 const wxPoint& pos = wxDefaultPosition,
970 const wxSize& size = wxDefaultSize,
971 long style = wxScrolledWindowStyle,
972 const wxString& name = wxPanelNameStr)
973 : wxScrolledWindow(parent, winid, pos, size, style, name)
974 {
975 Bind(wxEVT_SIZE, &RealtimeEffectListWindow::OnSizeChanged, this);
976#ifdef __WXMSW__
977 //Fixes flickering on redraw
978 wxScrolledWindow::SetDoubleBuffered(true);
979#endif
980 auto rootSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
981
982 auto effectListContainer = safenew ThemedWindowWrapper<wxPanel>(this, wxID_ANY);
983 effectListContainer->SetBackgroundColorIndex(clrMedium);
984 effectListContainer->SetSizer(safenew wxBoxSizer(wxVERTICAL));
985 effectListContainer->SetDoubleBuffered(true);
986 effectListContainer->Hide();
987 mEffectListContainer = effectListContainer;
988
989 auto addEffect = safenew ThemedAButtonWrapper<AButton>(this, wxID_ANY);
990 addEffect->SetImageIndices(0,
991 bmpHButtonNormal,
992 bmpHButtonHover,
993 bmpHButtonDown,
994 bmpHButtonHover,
995 bmpHButtonDisabled);
996 addEffect->SetTranslatableLabel(XO("Add effect"));
997 addEffect->SetButtonType(AButton::TextButton);
998 addEffect->SetBackgroundColorIndex(clrMedium);
999 addEffect->SetForegroundColorIndex(clrTrackPanelText);
1000 addEffect->Bind(wxEVT_BUTTON, &RealtimeEffectListWindow::OnAddEffectClicked, this);
1001 mAddEffect = addEffect;
1002
1003 auto addEffectHint = safenew ThemedWindowWrapper<wxStaticText>(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE);
1004 //Workaround: text is set in the OnSizeChange
1005 addEffectHint->SetForegroundColorIndex(clrTrackPanelText);
1006 mAddEffectHint = addEffectHint;
1007
1008 auto addEffectTutorialLink = safenew ThemedWindowWrapper<wxHyperlinkCtrl>(
1009 this, wxID_ANY, _("Watch video"),
1010 "https://www.audacityteam.org/realtime-video", wxDefaultPosition,
1011 wxDefaultSize, wxHL_ALIGN_LEFT | wxHL_CONTEXTMENU);
1012
1013 //i18n-hint: Hyperlink to the effects stack panel tutorial video
1014 addEffectTutorialLink->SetTranslatableLabel(XO("Watch video"));
1015#if wxUSE_ACCESSIBILITY
1016 safenew WindowAccessible(addEffectTutorialLink);
1017#endif
1018
1019 addEffectTutorialLink->Bind(
1020 wxEVT_HYPERLINK, [](wxHyperlinkEvent& event)
1021 { BasicUI::OpenInDefaultBrowser(event.GetURL()); });
1022
1023 mAddEffectTutorialLink = addEffectTutorialLink;
1024
1025 //indicates the insertion position of the item
1026 auto dropHintLine = safenew ThemedWindowWrapper<DropHintLine>(effectListContainer, wxID_ANY);
1027 dropHintLine->SetBackgroundColorIndex(clrDropHintHighlight);
1028 dropHintLine->Hide();
1029
1030 rootSizer->Add(mEffectListContainer, 0, wxEXPAND | wxBOTTOM, 10);
1031 rootSizer->Add(addEffect, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 20);
1032 rootSizer->Add(addEffectHint, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 20);
1033 rootSizer->Add(addEffectTutorialLink, 0, wxLEFT | wxRIGHT | wxEXPAND, 20);
1034
1035 SetSizer(rootSizer.release());
1036 SetMinSize({});
1037
1038 Bind(EVT_MOVABLE_CONTROL_DRAG_STARTED, [dropHintLine](const MovableControlEvent& event)
1039 {
1040 if(auto window = dynamic_cast<wxWindow*>(event.GetEventObject()))
1041 window->Raise();
1042 });
1043 Bind(EVT_MOVABLE_CONTROL_DRAG_POSITION, [this, dropHintLine](const MovableControlEvent& event)
1044 {
1045 constexpr auto DropHintLineHeight { 3 };//px
1046
1047 auto sizer = mEffectListContainer->GetSizer();
1048 assert(sizer != nullptr);
1049
1050 if(event.GetSourceIndex() == event.GetTargetIndex())
1051 {
1052 //do not display hint line if position didn't change
1053 dropHintLine->Hide();
1054 return;
1055 }
1056
1057 if(!dropHintLine->IsShown())
1058 {
1059 dropHintLine->Show();
1060 dropHintLine->Raise();
1061 if(auto window = dynamic_cast<wxWindow*>(event.GetEventObject()))
1062 window->Raise();
1063 }
1064
1065 auto item = sizer->GetItem(event.GetTargetIndex());
1066 dropHintLine->SetSize(item->GetSize().x, DropHintLineHeight);
1067
1068 if(event.GetTargetIndex() > event.GetSourceIndex())
1069 dropHintLine->SetPosition(item->GetRect().GetBottomLeft() - wxPoint(0, DropHintLineHeight));
1070 else
1071 dropHintLine->SetPosition(item->GetRect().GetTopLeft());
1072 });
1073 Bind(EVT_MOVABLE_CONTROL_DRAG_FINISHED, [this, dropHintLine](const MovableControlEvent& event)
1074 {
1075 dropHintLine->Hide();
1076
1077 if(mProject == nullptr)
1078 return;
1079
1080 auto& effectList = RealtimeEffectList::Get(*mTrack);
1081 const auto from = event.GetSourceIndex();
1082 const auto to = event.GetTargetIndex();
1083 if(from != to)
1084 {
1085 auto effectName =
1086 effectList.GetStateAt(from)->GetEffect()->GetName();
1087 bool up = (to < from);
1088 effectList.MoveEffect(from, to);
1089 ProjectHistory::Get(*mProject).PushState(
1090 (up
1095 ? XO("Moved %s up in %s")
1100 : XO("Moved %s down in %s"))
1101 .Format(effectName, mTrack->GetName()),
1102 XO("Change effect order"), UndoPush::CONSOLIDATE);
1103 }
1104 else
1105 {
1106 wxWindowUpdateLocker freeze(this);
1107 Layout();
1108 }
1109 });
1110 SetScrollRate(0, 20);
1111 }
1112
1113 void OnSizeChanged(wxSizeEvent& event)
1114 {
1115 if(auto sizerItem = GetSizer()->GetItem(mAddEffectHint))
1116 {
1117 //We need to wrap the text whenever panel width changes and adjust widget height
1118 //so that text is fully visible, but there is no height-for-width layout algorithm
1119 //in wxWidgets yet, so for now we just do it manually
1120
1121 //Restore original text, because 'Wrap' will replace it with wrapped one
1122 mAddEffectHint->SetLabel(_("Realtime effects are non-destructive and can be changed at any time."));
1123 mAddEffectHint->Wrap(GetClientSize().x - sizerItem->GetBorder() * 2);
1124 mAddEffectHint->InvalidateBestSize();
1125 }
1126 event.Skip();
1127 }
1128
1130 {
1131 auto sizer = mEffectListContainer->GetSizer();
1132 const auto insertItem = [this, &msg](){
1133 auto& effects = RealtimeEffectList::Get(*mTrack);
1134 InsertEffectRow(msg.srcIndex, effects.GetStateAt(msg.srcIndex));
1135 mAddEffectHint->Hide();
1136 mAddEffectTutorialLink->Hide();
1137 };
1138 const auto removeItem = [&](){
1140 // Don't need to auto-save changed settings of effect that is deleted
1141 // Undo history push will do it anyway
1142 ui.Hide();
1143
1144 auto window = sizer->GetItem(msg.srcIndex)->GetWindow();
1145 sizer->Remove(msg.srcIndex);
1146 wxTheApp->CallAfter([ref = wxWeakRef { window }] {
1147 if(ref) ref->Destroy();
1148 });
1149
1150 if(sizer->IsEmpty())
1151 {
1152 if(mEffectListContainer->IsDescendant(FindFocus()))
1153 mAddEffect->SetFocus();
1154
1155 mEffectListContainer->Hide();
1156 mAddEffectHint->Show();
1157 mAddEffectTutorialLink->Show();
1158 }
1159 };
1160
1161 wxWindowUpdateLocker freeze(this);
1163 {
1164 const auto sizer = mEffectListContainer->GetSizer();
1165
1166 const auto movedItem = sizer->GetItem(msg.srcIndex);
1167
1168 const auto proportion = movedItem->GetProportion();
1169 const auto flag = movedItem->GetFlag();
1170 const auto border = movedItem->GetBorder();
1171 const auto window = movedItem->GetWindow();
1172
1173 if(msg.srcIndex < msg.dstIndex)
1174 window->MoveAfterInTabOrder(sizer->GetItem(msg.dstIndex)->GetWindow());
1175 else
1176 window->MoveBeforeInTabOrder(sizer->GetItem(msg.dstIndex)->GetWindow());
1177
1178 sizer->Remove(msg.srcIndex);
1179 sizer->Insert(msg.dstIndex, window, proportion, flag, border);
1180 }
1182 {
1183 insertItem();
1184 }
1186 {
1187 removeItem();
1188 }
1190 {
1191 insertItem();
1192 }
1194 {
1195 removeItem();
1196 }
1197 SendSizeEventToParent();
1198 }
1199
1201 {
1203
1204 mTrack.reset();
1205 mProject = nullptr;
1207 }
1208
1209 void SetTrack(AudacityProject& project, const std::shared_ptr<Track>& track)
1210 {
1211 if (mTrack == track)
1212 return;
1213
1215
1216 mTrack = track;
1217 mProject = &project;
1219
1220 if (track)
1221 {
1222 auto& effects = RealtimeEffectList::Get(*mTrack);
1223 mEffectListItemMovedSubscription = effects.Subscribe(
1225
1227 }
1228 }
1229
1230 void EnableEffects(bool enable)
1231 {
1232 if (mTrack)
1233 RealtimeEffectList::Get(*mTrack).SetActive(enable);
1234 }
1235
1237 {
1238 wxWindowUpdateLocker freeze(this);
1239
1240 const auto hadFocus = mEffectListContainer->IsDescendant(FindFocus());
1241 //delete items that were added to the sizer
1242 mEffectListContainer->Hide();
1243 mEffectListContainer->GetSizer()->Clear(true);
1244
1245
1246 if(!mTrack || RealtimeEffectList::Get(*mTrack).GetStatesCount() == 0)
1247 mEffectListContainer->Hide();
1248
1249 auto isEmpty{true};
1250 if(mTrack)
1251 {
1252 auto& effects = RealtimeEffectList::Get(*mTrack);
1253 isEmpty = effects.GetStatesCount() == 0;
1254 for(size_t i = 0, count = effects.GetStatesCount(); i < count; ++i)
1255 InsertEffectRow(i, effects.GetStateAt(i));
1256 }
1258 //Workaround for GTK: Underlying GTK widget does not update
1259 //its size when wxWindow size is set to zero
1260 mEffectListContainer->Show(!isEmpty);
1261 mAddEffectHint->Show(isEmpty);
1262 mAddEffectTutorialLink->Show(isEmpty);
1263
1264 SendSizeEventToParent();
1265 }
1266
1267 void OnAddEffectClicked(const wxCommandEvent& event)
1268 {
1269 if(!mTrack || mProject == nullptr)
1270 return;
1271
1272 const auto effectID = ShowSelectEffectMenu(dynamic_cast<wxWindow*>(event.GetEventObject()));
1273 if(effectID.empty())
1274 return;
1275
1276 if(auto state = AudioIO::Get()->AddState(*mProject, &*mTrack, effectID))
1277 {
1278 auto effect = state->GetEffect();
1279 assert(effect); // postcondition of AddState
1280 const auto effectName = effect->GetName();
1281 ProjectHistory::Get(*mProject).PushState(
1286 XO("Added %s to %s").Format(effectName, mTrack->GetName()),
1287 //i18n-hint: undo history record
1288 XO("Add %s").Format(effectName));
1289 }
1290 }
1291
1292 void InsertEffectRow(size_t index,
1293 const std::shared_ptr<RealtimeEffectState> &pState)
1294 {
1295 if(mProject == nullptr)
1296 return;
1297
1298 // See comment in ReloadEffectsList
1299 if(!mEffectListContainer->IsShown())
1300 mEffectListContainer->Show();
1301
1303 row->SetBackgroundColorIndex(clrEffectListItemBackground);
1304 row->SetEffect(*mProject, mTrack, pState);
1305 mEffectListContainer->GetSizer()->Insert(index, row, 0, wxEXPAND);
1306 }
1307};
1308
1309
1311{
1313
1315 : mProject { project }
1316 {}
1317
1318 void UpdatePrefs() override
1319 {
1320 auto& trackList = TrackList::Get(mProject);
1321 for (auto waveTrack : trackList.Any<WaveTrack>())
1323 }
1324};
1325
1326namespace {
1327AttachedWindows::RegisteredFactory sKey{
1328[](AudacityProject &project) -> wxWeakRef<wxWindow> {
1329 const auto pProjectWindow = &ProjectWindow::Get(project);
1331 project, pProjectWindow->GetContainerWindow(), wxID_ANY);
1332 effectsPanel->SetName(_("Realtime effects"));
1333 effectsPanel->SetBackgroundColorIndex(clrMedium);
1334 effectsPanel->Hide();//initially hidden
1335 return effectsPanel;
1336}
1337};
1338}
1339
1341{
1343}
1344
1345const RealtimeEffectPanel &
1347{
1348 return Get(const_cast<AudacityProject &>(project));
1349}
1350
1352 AudacityProject& project, wxWindow* parent, wxWindowID id, const wxPoint& pos,
1353 const wxSize& size,
1354 long style, const wxString& name)
1355 : wxPanel(parent, id, pos, size, style, name)
1356 , mProject(project)
1357 , mPrefsListenerHelper(std::make_unique<PrefsListenerHelper>(project))
1358{
1359 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
1360
1361 auto header = safenew ThemedWindowWrapper<ListNavigationPanel>(this, wxID_ANY);
1362 header->SetMinClientSize({254, -1});
1363#if wxUSE_ACCESSIBILITY
1364 safenew WindowAccessible(header);
1365#endif
1366 header->SetBackgroundColorIndex(clrMedium);
1367 {
1368 auto hSizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
1369 auto toggleEffects = safenew ThemedAButtonWrapper<AButton>(header);
1370 toggleEffects->SetImageIndices(0, bmpEffectOff, bmpEffectOff, bmpEffectOn, bmpEffectOn, bmpEffectOff);
1371 toggleEffects->SetButtonToggles(true);
1372 toggleEffects->SetTranslatableLabel(XO("Power"));
1373 toggleEffects->SetBackgroundColorIndex(clrMedium);
1374 mToggleEffects = toggleEffects;
1375
1376 toggleEffects->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
1377 if (mEffectList)
1378 {
1380
1382 }
1383 });
1384
1385 hSizer->Add(toggleEffects, 0, wxSTRETCH_NOT | wxALIGN_CENTER | wxLEFT, 5);
1386 {
1387 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
1388
1389 auto headerText = safenew ThemedWindowWrapper<wxStaticText>(header, wxID_ANY, wxEmptyString);
1390 headerText->SetFont(wxFont(wxFontInfo().Bold()));
1391 headerText->SetTranslatableLabel(XO("Realtime Effects"));
1392 headerText->SetForegroundColorIndex(clrTrackPanelText);
1393
1394 auto trackTitle = safenew ThemedWindowWrapper<wxStaticText>(header, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
1395 trackTitle->SetForegroundColorIndex(clrTrackPanelText);
1396 mTrackTitle = trackTitle;
1397
1398 vSizer->Add(headerText);
1399 vSizer->Add(trackTitle);
1400
1401 hSizer->Add(vSizer.release(), 1, wxEXPAND | wxALL, 10);
1402 }
1403 auto close = safenew ThemedAButtonWrapper<AButton>(header);
1404 close->SetTranslatableLabel(XO("Close"));
1405 close->SetImageIndices(0, bmpCloseNormal, bmpCloseHover, bmpCloseDown, bmpCloseHover, bmpCloseDisabled);
1406 close->SetBackgroundColorIndex(clrMedium);
1407
1408 close->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
1409
1410 hSizer->Add(close, 0, wxSTRETCH_NOT | wxALIGN_CENTER | wxRIGHT, 5);
1411
1412 header->SetSizer(hSizer.release());
1413 }
1414 vSizer->Add(header, 0, wxEXPAND);
1415
1416 auto effectList = safenew ThemedWindowWrapper<RealtimeEffectListWindow>(this, wxID_ANY);
1417 effectList->SetBackgroundColorIndex(clrMedium);
1418 vSizer->Add(effectList, 1, wxEXPAND);
1419
1420 mHeader = header;
1421 mEffectList = effectList;
1422
1423 SetSizerAndFit(vSizer.release());
1424
1425 Bind(wxEVT_CHAR_HOOK, &RealtimeEffectPanel::OnCharHook, this);
1427 auto track = evt.mpTrack.lock();
1428 auto waveTrack = dynamic_cast<WaveTrack*>(track.get());
1429
1430 if (waveTrack == nullptr)
1431 return;
1432
1433 switch (evt.mType)
1434 {
1436 if (mCurrentTrack.lock() == track)
1437 mTrackTitle->SetLabel(track->GetName());
1438 UpdateRealtimeEffectUIData(*waveTrack);
1439 break;
1441 if (evt.mExtra == 0)
1442 mPotentiallyRemovedTracks.push_back(track);
1443 break;
1445 // Addition can be fired as a part of "replace" event.
1446 // Calling UpdateRealtimeEffectUIData is mostly no-op,
1447 // it will just create a new State and Access for it.
1448 UpdateRealtimeEffectUIData(*waveTrack);
1449 break;
1450 default:
1451 break;
1452 }
1453 });
1454
1456 [this](UndoRedoMessage message)
1457 {
1458 if (
1459 message.type == UndoRedoMessage::Type::Purge ||
1460 message.type == UndoRedoMessage::Type::BeginPurge ||
1461 message.type == UndoRedoMessage::Type::EndPurge)
1462 return;
1463
1464 auto& trackList = TrackList::Get(mProject);
1465
1466 // Realtime effect UI is only updated on Undo or Redo
1467 auto waveTracks = trackList.Any<WaveTrack>();
1468
1469 if (
1470 message.type == UndoRedoMessage::Type::UndoOrRedo ||
1471 message.type == UndoRedoMessage::Type::Reset)
1472 {
1473 for (auto waveTrack : waveTracks)
1474 UpdateRealtimeEffectUIData(*waveTrack);
1475 }
1476
1477 // But mPotentiallyRemovedTracks processing happens as fast as possible.
1478 // This event is fired right after the track is deleted, so we do not
1479 // hold the strong reference to the track much longer than need.
1480 if (mPotentiallyRemovedTracks.empty())
1481 return;
1482
1483 // Collect RealtimeEffectUIs that are currently shown
1484 // for the potentially removed tracks
1485 std::vector<RealtimeEffectStateUI*> shownUIs;
1486
1487 for (auto track : mPotentiallyRemovedTracks)
1488 {
1489 // By construction, track cannot be null
1490 assert(track != nullptr);
1491
1493 *track,
1494 [&shownUIs](auto& ui)
1495 {
1496 if (ui.IsShown())
1497 shownUIs.push_back(&ui);
1498 });
1499 }
1500
1501 // For every UI shown - check if the corresponding state
1502 // is reachable from the current track list.
1503 for (auto effectUI : shownUIs)
1504 {
1505 bool reachable = false;
1506
1507 for (auto track : waveTracks)
1508 {
1510 *track,
1511 [effectUI, &reachable](auto& ui)
1512 {
1513 if (effectUI == &ui)
1514 reachable = true;
1515 });
1516
1517 if (reachable)
1518 break;
1519 }
1520
1521 if (!reachable)
1522 // Don't need to autosave for an unreachable state
1523 effectUI->Hide();
1524 }
1525
1527 });
1528
1530 .Subscribe([this](const TrackFocusChangeMessage& msg) {
1531 if (IsShown())
1532 {
1533 auto& trackFocus = TrackFocus::Get(mProject);
1534 ShowPanel(trackFocus.Get(), false);
1535 }
1536 });
1537
1538 Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) {
1539 HidePanel(); });
1540}
1541
1543{
1544}
1545
1547{
1548 if(track == nullptr)
1549 {
1550 ResetTrack();
1551 return;
1552 }
1553
1554 wxWindowUpdateLocker freeze(this);
1555
1556 SetTrack(track->shared_from_this());
1557
1558 auto &projectWindow = ProjectWindow::Get(mProject);
1559 const auto pContainerWindow = projectWindow.GetContainerWindow();
1560 if (pContainerWindow->GetWindow1() != this)
1561 {
1562 //Restore previous effects window size
1563 pContainerWindow->SplitVertically(
1564 this,
1565 projectWindow.GetTrackListWindow(),
1566 this->GetSize().GetWidth());
1567 }
1568 if(focus)
1569 SetFocus();
1570 projectWindow.Layout();
1571}
1572
1574{
1575 wxWindowUpdateLocker freeze(this);
1576
1577 auto &projectWindow = ProjectWindow::Get(mProject);
1578 const auto pContainerWindow = projectWindow.GetContainerWindow();
1579 const auto pTrackListWindow = projectWindow.GetTrackListWindow();
1580 if (pContainerWindow->GetWindow2() == nullptr)
1581 //only effects panel is present, restore split positions before removing effects panel
1582 //Workaround: ::Replace and ::Initialize do not work here...
1583 pContainerWindow->SplitVertically(this, pTrackListWindow);
1584
1585 pContainerWindow->Unsplit(this);
1586 pTrackListWindow->SetFocus();
1587 projectWindow.Layout();
1588}
1589
1590void RealtimeEffectPanel::SetTrack(const std::shared_ptr<Track>& track)
1591{
1592 //Avoid creation-on-demand of a useless, empty list in case the track is of non-wave type.
1593 if(track && dynamic_cast<WaveTrack*>(&*track) != nullptr)
1594 {
1595 mTrackTitle->SetLabel(track->GetName());
1597 track && RealtimeEffectList::Get(*track).IsActive()
1599 : mToggleEffects->PopUp();
1600 mEffectList->SetTrack(mProject, track);
1601
1602 mCurrentTrack = track;
1603 //i18n-hint: argument - track name
1604 mHeader->SetName(wxString::Format(_("Realtime effects for %s"), track->GetName()));
1605 }
1606 else
1607 ResetTrack();
1608}
1609
1611{
1612 mTrackTitle->SetLabel(wxEmptyString);
1615 mCurrentTrack.reset();
1616 mHeader->SetName(wxEmptyString);
1617}
1618
1620{
1621 mHeader->SetFocus();
1622}
1623
1625{
1626 if(evt.GetKeyCode() == WXK_ESCAPE && IsShown() && IsDescendant(FindFocus()))
1627 Close();
1628 else
1629 evt.Skip();
1630}
Toolkit-neutral facade for basic user interface services.
const TranslatableString name
Definition: Distortion.cpp:82
@ EffectTypeProcess
wxString PluginID
Definition: EffectManager.h:30
const TranslatableString desc
Definition: ExportPCM.cpp:58
void OpenInDefaultBrowser(const URLString &link)
Definition: HelpSystem.cpp:532
#define XO(s)
Definition: Internat.h:31
#define _(s)
Definition: Internat.h:75
#define safenew
Definition: MemoryX.h:10
AUDACITY_DLL_API AttachedWindows & GetAttachedWindows(AudacityProject &project)
accessors for certain important windows associated with each project
RealtimeEffectStateChange
for(int ii=0, nn=names.size();ii< nn;++ii)
TranslatableString label
Definition: TagsEditor.cpp:163
THEME_API Theme theTheme
Definition: Theme.cpp:82
declares abstract base class Track, TrackList, and iterators over TrackList
int id
static std::once_flag flag
A wxButton with mouse-over behaviour.
Definition: AButton.h:104
void PushDown()
Definition: AButton.cpp:580
bool IsDown()
Definition: AButton.h:208
void Disable()
Definition: AButton.cpp:563
void Enable()
Definition: AButton.cpp:554
void PopUp()
Definition: AButton.cpp:588
@ TextButton
Definition: AButton.h:112
void SetEnabled(bool state)
Definition: AButton.h:182
static void DrawFocus(wxDC &dc, wxRect &r)
Definition: AColor.cpp:235
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:89
static AudioIO * Get()
Definition: AudioIO.cpp:133
void RemoveState(AudacityProject &project, Track *pTrack, std::shared_ptr< RealtimeEffectState > pState)
Forwards to RealtimeEffectManager::RemoveState with proper init scope.
Definition: AudioIO.cpp:373
Subclass & Get(const RegisteredFactory &key)
Get reference to an attachment, creating on demand if not present, down-cast it to Subclass.
Definition: ClientData.h:309
const wxString Translation() const
EffectPlugin * GetEffect(const PluginID &ID)
static EffectManager & Get()
Abstract base class used in importing a file.
Subscription Subscribe(Callback callback)
Connect a callback to the Publisher; later-connected are called earlier.
Definition: Observer.h:199
A move-only handle representing a connection to a Publisher.
Definition: Observer.h:70
void Reset() noexcept
Breaks the connection (constant time)
Definition: Observer.cpp:101
const ComponentInterfaceSymbol & GetSymbol() const
const wxString & GetVendor() const
static Identifier GetEffectNameFromID(const PluginID &ID)
const PluginDescriptor * GetPlugin(const PluginID &ID) const
static PluginManager & Get()
A listener notified of changes in preferences.
Definition: Prefs.h:543
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
void ModifyState(bool bWantsAutoSave)
static ProjectHistory & Get(AudacityProject &project)
static ProjectWindow & Get(AudacityProject &project)
static RealtimeEffectList & Get(AudacityProject &project)
size_t GetStatesCount() const noexcept
bool IsActive() const
Non-blocking atomic boolean load.
void SetActive(bool value)
Done by main thread only, under a lock guard.
void OnEffectListItemChange(const RealtimeEffectListMessage &msg)
std::shared_ptr< Track > mTrack
RealtimeEffectListWindow(wxWindow *parent, wxWindowID winid=wxID_ANY, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=wxScrolledWindowStyle, const wxString &name=wxPanelNameStr)
void OnAddEffectClicked(const wxCommandEvent &event)
void OnSizeChanged(wxSizeEvent &event)
void SetTrack(AudacityProject &project, const std::shared_ptr< Track > &track)
Observer::Subscription mEffectListItemMovedSubscription
void InsertEffectRow(size_t index, const std::shared_ptr< RealtimeEffectState > &pState)
wxWeakRef< AudacityProject > mProject
static RealtimeEffectManager & Get(AudacityProject &project)
UI Panel that displays realtime effects from the effect stack of an individual track,...
std::weak_ptr< Track > mCurrentTrack
Observer::Subscription mUndoSubscription
void ShowPanel(Track *track, bool focus)
std::vector< std::shared_ptr< Track > > mPotentiallyRemovedTracks
wxStaticText * mTrackTitle
void SetTrack(const std::shared_ptr< Track > &track)
Shows effects from the effect stack of the track.
RealtimeEffectListWindow * mEffectList
void OnCharHook(wxKeyEvent &evt)
static RealtimeEffectPanel & Get(AudacityProject &project)
RealtimeEffectPanel(AudacityProject &project, wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0, const wxString &name=wxPanelNameStr)
Observer::Subscription mFocusChangeSubscription
AudacityProject & mProject
Observer::Subscription mTrackListChanged
static RealtimeEffectStateUI & Get(RealtimeEffectState &state)
wxColour & Colour(int iIndex)
wxBitmap & Bitmap(int iIndex)
Track * Get()
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:225
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:1437
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:486
Holds a msgid for the translation catalog; may also bind format arguments.
static UndoManager & Get(AudacityProject &project)
Definition: UndoManager.cpp:67
A Track that contains audio waveform data.
Definition: WaveTrack.h:57
An alternative to using wxWindowAccessible, which in wxWidgets 3.1.1 contained GetParent() which was ...
DropHintLine(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize)
Changes default arrow navigation to behave more list- or table-like. Instead of searching focusable i...
ListNavigationPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, const wxString &name=wxPanelNameStr)
void Create(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, const wxString &name=wxPanelNameStr)
MovableControl(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0, const wxString &name=wxPanelNameStr)
void Create(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0, const wxString &name=wxPanelNameStr)
void ProcessDragEvent(wxWindow *target, wxEventType eventType)
RealtimeEffectControl(wxWindow *parent, wxWindowID winid, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize)
void SetEffect(AudacityProject &project, const std::shared_ptr< Track > &track, const std::shared_ptr< RealtimeEffectState > &pState)
void Create(wxWindow *parent, wxWindowID winid, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize)
static const PluginDescriptor * GetPlugin(const PluginID &ID)
bool OpenInDefaultBrowser(const wxString &url)
Open an URL in default browser.
Definition: BasicUI.cpp:238
static wxString GetSafeVendor(const PluginDescriptor &descriptor)
wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_FINISHED, MovableControlEvent)
PluginID ShowSelectEffectMenu(wxWindow *parent, RealtimeEffectControl *currentEffectControl=nullptr)
void ReopenRealtimeEffectUIData(AudacityProject &project, Track &track)
void VisitRealtimeEffectStateUIs(Track &track, Visitor &&visitor)
STL namespace.
@ Remove
Effect item was removed from the list at srcIndex position. affectedState is removed state.
@ DidReplace
Effect item was replaced with a new item at srcIndex position. affectedState is an old state.
@ Move
Item position has changed, from srcIndex to dstIndex. affectedState is the moved state.
@ Insert
New effect item was added to the list at srcIndex position. affectedState is a new state.
@ WillReplace
Effect item will be replaced with a new item at srcIndex position. affectedState is the state to be r...
std::shared_ptr< RealtimeEffectState > affectedState
Notification of changes in individual tracks of TrackList, or of TrackList's composition.
Definition: Track.h:1288
const int mExtra
Definition: Track.h:1327
const std::weak_ptr< Track > mpTrack
Definition: Track.h:1326
const Type mType
Definition: Track.h:1325
@ DELETION
Posted when a track has been deleted from a tracklist. Also posted when one track replaces another.
Definition: Track.h:1313
@ ADDITION
Posted when a track has been added to a tracklist. Also posted when one track replaces another.
Definition: Track.h:1307
@ TRACK_DATA_CHANGE
Posted when certain fields of a track change.
Definition: Track.h:1294
Type of message published by UndoManager.
Definition: UndoManager.h:61
enum UndoRedoMessage::Type type