Browse Source

Windows: Refactored some of the recent DPI-aware VST2 and VST3 plug-in changes

tags/2021-05-28
ed 7 years ago
parent
commit
fc203d62d9
3 changed files with 138 additions and 180 deletions
  1. +47
    -77
      modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp
  2. +87
    -94
      modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp
  3. +4
    -9
      modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp

+ 47
- 77
modules/juce_audio_plugin_client/VST/juce_VST_Wrapper.cpp View File

@@ -1100,15 +1100,7 @@ public:
} }
if (editorComp != nullptr) if (editorComp != nullptr)
{
editorComp->checkVisibility(); editorComp->checkVisibility();
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
if (getHostType().isWavelab())
if (auto* peer = editorComp->getTopLevelComponent()->getPeer())
handleSetContentScaleFactor ((float) peer->getPlatformScaleFactor());
#endif
}
} }
void createEditorComp() void createEditorComp()
@@ -1241,17 +1233,9 @@ public:
// A component to hold the AudioProcessorEditor, and cope with some housekeeping // A component to hold the AudioProcessorEditor, and cope with some housekeeping
// chores when it changes or repaints. // chores when it changes or repaints.
struct EditorCompWrapper : public Component struct EditorCompWrapper : public Component
#if ! JUCE_MAC
, public ComponentPeer::ScaleFactorListener,
public ComponentMovementWatcher
#endif
{ {
EditorCompWrapper (JuceVSTWrapper& w, AudioProcessorEditor& editor) EditorCompWrapper (JuceVSTWrapper& w, AudioProcessorEditor& editor)
:
#if ! JUCE_MAC
ComponentMovementWatcher (this),
#endif
wrapper (w)
: wrapper (w)
{ {
editor.setOpaque (true); editor.setOpaque (true);
editor.setVisible (true); editor.setVisible (true);
@@ -1276,11 +1260,6 @@ public:
{ {
deleteAllChildren(); // note that we can't use a std::unique_ptr because the editor may deleteAllChildren(); // note that we can't use a std::unique_ptr because the editor may
// have been transferred to another parent which takes over ownership. // have been transferred to another parent which takes over ownership.
#if ! JUCE_MAC
for (int i = 0; i < ComponentPeer::getNumPeers(); ++i)
if (auto* peer = ComponentPeer::getPeer (i))
peer->removeScaleFactorListener (this);
#endif
} }
void paint (Graphics&) override {} void paint (Graphics&) override {}
@@ -1293,6 +1272,11 @@ public:
bounds.left = 0; bounds.left = 0;
bounds.bottom = (int16) b.getHeight(); bounds.bottom = (int16) b.getHeight();
bounds.right = (int16) b.getWidth(); bounds.right = (int16) b.getWidth();
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
bounds.bottom = (int16) roundToInt (bounds.bottom * wrapper.editorScaleFactor);
bounds.right = (int16) roundToInt (bounds.right * wrapper.editorScaleFactor);
#endif
} }
void attachToHost (VstOpCodeArguments args) void attachToHost (VstOpCodeArguments args)
@@ -1303,10 +1287,14 @@ public:
#if JUCE_WINDOWS #if JUCE_WINDOWS
addToDesktop (0, args.ptr); addToDesktop (0, args.ptr);
hostWindow = (HWND) args.ptr; hostWindow = (HWND) args.ptr;
#if JUCE_WIN_PER_MONITOR_DPI_AWARE
// workaround for plug-ins opening on an auxiliary monitor
Timer::callAfterDelay (250, [this] { updateWindowSize (false); });
#endif
if (auto* ed = getEditorComp())
#if JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* peer = ed->getPeer())
wrapper.editorScaleFactor = (float) peer->getPlatformScaleFactor();
#else
ed->setScaleFactor (wrapper.editorScaleFactor);
#endif
#elif JUCE_LINUX #elif JUCE_LINUX
addToDesktop (0, args.ptr); addToDesktop (0, args.ptr);
hostWindow = (Window) args.ptr; hostWindow = (Window) args.ptr;
@@ -1346,29 +1334,16 @@ public:
return dynamic_cast<AudioProcessorEditor*> (getChildComponent(0)); return dynamic_cast<AudioProcessorEditor*> (getChildComponent(0));
} }
float getNativeEditorScaleFactor() const noexcept { return nativeScaleFactor; }
#if ! JUCE_MAC
void componentMovedOrResized (bool, bool) override {}
void componentPeerChanged() override
{
if (auto* peer = getTopLevelComponent()->getPeer())
peer->addScaleFactorListener (this);
}
void componentVisibilityChanged() override
{
if (auto* peer = getTopLevelComponent()->getPeer())
nativeScaleFactorChanged (peer->getPlatformScaleFactor());
}
void nativeScaleFactorChanged (double newScaleFactor) override
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
void checkScaleFactorIsCorrect()
{ {
nativeScaleFactor = (float) newScaleFactor;
if (auto* peer = getEditorComp()->getPeer())
{
auto peerScaleFactor = (float) peer->getPlatformScaleFactor();
if (getHostType().isBitwigStudio())
updateWindowSize (true);
if (! approximatelyEqual (peerScaleFactor, wrapper.editorScaleFactor))
wrapper.handleSetContentScaleFactor (peerScaleFactor);
}
} }
#endif #endif
@@ -1376,13 +1351,16 @@ public:
{ {
if (auto* ed = getEditorComp()) if (auto* ed = getEditorComp())
{ {
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
checkScaleFactorIsCorrect();
#endif
ed->setTopLeftPosition (0, 0); ed->setTopLeftPosition (0, 0);
if (shouldResizeEditor) if (shouldResizeEditor)
ed->setBounds (ed->getLocalArea (this, getLocalBounds())); ed->setBounds (ed->getLocalArea (this, getLocalBounds()));
if (! getHostType().isBitwigStudio())
updateWindowSize (false);
updateWindowSize (false);
} }
#if JUCE_MAC && ! JUCE_64BIT #if JUCE_MAC && ! JUCE_64BIT
@@ -1430,8 +1408,8 @@ public:
#else #else
ignoreUnused (resizeEditor); ignoreUnused (resizeEditor);
XResizeWindow (display.display, (Window) getWindowHandle(), XResizeWindow (display.display, (Window) getWindowHandle(),
static_cast<unsigned int> (roundToInt (pos.getWidth() * nativeScaleFactor)),
static_cast<unsigned int> (roundToInt (pos.getHeight() * nativeScaleFactor)));
static_cast<unsigned int> (roundToInt (pos.getWidth() * wrapper.editorScaleFactor)),
static_cast<unsigned int> (roundToInt (pos.getHeight() * wrapper.editorScaleFactor)));
#endif #endif
#if JUCE_MAC #if JUCE_MAC
@@ -1451,11 +1429,15 @@ public:
if (status == (pointer_sized_int) 1 || getHostType().isAbletonLive()) if (status == (pointer_sized_int) 1 || getHostType().isAbletonLive())
{ {
isInSizeWindow = true;
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
newWidth = roundToInt (newWidth * wrapper.editorScaleFactor);
newHeight = roundToInt (newHeight * wrapper.editorScaleFactor);
#endif
const ScopedValueSetter<bool> inSizeWindowSetter (isInSizeWindow, true);
sizeWasSuccessful = (host (wrapper.getAEffect(), Vst2::audioMasterSizeWindow, sizeWasSuccessful = (host (wrapper.getAEffect(), Vst2::audioMasterSizeWindow,
roundToInt (newWidth * nativeScaleFactor),
roundToInt (newHeight * nativeScaleFactor), 0, 0) != 0);
isInSizeWindow = false;
newWidth, newHeight, 0, 0) != 0);
} }
} }
@@ -1552,8 +1534,6 @@ public:
bool isInSizeWindow = false; bool isInSizeWindow = false;
bool shouldResizeEditor = true; bool shouldResizeEditor = true;
float nativeScaleFactor = 1.0f;
#if JUCE_MAC #if JUCE_MAC
void* hostWindow = {}; void* hostWindow = {};
#elif JUCE_LINUX #elif JUCE_LINUX
@@ -2192,31 +2172,17 @@ private:
pointer_sized_int handleSetContentScaleFactor (float scale) pointer_sized_int handleSetContentScaleFactor (float scale)
{ {
#if ! JUCE_MAC #if ! JUCE_MAC
if (editorComp != nullptr)
if (! approximatelyEqual (scale, editorScaleFactor))
{ {
#if JUCE_WINDOWS && ! JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* ed = editorComp->getEditorComp())
{
ed->setScaleFactor (scale);
editorComp->updateWindowSize (true);
}
#else
if (! approximatelyEqual (scale, (float) editorComp->getNativeEditorScaleFactor()))
{
editorComp->nativeScaleFactorChanged ((double) scale);
editorScaleFactor = scale;
#if JUCE_LINUX
MessageManager::callAsync ([this] { if (editorComp != nullptr) editorComp->updateWindowSize (true); });
if (editorComp != nullptr)
#if JUCE_WINDOWS && ! JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* ed = editorComp->getEditorComp())
ed->setScaleFactor (scale);
#else #else
editorComp->updateWindowSize (true); editorComp->updateWindowSize (true);
#endif #endif
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
if (getHostType().isStudioOne())
Timer::callAfterDelay (100, [this] { if (editorComp != nullptr) editorComp->updateWindowSize (false); });
#endif
}
#endif
} }
#else #else
ignoreUnused (scale); ignoreUnused (scale);
@@ -2277,6 +2243,10 @@ private:
MidiBuffer midiEvents; MidiBuffer midiEvents;
VSTMidiEventList outgoingEvents; VSTMidiEventList outgoingEvents;
#if ! JUCE_MAC
float editorScaleFactor = 1.0f;
#endif
LegacyAudioParametersWrapper juceParameters; LegacyAudioParametersWrapper juceParameters;
bool isProcessing = false, isBypassed = false, hasShutdown = false; bool isProcessing = false, isBypassed = false, hasShutdown = false;


+ 87
- 94
modules/juce_audio_plugin_client/VST3/juce_VST3_Wrapper.cpp View File

@@ -720,6 +720,7 @@ private:
//============================================================================== //==============================================================================
Atomic<int> vst3IsPlaying { 0 }; Atomic<int> vst3IsPlaying { 0 };
float lastScaleFactorReceived = 1.0f;
void setupParameters() void setupParameters()
{ {
@@ -808,6 +809,8 @@ private:
: Vst::EditorView (&ec, nullptr), : Vst::EditorView (&ec, nullptr),
owner (&ec), pluginInstance (p) owner (&ec), pluginInstance (p)
{ {
editorScaleFactor = ec.lastScaleFactorReceived;
component.reset (new ContentWrapperComponent (*this, p)); component.reset (new ContentWrapperComponent (*this, p));
} }
@@ -853,7 +856,7 @@ private:
#endif #endif
#if ! JUCE_MAC #if ! JUCE_MAC
setContentScaleFactor ((Steinberg::IPlugViewContentScaleSupport::ScaleFactor) scaleFactor);
setContentScaleFactor ((Steinberg::IPlugViewContentScaleSupport::ScaleFactor) editorScaleFactor);
#endif #endif
component->resizeHostWindow(); component->resizeHostWindow();
@@ -895,10 +898,15 @@ private:
if (component != nullptr) if (component != nullptr)
{ {
auto scale = component->getNativeEditorScaleFactor();
auto w = rect.getWidth();
auto h = rect.getHeight();
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
w = roundToInt (w / editorScaleFactor);
h = roundToInt (h / editorScaleFactor);
#endif
component->setSize (roundToInt (rect.getWidth() / scale),
roundToInt (rect.getHeight() / scale));
component->setSize (w, h);
if (auto* peer = component->getPeer()) if (auto* peer = component->getPeer())
peer->updateBounds(); peer->updateBounds();
@@ -915,11 +923,15 @@ private:
{ {
if (size != nullptr && component != nullptr) if (size != nullptr && component != nullptr)
{ {
auto scale = component->getNativeEditorScaleFactor();
auto w = component->getWidth();
auto h = component->getHeight();
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
w = roundToInt (w * editorScaleFactor);
h = roundToInt (h * editorScaleFactor);
#endif
*size = ViewRect (0, 0,
roundToInt (component->getWidth() * scale),
roundToInt (component->getHeight() * scale));
*size = ViewRect (0, 0, w, h);
return kResultTrue; return kResultTrue;
} }
@@ -942,12 +954,14 @@ private:
{ {
if (auto* editor = component->pluginEditor.get()) if (auto* editor = component->pluginEditor.get())
{ {
// checkSizeConstraint
auto scale = component->getNativeEditorScaleFactor();
auto scaledRect = (Rectangle<int>::leftTopRightBottom (rectToCheck->left, rectToCheck->top,
rectToCheck->right, rectToCheck->bottom).toFloat() / scale).toNearestInt();
auto juceRect = editor->getLocalArea (component.get(), scaledRect);
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
auto juceRect = editor->getLocalArea (component.get(),
Rectangle<int>::leftTopRightBottom (rectToCheck->left, rectToCheck->top,
rectToCheck->right, rectToCheck->bottom) / editorScaleFactor);
#else
auto juceRect = editor->getLocalArea (component.get(),
{ rectToCheck->left, rectToCheck->top, rectToCheck->right, rectToCheck->bottom });
#endif
if (auto* constrainer = editor->getConstrainer()) if (auto* constrainer = editor->getConstrainer())
{ {
@@ -963,8 +977,13 @@ private:
juceRect = component->getLocalArea (editor, juceRect); juceRect = component->getLocalArea (editor, juceRect);
rectToCheck->right = rectToCheck->left + roundToInt (juceRect.getWidth() * scale);
rectToCheck->bottom = rectToCheck->top + roundToInt (juceRect.getHeight() * scale);
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
rectToCheck->right = rectToCheck->left + roundToInt (juceRect.getWidth() * editorScaleFactor);
rectToCheck->bottom = rectToCheck->top + roundToInt (juceRect.getHeight() * editorScaleFactor);
#else
rectToCheck->right = rectToCheck->left + juceRect.getWidth();
rectToCheck->bottom = rectToCheck->top + juceRect.getHeight();
#endif
} }
} }
@@ -978,20 +997,29 @@ private:
tresult PLUGIN_API setContentScaleFactor (Steinberg::IPlugViewContentScaleSupport::ScaleFactor factor) override tresult PLUGIN_API setContentScaleFactor (Steinberg::IPlugViewContentScaleSupport::ScaleFactor factor) override
{ {
#if ! JUCE_MAC #if ! JUCE_MAC
scaleFactor = static_cast<float> (factor);
if (! approximatelyEqual ((float) factor, editorScaleFactor))
{
editorScaleFactor = (float) factor;
if (component == nullptr)
return kResultFalse;
if (auto* o = owner.get())
o->lastScaleFactorReceived = editorScaleFactor;
#if JUCE_WINDOWS && ! JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* ed = component->pluginEditor.get())
ed->setScaleFactor (scaleFactor);
#else
if (! approximatelyEqual (component->getNativeEditorScaleFactor(), scaleFactor))
component->nativeScaleFactorChanged ((double) scaleFactor);
#endif
if (component == nullptr)
return kResultFalse;
component->resizeHostWindow();
#if JUCE_WINDOWS && ! JUCE_WIN_PER_MONITOR_DPI_AWARE
if (auto* ed = component->pluginEditor.get())
ed->setScaleFactor ((float) factor);
#endif
component->resizeHostWindow();
if (getHostType().isBitwigStudio())
{
component->setTopLeftPosition (0, 0);
component->repaint();
}
}
return kResultTrue; return kResultTrue;
#else #else
@@ -1012,18 +1040,10 @@ private:
//============================================================================== //==============================================================================
struct ContentWrapperComponent : public Component struct ContentWrapperComponent : public Component
#if ! JUCE_MAC
, public ComponentPeer::ScaleFactorListener,
public ComponentMovementWatcher
#endif
{ {
ContentWrapperComponent (JuceVST3Editor& editor, AudioProcessor& plugin) ContentWrapperComponent (JuceVST3Editor& editor, AudioProcessor& plugin)
:
#if ! JUCE_MAC
ComponentMovementWatcher (this),
#endif
pluginEditor (plugin.createEditorIfNeeded()),
owner (editor)
: pluginEditor (plugin.createEditorIfNeeded()),
owner (editor)
{ {
setOpaque (true); setOpaque (true);
setBroughtToFrontOnMouseClick (true); setBroughtToFrontOnMouseClick (true);
@@ -1054,12 +1074,6 @@ private:
PopupMenu::dismissAllActiveMenus(); PopupMenu::dismissAllActiveMenus();
pluginEditor->processor.editorBeingDeleted (pluginEditor.get()); pluginEditor->processor.editorBeingDeleted (pluginEditor.get());
} }
#if ! JUCE_MAC
for (int i = 0; i < ComponentPeer::getNumPeers(); ++i)
if (auto* p = ComponentPeer::getPeer (i))
p->removeScaleFactorListener (this);
#endif
} }
void paint (Graphics& g) override void paint (Graphics& g) override
@@ -1091,10 +1105,27 @@ private:
} }
} }
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
void checkScaleFactorIsCorrect()
{
if (auto* peer = pluginEditor->getPeer())
{
auto peerScaleFactor = (float) peer->getPlatformScaleFactor();
if (! approximatelyEqual (peerScaleFactor, owner.editorScaleFactor))
owner.setContentScaleFactor (peerScaleFactor);
}
}
#endif
void resized() override void resized() override
{ {
if (pluginEditor != nullptr) if (pluginEditor != nullptr)
{ {
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
checkScaleFactorIsCorrect();
#endif
if (! isResizingParentToFitChild) if (! isResizingParentToFitChild)
{ {
lastBounds = getLocalBounds(); lastBounds = getLocalBounds();
@@ -1140,10 +1171,17 @@ private:
if (owner.plugFrame != nullptr) if (owner.plugFrame != nullptr)
{ {
ViewRect newSize (0, 0, roundToInt (w * nativeScaleFactor), roundToInt (h * nativeScaleFactor));
isResizingParentToFitChild = true;
owner.plugFrame->resizeView (&owner, &newSize);
isResizingParentToFitChild = false;
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
w = roundToInt (w * owner.editorScaleFactor);
h = roundToInt (h * owner.editorScaleFactor);
#endif
ViewRect newSize (0, 0, w, h);
{
const ScopedValueSetter<bool> resizingParentSetter (isResizingParentToFitChild, true);
owner.plugFrame->resizeView (&owner, &newSize);
}
#if JUCE_MAC #if JUCE_MAC
if (host.isWavelab() || host.isReaper()) if (host.isWavelab() || host.isReaper())
@@ -1155,49 +1193,6 @@ private:
} }
} }
float getNativeEditorScaleFactor() const noexcept { return nativeScaleFactor; }
#if ! JUCE_MAC
void componentMovedOrResized (bool, bool) override {}
void componentPeerChanged() override
{
if (auto* peer = getTopLevelComponent()->getPeer())
peer->addScaleFactorListener (this);
}
void componentVisibilityChanged() override
{
if (auto* peer = getTopLevelComponent()->getPeer())
nativeScaleFactor = (float) peer->getPlatformScaleFactor();
}
void nativeScaleFactorChanged (double newScaleFactor) override
{
nativeScaleFactor = (float) newScaleFactor;
auto host = getHostType();
if (host.isWavelab())
{
Timer::callAfterDelay (250, [this] {
if (auto* peer = getPeer())
{
peer->updateBounds();
repaint();
}
resizeHostWindow();
});
}
else if (host.isBitwigStudio())
{
resizeHostWindow();
setTopLeftPosition (0, 0);
}
}
#endif
std::unique_ptr<AudioProcessorEditor> pluginEditor; std::unique_ptr<AudioProcessorEditor> pluginEditor;
private: private:
@@ -1207,8 +1202,6 @@ private:
bool isResizingChildToFitParent = false; bool isResizingChildToFitParent = false;
bool isResizingParentToFitChild = false; bool isResizingParentToFitChild = false;
float nativeScaleFactor = 1.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContentWrapperComponent) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContentWrapperComponent)
}; };
@@ -1222,10 +1215,10 @@ private:
#if JUCE_MAC #if JUCE_MAC
void* macHostWindow = nullptr; void* macHostWindow = nullptr;
bool isNSView = false; bool isNSView = false;
#else
float scaleFactor = 1.0f;
#endif #endif
float editorScaleFactor = 1.0f;
#if JUCE_WINDOWS #if JUCE_WINDOWS
WindowsHooks hooks; WindowsHooks hooks;
#endif #endif


+ 4
- 9
modules/juce_audio_processors/format_types/juce_VSTPluginFormat.cpp View File

@@ -3079,6 +3079,10 @@ private:
pluginRespondsToDPIChanges = plugin.pluginCanDo ("supportsViewDpiScaling") > 0; pluginRespondsToDPIChanges = plugin.pluginCanDo ("supportsViewDpiScaling") > 0;
if (pluginRespondsToDPIChanges)
if (auto* peer = getTopLevelComponent()->getPeer())
setScaleFactorAndDispatchMessage (peer->getPlatformScaleFactor());
#if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
std::unique_ptr<ScopedDPIAwarenessDisabler> dpiDisabler; std::unique_ptr<ScopedDPIAwarenessDisabler> dpiDisabler;
@@ -3108,9 +3112,6 @@ private:
// Install keyboard hooks // Install keyboard hooks
pluginWantsKeys = (dispatch (Vst2::effKeysRequired, 0, 0, 0, 0) == 0); pluginWantsKeys = (dispatch (Vst2::effKeysRequired, 0, 0, 0, 0) == 0);
if (auto* peer = getTopLevelComponent()->getPeer())
setScaleFactorAndDispatchMessage (peer->getPlatformScaleFactor());
#if JUCE_WINDOWS #if JUCE_WINDOWS
originalWndProc = 0; originalWndProc = 0;
pluginHWND = GetWindow ((HWND) getWindowHandle(), GW_CHILD); pluginHWND = GetWindow ((HWND) getWindowHandle(), GW_CHILD);
@@ -3147,12 +3148,6 @@ private:
auto rw = rect->right - rect->left; auto rw = rect->right - rect->left;
auto rh = rect->bottom - rect->top; auto rh = rect->bottom - rect->top;
if (pluginRespondsToDPIChanges)
{
rw = roundToInt (rw * nativeScaleFactor);
rh = roundToInt (rh * nativeScaleFactor);
}
if ((rw > 50 && rh > 50 && rw < 2000 && rh < 2000 && (! isWithin (w, rw, 2) || ! isWithin (h, rh, 2))) if ((rw > 50 && rh > 50 && rw < 2000 && rh < 2000 && (! isWithin (w, rw, 2) || ! isWithin (h, rh, 2)))
|| ((w == 0 && rw > 0) || (h == 0 && rh > 0))) || ((w == 0 && rw > 0) || (h == 0 && rh > 0)))
{ {


Loading…
Cancel
Save