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/sizer.h>
14#include <wx/button.h>
15#include <wx/statbmp.h>
16#include <wx/stattext.h>
17#include <wx/menu.h>
18#include <wx/wupdlock.h>
19#include <wx/bmpbuttn.h>
20#include <wx/hyperlink.h>
21
22#ifdef __WXMSW__
23#include <wx/dcbuffer.h>
24#else
25#include <wx/dcclient.h>
26#endif
27
28#include "widgets/HelpSystem.h"
29#include "Theme.h"
30#include "AllThemeResources.h"
31#include "AudioIO.h"
32#include "PluginManager.h"
33#include "Project.h"
34#include "ProjectHistory.h"
35#include "ProjectWindow.h"
36#include "Track.h"
37#include "WaveTrack.h"
38#include "effects/EffectUI.h"
42
43namespace
44{
46 const std::shared_ptr<RealtimeEffectState> &pEffectState
47 ){
48 return [=](wxWindow &parent,
52 // Make sure there is an associated project, whose lifetime will
53 // govern the lifetime of the dialog
54 if (auto project = FindProjectFromWindow(&parent)) {
55 std::shared_ptr<EffectInstance> pInstance;
57 &parent, *project, host, client, pInstance, access,
58 pEffectState } }
59 ; dlg->Initialize()
60 ){
61 auto pValidator = dlg->GetValidator();
62 // release() is safe because parent will own it
63 return { dlg.release(), pInstance, pValidator };
64 }
65 }
66 return {};
67 };
68 }
69
70 //fwd
71 class RealtimeEffectControl;
72 PluginID ShowSelectEffectMenu(wxWindow* parent, RealtimeEffectControl* currentEffectControl = nullptr);
73
74 class DropHintLine : public wxWindow
75 {
76 public:
77 DropHintLine(wxWindow *parent,
78 wxWindowID id,
79 const wxPoint& pos = wxDefaultPosition,
80 const wxSize& size = wxDefaultSize)
81 : wxWindow(parent, id, pos, size, wxNO_BORDER, wxEmptyString)
82 {
83 wxWindow::SetBackgroundStyle(wxBG_STYLE_PAINT);
84 Bind(wxEVT_PAINT, &DropHintLine::OnPaint, this);
85 }
86
87 private:
88 void OnPaint(wxPaintEvent&)
89 {
90#ifdef __WXMSW__
91 wxBufferedPaintDC dc(this);
92#else
93 wxPaintDC dc(this);
94#endif
95 const auto rect = wxRect(GetSize());
96
97 dc.SetPen(*wxTRANSPARENT_PEN);
98 dc.SetBrush(GetBackgroundColour());
99 dc.DrawRectangle(rect);
100 }
101 };
102
103 //Event generated by MovableControl when item is picked,
104 //dragged or dropped, extends wxCommandEvent interface
105 //with "source" and "target" indices which denote the
106 //initial and final element positions inside wxSizer (if present)
107 class MovableControlEvent final : public wxCommandEvent
108 {
109 int mSourceIndex{-1};
110 int mTargetIndex{-1};
111 public:
112 MovableControlEvent(wxEventType eventType, int winid = 0)
113 : wxCommandEvent(eventType, winid) { }
114
115 void SetSourceIndex(int index) noexcept { mSourceIndex = index; }
116 int GetSourceIndex() const noexcept { return mSourceIndex; }
117
118 void SetTargetIndex(int index) noexcept { mTargetIndex = index; }
119 int GetTargetIndex() const noexcept { return mTargetIndex; }
120
121 wxEvent* Clone() const override
122 {
123 return new MovableControlEvent(*this);
124 }
125 };
126
127 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_STARTED, MovableControlEvent);
128 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_POSITION, MovableControlEvent);
129 wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_FINISHED, MovableControlEvent);
130
131 //Base class for the controls that can be moved with drag-and-drop
132 //action. Currently implementation is far from being generic and
133 //can work only in pair with wxBoxSizer with wxVERTICAL layout.
134 class MovableControl : public wxWindow
135 {
136 bool mDragging { false };
138
139 int mTargetIndex { -1 };
140 int mSourceIndex { -1 };
141 public:
142
143 MovableControl(wxWindow* parent,
144 wxWindowID id,
145 const wxPoint& pos = wxDefaultPosition,
146 const wxSize& size = wxDefaultSize,
147 long style = wxTAB_TRAVERSAL | wxNO_BORDER,
148 const wxString& name = wxPanelNameStr)
149 : wxWindow(parent, id, pos, size, style, name)
150 {
151 Bind(wxEVT_LEFT_DOWN, &MovableControl::OnMouseDown, this);
152 Bind(wxEVT_LEFT_UP, &MovableControl::OnMouseUp, this);
153 Bind(wxEVT_MOTION, &MovableControl::OnMove, this);
154 Bind(wxEVT_MOUSE_CAPTURE_LOST, &MovableControl::OnMouseCaptureLost, this);
155 }
156
157 void PostDragEvent(wxWindow* target, wxEventType eventType)
158 {
159 MovableControlEvent event(eventType);
160 event.SetSourceIndex(mSourceIndex);
161 event.SetTargetIndex(mTargetIndex);
162 event.SetEventObject(this);
163 wxPostEvent(target, event);
164 }
165
166 private:
167
168 void OnMouseCaptureLost(wxMouseCaptureLostEvent& event)
169 {
170 if(mDragging)
171 DragFinished();
172 }
173
175 {
176 if(auto parent = GetParent())
177 {
178 wxWindowUpdateLocker freeze(this);
179 PostDragEvent(parent, EVT_MOVABLE_CONTROL_DRAG_FINISHED);
180 }
181 mDragging = false;
182 }
183
184 void OnMouseDown(wxMouseEvent& evt)
185 {
186 if(mDragging)
187 {
188 DragFinished();
189 return;
190 }
191
192 auto parent = GetParent();
193 if(!parent)
194 return;
195
196 mSourceIndex = mTargetIndex = -1;
197
198 CaptureMouse();
199
200 mInitialPosition = evt.GetPosition();
201 mDragging=true;
202
203 if(auto sizer = parent->GetSizer())
204 {
205 for(size_t i = 0, count = sizer->GetItemCount(); i < count; ++i)
206 {
207 if(sizer->GetItem(i)->GetWindow() == this)
208 {
209 mSourceIndex = mTargetIndex = static_cast<int>(i);
210 PostDragEvent(parent, EVT_MOVABLE_CONTROL_DRAG_STARTED);
211 break;
212 }
213 }
214 }
215 }
216
217 void OnMouseUp(wxMouseEvent& evt)
218 {
219 if(!mDragging)
220 return;
221
222 ReleaseMouse();
223
224 DragFinished();
225 }
226
227 void OnMove(wxMouseEvent& evt)
228 {
229 if(!mDragging)
230 return;
231
232 auto parent = GetParent();
233 if(!parent)
234 return;
235
236 wxPoint newPosition = wxGetMousePosition() - mInitialPosition;
237 Move(GetParent()->ScreenToClient(newPosition));
238
239 if(auto boxSizer = dynamic_cast<wxBoxSizer*>(parent->GetSizer()))
240 {
241 if(boxSizer->GetOrientation() == wxVERTICAL)
242 {
243 auto targetIndex = mSourceIndex;
244
245 //assuming that items are ordered from top to bottom (i > j <=> y(i) > y(j))
246 //compare wxSizerItem position with the current MovableControl position!
247 if(GetPosition().y < boxSizer->GetItem(mSourceIndex)->GetPosition().y)
248 {
249 //moving up
250 for(int i = 0; i < mSourceIndex; ++i)
251 {
252 const auto item = boxSizer->GetItem(i);
253
254 if(GetRect().GetTop() <= item->GetPosition().y + item->GetSize().y / 2)
255 {
256 targetIndex = i;
257 break;
258 }
259 }
260 }
261 else
262 {
263 //moving down
264 for(int i = static_cast<int>(boxSizer->GetItemCount()) - 1; i > mSourceIndex; --i)
265 {
266 const auto item = boxSizer->GetItem(i);
267 if(GetRect().GetBottom() >= item->GetPosition().y + item->GetSize().y / 2)
268 {
269 targetIndex = i;
270 break;
271 }
272 }
273 }
274
275 if(targetIndex != mTargetIndex)
276 {
277 mTargetIndex = targetIndex;
278 PostDragEvent(parent, EVT_MOVABLE_CONTROL_DRAG_POSITION);
279 }
280 }
281 }
282 }
283 };
284
285 //UI control that represents individual effect from the effect list
287 {
288 wxWeakRef<AudacityProject> mProject;
289 std::shared_ptr<Track> mTrack;
290 std::shared_ptr<RealtimeEffectState> mEffectState;
291 std::shared_ptr<EffectSettingsAccess> mSettingsAccess;
292
293 ThemedButtonWrapper<wxButton>* mChangeButton{nullptr};
294 ThemedButtonWrapper<wxBitmapButton>* mEnableButton{nullptr};
295 wxWindow *mOptionsButton{};
296
297 public:
298 RealtimeEffectControl(wxWindow* parent,
299 wxWindowID winid,
300 const wxPoint& pos = wxDefaultPosition,
301 const wxSize& size = wxDefaultSize,
302 long style = wxTAB_TRAVERSAL | wxNO_BORDER)
303 : MovableControl(parent, winid, pos, size, style)
304 {
305 //Prevents flickering and paint order issues
306 MovableControl::SetBackgroundStyle(wxBG_STYLE_PAINT);
307
308 Bind(wxEVT_PAINT, &RealtimeEffectControl::OnPaint, this);
309
310 auto sizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
311
312 //On/off button
313 auto enableButton = safenew ThemedButtonWrapper<wxBitmapButton>(this, wxID_ANY, wxBitmap{}, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
314 enableButton->SetBitmapIndex(bmpEffectOn);
315 enableButton->SetBackgroundColorIndex(clrEffectListItemBackground);
316 mEnableButton = enableButton;
317
318 enableButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
319 auto pButton =
320 static_cast<ThemedButtonWrapper<wxBitmapButton>*>(mEnableButton);
321 auto index = pButton->GetBitmapIndex();
322 bool wasEnabled = (index == bmpEffectOn);
323 if (mSettingsAccess) {
324 mSettingsAccess->ModifySettings([&](EffectSettings &settings){
325 settings.extra.SetActive(!wasEnabled);
326 });
327 mSettingsAccess->Flush();
328 }
329 pButton->SetBitmapIndex(wasEnabled ? bmpEffectOff : bmpEffectOn);
330 if (mProject)
331 ProjectHistory::Get(*mProject).ModifyState(false);
332 });
333
334 //Central button with effect name
335 mChangeButton = safenew ThemedButtonWrapper<wxButton>(this, wxID_ANY);
336 mChangeButton->Bind(wxEVT_BUTTON, &RealtimeEffectControl::OnChangeButtonClicked, this);
337
338 //Show effect settings
339 auto optionsButton = safenew ThemedButtonWrapper<wxBitmapButton>(this, wxID_ANY, wxBitmap{}, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
340 optionsButton->SetBitmapIndex(bmpEffectSettingsNormal);
341 optionsButton->SetBitmapPressedIndex(bmpEffectSettingsDown);
342 optionsButton->SetBitmapCurrentIndex(bmpEffectSettingsHover);
343 optionsButton->SetBackgroundColorIndex(clrEffectListItemBackground);
344 optionsButton->Bind(wxEVT_BUTTON, &RealtimeEffectControl::OnOptionsClicked, this);
345
346 auto dragArea = safenew wxStaticBitmap(this, wxID_ANY, theTheme.Bitmap(bmpDragArea));
347 dragArea->Disable();
348 sizer->Add(dragArea, 0, wxLEFT | wxCENTER, 5);
349 sizer->Add(mEnableButton, 0, wxLEFT | wxCENTER, 5);
350 sizer->Add(mChangeButton, 1, wxLEFT | wxCENTER, 5);
351 sizer->Add(optionsButton, 0, wxLEFT | wxRIGHT | wxCENTER, 5);
352 mOptionsButton = optionsButton;
353
354 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
355 vSizer->Add(sizer.release(), 0, wxUP | wxDOWN | wxEXPAND, 10);
356
357 SetSizer(vSizer.release());
358 }
359
360 static const PluginDescriptor *GetPlugin(const PluginID &ID) {
361 auto desc = PluginManager::Get().GetPlugin(ID);
362 return desc;
363 }
364
367 {
368 const auto &ID = mEffectState->GetID();
369 const auto desc = GetPlugin(ID);
370 return desc
371 ? desc->GetSymbol().Msgid()
372 : XO("%s (missing)")
373 .Format(PluginManager::GetEffectNameFromID(ID).GET());
374 }
375
377 const std::shared_ptr<Track>& track,
378 const std::shared_ptr<RealtimeEffectState> &pState)
379 {
380 mProject = &project;
381 mTrack = track;
382 mEffectState = pState;
384 if (pState) {
385 label = GetEffectName();
386 mSettingsAccess = pState->GetAccess();
387 }
388 else
389 mSettingsAccess.reset();
390 if (mEnableButton)
391 mEnableButton->SetBitmapIndex(
392 (mSettingsAccess && mSettingsAccess->Get().extra.GetActive())
393 ? bmpEffectOn
394 : bmpEffectOff
395 );
396 mChangeButton->SetTranslatableLabel(label);
397 if (mOptionsButton)
398 mOptionsButton->Enable(pState && GetPlugin(pState->GetID()));
399 }
400
402 {
403 if(mProject == nullptr || mEffectState == nullptr)
404 return;
405
406 auto effectName = GetEffectName();
407 //After AudioIO::RemoveState call this will be destroyed
408 auto project = mProject.get();
409 auto trackName = mTrack->GetName();
410
411 AudioIO::Get()->RemoveState(*project, &*mTrack, mEffectState);
417 XO("Removed %s from %s").Format(effectName, trackName),
420 XO("Remove %s").Format(effectName)
421 );
422 }
423
424 void OnOptionsClicked(wxCommandEvent& event)
425 {
426 if(mProject == nullptr || mEffectState == nullptr)
427 return;//not initialized
428
429 const auto ID = mEffectState->GetID();
430 const auto effectPlugin = EffectManager::Get().GetEffect(ID);
431 if(effectPlugin == nullptr)
432 {
434 return;
435 }
436 auto access = mEffectState->GetAccess();
437 // Copy settings
438 auto initialSettings = access->Get();
439 auto cleanup = EffectManager::Get().SetBatchProcessing(ID);
440
441 std::shared_ptr<EffectInstance> pInstance;
442
443 // Like the call in EffectManager::PromptUser, but access causes
444 // the necessary inter-thread communication of settings changes
445 // if play is in progress
446 bool changed = effectPlugin->ShowHostInterface(
447 ProjectWindow::Get( *mProject), DialogFactory(mEffectState),
448 pInstance, *access, true );
449
450 if (changed)
451 {
452 auto effectName = GetEffectName();
458 XO("Changed %s in %s").Format(effectName, mTrack->GetName()),
461 XO("Change %s").Format(effectName)
462 );
463 }
464 else
465 // Dialog was cancelled.
466 // Reverse any temporary changes made to the state
467 access->Set(std::move(initialSettings));
468 }
469
470 void OnChangeButtonClicked(wxCommandEvent& event)
471 {
472 if(!mTrack || mProject == nullptr)
473 return;
474 if(mEffectState == nullptr)
475 return;//not initialized
476
477 const auto effectID = ShowSelectEffectMenu(mChangeButton, this);
478 if(effectID.empty())
479 return;
480
481 auto &em = RealtimeEffectManager::Get(*mProject);
482 auto oIndex = em.FindState(&*mTrack, mEffectState);
483 if (!oIndex)
484 return;
485
486 auto oldName = GetEffectName();
487 auto &project = *mProject;
488 auto trackName = mTrack->GetName();
489 if (auto state = AudioIO::Get()
490 ->ReplaceState(project, &*mTrack, *oIndex, effectID)
491 ){
492 // Message subscription took care of updating the button text
493 // and destroyed `this`!
494 auto effect = state->GetEffect();
495 assert(effect); // postcondition of ReplaceState
497 /*i18n-hint: undo history,
498 first and second parameters - realtime effect names
499 */
500 XO("Replaced %s with %s")
501 .Format(oldName, effect->GetName()),
504 XO("Replace %s").Format(oldName));
505 }
506 }
507
508 void OnPaint(wxPaintEvent&)
509 {
510#ifdef __WXMSW__
511 wxBufferedPaintDC dc(this);
512#else
513 wxPaintDC dc(this);
514#endif
515 const auto rect = wxRect(GetSize());
516
517 dc.SetPen(*wxTRANSPARENT_PEN);
518 dc.SetBrush(GetBackgroundColour());
519 dc.DrawRectangle(rect);
520
521 dc.SetPen(theTheme.Colour(clrEffectListItemBorder));
522 dc.SetBrush(theTheme.Colour(clrEffectListItemBorder));
523 dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
524 }
525
526 };
527
528 static wxString GetSafeVendor(const PluginDescriptor& descriptor)
529 {
530 if (descriptor.GetVendor().empty())
531 return XO("Unknown").Translation();
532
533 return descriptor.GetVendor();
534 }
535
536 PluginID ShowSelectEffectMenu(wxWindow* parent, RealtimeEffectControl* currentEffectControl)
537 {
538 wxMenu menu;
539
540 if(currentEffectControl != nullptr)
541 {
542 //no need to handle language change since menu creates it's own event loop
543 menu.Append(wxID_REMOVE, _("No Effect"));
544 menu.AppendSeparator();
545 }
546
547 auto& pluginManager = PluginManager::Get();
548
549 std::vector<const PluginDescriptor*> effects;
550 int selectedEffectIndex = -1;
551
552 auto compareEffects = [](const PluginDescriptor* a, const PluginDescriptor* b)
553 {
554 const auto vendorA = GetSafeVendor(*a);
555 const auto vendorB = GetSafeVendor(*b);
556
557 return vendorA < vendorB ||
558 (vendorA == vendorB &&
559 a->GetSymbol().Translation() < b->GetSymbol().Translation());
560 };
561
562 for(auto& effect : pluginManager.EffectsOfType(EffectTypeProcess))
563 {
564 if(!effect.IsEffectRealtime() || !effect.IsEnabled())
565 continue;
566 effects.push_back(&effect);
567 }
568
569 std::sort(effects.begin(), effects.end(), compareEffects);
570
571 wxString currentSubMenuName;
572 std::unique_ptr<wxMenu> currentSubMenu;
573
574 auto submenuEventHandler = [&](wxCommandEvent& event)
575 {
576 selectedEffectIndex = event.GetId() - wxID_HIGHEST;
577 };
578
579 for(int i = 0, count = effects.size(); i < count; ++i)
580 {
581 auto& effect = *effects[i];
582
583 const wxString vendor = GetSafeVendor(effect);
584
585 if(currentSubMenuName != vendor)
586 {
587 if(currentSubMenu)
588 {
589 currentSubMenu->Bind(wxEVT_MENU, submenuEventHandler);
590 menu.AppendSubMenu(currentSubMenu.release(), currentSubMenuName);
591 }
592 currentSubMenuName = vendor;
593 currentSubMenu = std::make_unique<wxMenu>();
594 }
595
596 const auto ID = wxID_HIGHEST + i;
597 currentSubMenu->Append(ID, effect.GetSymbol().Translation());
598 }
599 if(currentSubMenu)
600 {
601 currentSubMenu->Bind(wxEVT_MENU, submenuEventHandler);
602 menu.AppendSubMenu(currentSubMenu.release(), currentSubMenuName);
603 menu.AppendSeparator();
604 }
605 menu.Append(wxID_MORE, _("Get more effects..."));
606
607 menu.Bind(wxEVT_MENU, [&](wxCommandEvent& event)
608 {
609 if(event.GetId() == wxID_REMOVE)
610 currentEffectControl->RemoveFromList();
611 else if(event.GetId() == wxID_MORE)
612 OpenInDefaultBrowser("https://plugins.audacityteam.org/");
613 });
614
615 if(parent->PopupMenu(&menu) && selectedEffectIndex != -1)
616 return effects[selectedEffectIndex]->GetID();
617
618 return {};
619 }
620}
621
622class RealtimeEffectListWindow : public wxScrolledWindow
623{
624 wxWeakRef<AudacityProject> mProject;
625 std::shared_ptr<Track> mTrack;
626 wxButton* mAddEffect{nullptr};
627 wxStaticText* mAddEffectHint{nullptr};
628 wxWindow* mAddEffectTutorialLink{nullptr};
629 wxWindow* mEffectListContainer{nullptr};
630
632
633public:
634 RealtimeEffectListWindow(wxWindow *parent,
635 wxWindowID winid = wxID_ANY,
636 const wxPoint& pos = wxDefaultPosition,
637 const wxSize& size = wxDefaultSize,
638 long style = wxScrolledWindowStyle,
639 const wxString& name = wxPanelNameStr)
640 : wxScrolledWindow(parent, winid, pos, size, style, name)
641 {
642 Bind(wxEVT_SIZE, &RealtimeEffectListWindow::OnSizeChanged, this);
643#ifdef __WXMSW__
644 //Fixes flickering on redraw
645 wxScrolledWindow::SetDoubleBuffered(true);
646#endif
647 auto rootSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
648
649 auto addEffect = safenew ThemedButtonWrapper<wxButton>(this, wxID_ANY);
650 addEffect->SetTranslatableLabel(XO("Add effect"));
651 addEffect->Bind(wxEVT_BUTTON, &RealtimeEffectListWindow::OnAddEffectClicked, this);
652 mAddEffect = addEffect;
653
654 auto addEffectHint = safenew ThemedWindowWrapper<wxStaticText>(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE);
655 //Workaround: text is set in the OnSizeChange
656 addEffectHint->SetForegroundColorIndex(clrTrackPanelText);
657 mAddEffectHint = addEffectHint;
658
659 //TODO: set link
660 auto addEffectTutorialLink = safenew ThemedWindowWrapper<wxHyperlinkCtrl>(this, wxID_ANY, "x", "https://www.audacityteam.org");
661 //i18n-hint: Hyperlink to the effects stack panel tutorial video
662 addEffectTutorialLink->SetTranslatableLabel(XO("Watch video"));
663 mAddEffectTutorialLink = addEffectTutorialLink;
664
665 auto effectListContainer = safenew ThemedWindowWrapper<wxWindow>(this, wxID_ANY);
666 effectListContainer->SetBackgroundColorIndex(clrMedium);
667 effectListContainer->SetSizer(safenew wxBoxSizer(wxVERTICAL));
668 effectListContainer->SetDoubleBuffered(true);
669 mEffectListContainer = effectListContainer;
670
671 //indicates the insertion position of the item
672 auto dropHintLine = safenew ThemedWindowWrapper<DropHintLine>(effectListContainer, wxID_ANY);
673 dropHintLine->SetBackgroundColorIndex(clrDropHintHighlight);
674 dropHintLine->Hide();
675
676 rootSizer->Add(mEffectListContainer, 0, wxEXPAND | wxBOTTOM, 10);
677 rootSizer->Add(addEffect, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 20);
678 rootSizer->Add(addEffectHint, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 20);
679 rootSizer->Add(addEffectTutorialLink, 0, wxLEFT | wxRIGHT | wxEXPAND, 20);
680
681 SetSizer(rootSizer.release());
682 SetMinSize({});
683
684 Bind(EVT_MOVABLE_CONTROL_DRAG_STARTED, [dropHintLine](const MovableControlEvent& event)
685 {
686 if(auto window = dynamic_cast<wxWindow*>(event.GetEventObject()))
687 window->Raise();
688 });
689 Bind(EVT_MOVABLE_CONTROL_DRAG_POSITION, [this, dropHintLine](const MovableControlEvent& event)
690 {
691 constexpr auto DropHintLineHeight { 3 };//px
692
693 auto sizer = mEffectListContainer->GetSizer();
694 assert(sizer != nullptr);
695
696 if(event.GetSourceIndex() == event.GetTargetIndex())
697 {
698 //do not display hint line if position didn't change
699 dropHintLine->Hide();
700 return;
701 }
702
703 if(!dropHintLine->IsShown())
704 {
705 dropHintLine->Show();
706 dropHintLine->Raise();
707 if(auto window = dynamic_cast<wxWindow*>(event.GetEventObject()))
708 window->Raise();
709 }
710
711 auto item = sizer->GetItem(event.GetTargetIndex());
712 dropHintLine->SetSize(item->GetSize().x, DropHintLineHeight);
713
714 if(event.GetTargetIndex() > event.GetSourceIndex())
715 dropHintLine->SetPosition(item->GetRect().GetBottomLeft() - wxPoint(0, DropHintLineHeight));
716 else
717 dropHintLine->SetPosition(item->GetRect().GetTopLeft());
718 });
719 Bind(EVT_MOVABLE_CONTROL_DRAG_FINISHED, [this, dropHintLine](const MovableControlEvent& event)
720 {
721 dropHintLine->Hide();
722
723 if(mProject == nullptr)
724 return;
725
726 auto& effectList = RealtimeEffectList::Get(*mTrack);
727 const auto from = event.GetSourceIndex();
728 const auto to = event.GetTargetIndex();
729 if(from != to)
730 {
731 auto effectName =
732 effectList.GetStateAt(from)->GetEffect()->GetName();
733 bool up = (to < from);
734 effectList.MoveEffect(from, to);
736 (up
741 ? XO("Moved %s up in %s")
746 : XO("Moved %s down in %s"))
747 .Format(effectName, mTrack->GetName()),
748 XO("Change effect order"), UndoPush::CONSOLIDATE);
749 }
750 else
751 {
752 wxWindowUpdateLocker freeze(this);
753 Layout();
754 }
755 });
756 SetScrollRate(0, 20);
757 }
758
759 void OnSizeChanged(wxSizeEvent& event)
760 {
761 if(auto sizerItem = GetSizer()->GetItem(mAddEffectHint))
762 {
763 //We need to wrap the text whenever panel width changes and adjust widget height
764 //so that text is fully visible, but there is no height-for-width layout algorithm
765 //in wxWidgets yet, so for now we just do it manually
766
767 //Restore original text, because 'Wrap' will replace it with wrapped one
768 mAddEffectHint->SetLabel(_("Realtime effects are non-destructive and can be changed at any time."));
769 mAddEffectHint->Wrap(GetClientSize().x - sizerItem->GetBorder() * 2);
770 mAddEffectHint->InvalidateBestSize();
771 }
772 event.Skip();
773 }
774
776 {
777 const auto insertItem = [this, &msg](){
778 auto& effects = RealtimeEffectList::Get(*mTrack);
779 InsertEffectRow(msg.srcIndex, effects.GetStateAt(msg.srcIndex));
780 };
781 const auto removeItem = [this, &msg](){
782 auto sizer = mEffectListContainer->GetSizer();
783 auto item = sizer->GetItem(msg.srcIndex)->GetWindow();
784 item->Destroy();
785#ifdef __WXGTK__
786 // See comment in ReloadEffectsList
787 if(sizer->IsEmpty())
788 mEffectListContainer->Hide();
789#endif
790 };
791
792 wxWindowUpdateLocker freeze(this);
794 {
795 const auto sizer = mEffectListContainer->GetSizer();
796
797 const auto movedItem = sizer->GetItem(msg.srcIndex);
798
799 const auto proportion = movedItem->GetProportion();
800 const auto flag = movedItem->GetFlag();
801 const auto border = movedItem->GetBorder();
802 const auto window = movedItem->GetWindow();
803
804 sizer->Remove(msg.srcIndex);
805 sizer->Insert(msg.dstIndex, window, proportion, flag, border);
806 }
808 {
809 insertItem();
810 }
812 {
813 removeItem();
814 insertItem();
815 }
817 {
818 removeItem();
819 }
820 SendSizeEventToParent();
821 }
822
824 {
826
827 mTrack.reset();
828 mProject = nullptr;
830 }
831
832 void SetTrack(AudacityProject& project, const std::shared_ptr<Track>& track)
833 {
834 if(mTrack == track)
835 return;
836
838
839 mTrack = track;
840 mProject = &project;
842
843 if(track)
844 {
845 auto& effects = RealtimeEffectList::Get(*mTrack);
847 }
848 }
849
850 void EnableEffects(bool enable)
851 {
852 if (mTrack)
853 RealtimeEffectList::Get(*mTrack).SetActive(enable);
854 }
855
857 {
858 wxWindowUpdateLocker freeze(this);
859 //delete items that were added to the sizer
860 mEffectListContainer->GetSizer()->Clear(true);
861
862#ifdef __WXGTK__
863 //Workaround for GTK: Underlying GTK widget does not update
864 //its size when wxWindow size is set to zero
865 if(!mTrack || RealtimeEffectList::Get(*mTrack).GetStatesCount() == 0)
866 mEffectListContainer->Hide();
867#endif
868
869 if(mTrack)
870 {
871 auto& effects = RealtimeEffectList::Get(*mTrack);
872 for(size_t i = 0, count = effects.GetStatesCount(); i < count; ++i)
873 InsertEffectRow(i, effects.GetStateAt(i));
874 }
875 mAddEffect->Enable(!!mTrack);
876 SendSizeEventToParent();
877 }
878
879 void OnAddEffectClicked(const wxCommandEvent& event)
880 {
881 if(!mTrack || mProject == nullptr)
882 return;
883
884 const auto effectID = ShowSelectEffectMenu(dynamic_cast<wxWindow*>(event.GetEventObject()));
885 if(effectID.empty())
886 return;
887
888 if(auto state = AudioIO::Get()->AddState(*mProject, &*mTrack, effectID))
889 {
890 auto effect = state->GetEffect();
891 assert(effect); // postcondition of AddState
892 const auto effectName = effect->GetName();
898 XO("Added %s to %s").Format(effectName, mTrack->GetName()),
899 //i18n-hint: undo history record
900 XO("Add %s").Format(effectName));
901 }
902 }
903
904 void InsertEffectRow(size_t index,
905 const std::shared_ptr<RealtimeEffectState> &pState)
906 {
907 if(mProject == nullptr)
908 return;
909
910#ifdef __WXGTK__
911 // See comment in ReloadEffectsList
912 if(!mEffectListContainer->IsShown())
913 mEffectListContainer->Show();
914#endif
915
917 row->SetBackgroundColorIndex(clrEffectListItemBackground);
918 row->SetEffect(*mProject, mTrack, pState);
919 mEffectListContainer->GetSizer()->Insert(index, row, 0, wxEXPAND);
920 }
921};
922
923
924RealtimeEffectPanel::RealtimeEffectPanel(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size,
925 long style, const wxString& name)
926 : wxWindow(parent, id, pos, size, style, name)
927{
928 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
929
930 auto header = safenew ThemedWindowWrapper<wxWindow>(this, wxID_ANY);
931 header->SetBackgroundColorIndex(clrMedium);
932 {
933 auto hSizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
934 auto toggleEffects = safenew ThemedButtonWrapper<wxBitmapButton>(header, wxID_ANY, wxBitmap{}, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
935 toggleEffects->SetBitmapIndex(bmpEffectOn);
936 toggleEffects->SetBackgroundColorIndex(clrMedium);
937 mToggleEffects = toggleEffects;
938
939 toggleEffects->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
940 auto pButton =
942 auto index = pButton->GetBitmapIndex();
943 bool wasEnabled = (index == bmpEffectOn);
944 if (mEffectList) {
945 mEffectList->EnableEffects(!wasEnabled);
946 }
947 pButton->SetBitmapIndex(wasEnabled ? bmpEffectOff : bmpEffectOn);
948 if (mProject)
949 ProjectHistory::Get(*mProject).ModifyState(false);
950 });
951
952 hSizer->Add(toggleEffects, 0, wxSTRETCH_NOT | wxALIGN_CENTER | wxLEFT, 5);
953 {
954 auto vSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
955
956 auto headerText = safenew ThemedWindowWrapper<wxStaticText>(header, wxID_ANY, wxEmptyString);
957 headerText->SetFont(wxFont(wxFontInfo().Bold()));
958 headerText->SetTranslatableLabel(XO("Realtime Effects"));
959 headerText->SetForegroundColorIndex(clrTrackPanelText);
960
961 auto trackTitle = safenew ThemedWindowWrapper<wxStaticText>(header, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
962 trackTitle->SetForegroundColorIndex(clrTrackPanelText);
963 mTrackTitle = trackTitle;
964
965 vSizer->Add(headerText);
966 vSizer->Add(trackTitle);
967
968 hSizer->Add(vSizer.release(), 1, wxEXPAND | wxALL, 10);
969 }
970 auto close = safenew ThemedButtonWrapper<wxBitmapButton>(header, wxID_ANY, wxBitmap{}, wxDefaultPosition, wxDefaultSize, wxNO_BORDER);
971 close->SetBitmapLabelIndex(bmpCloseNormal);
972 close->SetBitmapPressedIndex(bmpCloseDown);
973 close->SetBitmapCurrentIndex(bmpCloseHover);
974 close->SetBackgroundColorIndex(clrMedium);
975
976 close->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
977
978 hSizer->Add(close, 0, wxSTRETCH_NOT | wxALIGN_CENTER | wxRIGHT, 5);
979
980 header->SetSizer(hSizer.release());
981 }
982 vSizer->Add(header, 0, wxEXPAND);
983
984 auto effectList = safenew ThemedWindowWrapper<RealtimeEffectListWindow>(this, wxID_ANY);
985 effectList->SetBackgroundColorIndex(clrMedium);
986 vSizer->Add(effectList, 1, wxEXPAND);
987 mEffectList = effectList;
988
989 SetSizerAndFit(vSizer.release());
990}
991
992void RealtimeEffectPanel::SetTrack(AudacityProject& project, const std::shared_ptr<Track>& track)
993{
994 //Avoid creation-on-demand of a useless, empty list in case the track is of non-wave type.
995 if(track && dynamic_cast<WaveTrack*>(&*track) != nullptr)
996 {
997 mTrackTitle->SetLabel(track->GetName());
998 mToggleEffects->Enable(true);
1000 (track && RealtimeEffectList::Get(*track).IsActive())
1001 ? bmpEffectOn
1002 : bmpEffectOff
1003 );
1004 mEffectList->SetTrack(project, track);
1005 mProject = &project;
1006 }
1007 else
1008 ResetTrack();
1009}
1010
1012{
1013 mTrackTitle->SetLabel(wxEmptyString);
1014 mToggleEffects->SetBitmapIndex(bmpEffectOff);
1015 mToggleEffects->Enable(false);
1017 mProject = nullptr;
1018}
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:522
#define XO(s)
Definition: Internat.h:31
#define _(s)
Definition: Internat.h:75
#define safenew
Definition: MemoryX.h:10
std::unique_ptr< T, Destroyer< T > > Destroy_ptr
a convenience for using Destroyer
Definition: MemoryX.h:162
AudacityProject * FindProjectFromWindow(wxWindow *pWindow)
TranslatableString label
Definition: TagsEditor.cpp:163
THEME_API Theme theTheme
Definition: Theme.cpp:82
declares abstract base class Track, TrackList, and iterators over TrackList
static Settings & settings()
Definition: TrackInfo.cpp:87
int id
static std::once_flag flag
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:372
const wxString Translation() const
EffectPlugin * GetEffect(const PluginID &ID)
BatchProcessingScope SetBatchProcessing(const PluginID &ID)
Begin a scope that ends when the returned object is destroyed.
static EffectManager & Get()
Factory of instances of an effect and of dialogs to control them.
Definition: EffectPlugin.h:54
EffectUIClientInterface is an abstract base class to populate a UI and validate UI values....
bool Initialize()
Definition: EffectUI.cpp:469
Abstract base class used in importing a file.
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()
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)
wxStaticText * mTrackTitle
ThemedButtonWrapper< wxBitmapButton > * mToggleEffects
void SetTrack(AudacityProject &project, const std::shared_ptr< Track > &track)
Shows effects from the effect stack of the track.
wxWeakRef< AudacityProject > mProject
RealtimeEffectListWindow * mEffectList
RealtimeEffectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0, const wxString &name=wxPanelNameStr)
wxColour & Colour(int iIndex)
wxBitmap & Bitmap(int iIndex)
void SetBitmapIndex(int index)
int GetBitmapIndex() const
void SetBitmapLabelIndex(int index)
Holds a msgid for the translation catalog; may also bind format arguments.
A Track that contains audio waveform data.
Definition: WaveTrack.h:57
DropHintLine(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize)
void PostDragEvent(wxWindow *target, wxEventType eventType)
MovableControl(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=wxTAB_TRAVERSAL|wxNO_BORDER, const wxString &name=wxPanelNameStr)
void SetEffect(AudacityProject &project, const std::shared_ptr< Track > &track, const std::shared_ptr< RealtimeEffectState > &pState)
static const PluginDescriptor * GetPlugin(const PluginID &ID)
RealtimeEffectControl(wxWindow *parent, wxWindowID winid, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=wxTAB_TRAVERSAL|wxNO_BORDER)
static wxString GetSafeVendor(const PluginDescriptor &descriptor)
wxDEFINE_EVENT(EVT_MOVABLE_CONTROL_DRAG_FINISHED, MovableControlEvent)
PluginID ShowSelectEffectMenu(wxWindow *parent, RealtimeEffectControl *currentEffectControl=nullptr)
auto DialogFactory(const std::shared_ptr< RealtimeEffectState > &pEffectState)
Externalized state of a plug-in.
@ Replace
Effect item was replaced with a new item at srcIndex position.
@ Remove
Effect item was removed from the list at srcIndex position.
@ Move
Item position has changed, from srcIndex to dstIndex.
@ Insert
New effect item was added to the list at srcIndex position.