// This file is part of VSTGUI. It is subject to the license terms // in the LICENSE file found in the top-level directory of this // distribution and at http://github.com/steinbergmedia/vstgui/LICENSE #include "vst3editor.h" #include "../vstgui.h" #include "../lib/cvstguitimer.h" #include "../lib/vstkeycode.h" #include "../lib/animation/timingfunctions.h" #include "../lib/animation/animations.h" #include "../lib/platform/platformfactory.h" #include "../uidescription/detail/uiviewcreatorattributes.h" #include "../uidescription/editing/uieditcontroller.h" #include "../uidescription/editing/uieditmenucontroller.h" #include "../uidescription/uiattributes.h" #include "../uidescription/uiviewfactory.h" #include "../uidescription/cstream.h" #include "base/source/fstring.h" #include "base/source/updatehandler.h" #include "pluginterfaces/base/keycodes.h" #include #include #include #include #if LINUX #include "../lib/platform/linux/x11frame.h" #include "../lib/platform/linux/waylandframe.h" #include "pluginterfaces/gui/iplugview.h" #include "pluginterfaces/gui/iwaylandframe.h" DEF_CLASS_IID (Steinberg::IWaylandFrame); DEF_CLASS_IID (Steinberg::IWaylandHost); #endif #if defined(kVstVersionMajor) && defined(kVstVersionMinor) #define VST3_SUPPORTS_CONTEXTMENU \ (kVstVersionMajor > 3 || (kVstVersionMajor == 3 && kVstVersionMinor > 1)) #if VST3_SUPPORTS_CONTEXTMENU #include "pluginterfaces/vst/ivstcontextmenu.h" #endif #else #define VST3_SUPPORTS_CONTEXTMENU 0 #endif /// @cond ignore namespace Steinberg { //----------------------------------------------------------------------------- class UpdateHandlerInit { public: UpdateHandlerInit () { get (); } UpdateHandler* get () { return UpdateHandler::instance (); } }; static UpdateHandlerInit gUpdateHandlerInit; //----------------------------------------------------------------------------- class IdleUpdateHandler { public: static void start () { auto& instance = get (); if (++instance.users == 1) { instance.timer = VSTGUI::makeOwned ( [] (VSTGUI::CVSTGUITimer*) { gUpdateHandlerInit.get ()->triggerDeferedUpdates (); }, 1000 / 30); } } static void stop () { auto& instance = get (); if (--instance.users == 0) { instance.timer = nullptr; } } protected: static IdleUpdateHandler& get () { static IdleUpdateHandler gInstance; return gInstance; } VSTGUI::SharedPointer timer; std::atomic users {0}; }; } // namespace Steinberg /// @endcond ignore namespace VSTGUI { //----------------------------------------------------------------------------- class ParameterChangeListener : public Steinberg::FObject { public: ParameterChangeListener (Steinberg::Vst::EditController* editController, Steinberg::Vst::Parameter* parameter, CControl* control) : editController (editController) , parameter (parameter) { if (parameter) { parameter->addRef (); parameter->addDependent (this); } addControl (control); if (parameter) parameter->changed (); } ~ParameterChangeListener () override { if (parameter) { parameter->removeDependent (this); parameter->release (); } for (const auto& c : controls) c->forget (); } void addControl (CControl* control) { if (containsControl (control)) return; control->remember (); controls.push_back (control); Steinberg::Vst::ParamValue value = 0.; if (parameter) { value = editController->getParamNormalized (getParameterID ()); } else { if (auto ctrl = controls.front ()) value = ctrl->getValueNormalized (); } auto* display = dynamic_cast (control); if (display) display->setValueToStringFunction([this](float value, char utf8String[256], CParamDisplay* display) { return convertValueToString (value, utf8String); }); if (parameter) parameter->deferUpdate (); else updateControlValue (value); } void removeControl (CControl* control) { for (const auto& c : controls) { if (c == control) { controls.remove (control); control->forget (); return; } } } bool containsControl (CControl* control) { return std::find (controls.begin (), controls.end (), control) != controls.end (); } void PLUGIN_API update (FUnknown* changedUnknown, Steinberg::int32 message) override { if (message == IDependent::kChanged && parameter) { updateControlValue (editController->getParamNormalized (getParameterID ())); } } Steinberg::Vst::ParamID getParameterID () { if (parameter) return parameter->getInfo ().id; CControl* control = controls.front (); if (control) return static_cast (control->getTag ()); return 0xFFFFFFFF; } void beginEdit () { if (parameter) editController->beginEdit (getParameterID ()); } void endEdit () { if (parameter) editController->endEdit (getParameterID ()); // fix textual representation. // It can happen that a parameter edit does not change the normalized value but the textual // representation shows a wrong text because the text is only translated when the // normalized value changes. Steinberg::Vst::String128 str {}; for (const auto& c : controls) { if (auto label = dynamic_cast (c)) { if (str[0] == 0) { editController->getParamStringByValue ( getParameterID (), editController->getParamNormalized (getParameterID ()), str); } Steinberg::String s (str); s.toMultiByte (Steinberg::kCP_Utf8); if (label->getText () != s.text8 ()) label->setText (s.text8 ()); } } } void performEdit (Steinberg::Vst::ParamValue value) { if (parameter) { auto id = getParameterID (); if (editController->setParamNormalized (id, value) == Steinberg::kResultTrue) editController->performEdit (id, editController->getParamNormalized (id)); } else { updateControlValue (value); } } Steinberg::Vst::Parameter* getParameter () const { return parameter; } protected: bool convertValueToString (float value, char utf8String[256]) { if (parameter) { Steinberg::Vst::String128 utf16Str; if (parameter && parameter->getInfo ().stepCount) { // convert back to normalized value value = (float)editController->plainParamToNormalized (getParameterID (), (Steinberg::Vst::ParamValue)value); } editController->getParamStringByValue (getParameterID (), value, utf16Str); Steinberg::String utf8Str (utf16Str); utf8Str.toMultiByte (Steinberg::kCP_Utf8); utf8Str.copyTo8 (utf8String, 0, 256); return true; } return false; } void updateControlValue (Steinberg::Vst::ParamValue value) { bool mouseEnabled = true; bool isStepCount = false; Steinberg::Vst::ParamValue defaultValue = 0.5; float minValue = 0.f; float maxValue = 1.f; if (parameter) { defaultValue = parameter->getInfo ().defaultNormalizedValue; if (parameter->getInfo ().flags & Steinberg::Vst::ParameterInfo::kIsReadOnly) mouseEnabled = false; if (parameter->getInfo ().stepCount) { isStepCount = true; value = parameter->toPlain (value); defaultValue = parameter->toPlain (defaultValue); minValue = (float)parameter->toPlain ((Steinberg::Vst::ParamValue)minValue); maxValue = (float)parameter->toPlain ((Steinberg::Vst::ParamValue)maxValue); } } for (const auto& c : controls) { c->setMouseEnabled (mouseEnabled); if (parameter) { c->setDefaultValue ((float)defaultValue); c->setMin (minValue); c->setMax (maxValue); } auto* label = dynamic_cast(c); if (label) { Steinberg::Vst::ParamValue normValue = value; if (isStepCount) { normValue = parameter->toNormalized (value); } Steinberg::Vst::String128 utf16Str; if (editController->getParamStringByValue (getParameterID (), normValue, utf16Str) != Steinberg::kResultTrue) continue; Steinberg::String utf8Str (utf16Str); utf8Str.toMultiByte (Steinberg::kCP_Utf8); label->setText (utf8Str.text8 ()); } else { if (isStepCount) { c->setMin (minValue); c->setMax (maxValue); auto getParamStringByIndex = [&] (int32_t i) { Steinberg::Vst::String128 utf16Str; editController->getParamStringByValue ( getParameterID (), (Steinberg::Vst::ParamValue)i / (Steinberg::Vst::ParamValue)parameter->getInfo ().stepCount, utf16Str); Steinberg::String utf8Str (utf16Str); utf8Str.toMultiByte (Steinberg::kCP_Utf8); return utf8Str; }; if (auto optMenu = dynamic_cast (c)) { optMenu->removeAllEntry (); for (Steinberg::int32 i = 0; i <= parameter->getInfo ().stepCount; i++) optMenu->addEntry (getParamStringByIndex (i).text8 ()); c->setValue ((float)value - minValue); } else if (auto segmentButton = dynamic_cast (c)) { segmentButton->removeAllSegments (); for (Steinberg::int32 i = 0; i <= parameter->getInfo ().stepCount; i++) { CSegmentButton::Segment segment; segment.name = getParamStringByIndex (i).text8 (); segmentButton->addSegment (std::move (segment)); } c->setValue ((float)value); } else { c->setValue ((float)value); } } else c->setValueNormalized ((float)value); c->valueChanged (); } c->invalid (); } } Steinberg::Vst::EditController* editController; Steinberg::Vst::Parameter* parameter; using ControlList = std::list; ControlList controls; }; namespace VST3EditorInternal { //----------------------------------------------------------------------------- static bool parseSize (const std::string& str, CPoint& point) { size_t sep = str.find (',', 0); if (sep != std::string::npos) { point.x = strtol (str.c_str (), nullptr, 10); point.y = strtol (str.c_str () + sep+1, nullptr, 10); return true; } return false; } //----------------------------------------------------------------------------- static void releaseSubController (IController* subController) { if (auto ref = dynamic_cast (subController)) ref->forget(); else if (auto fobj = dynamic_cast (subController)) fobj->release (); else delete subController; } } // namespace VST3EditorInternal //----------------------------------------------------------------------------- /*! @class VST3Editor The VST3Editor class represents the view for a VST3 plug-in. It automatically binds the VST3 parameters to VSTGUI control tags and it includes an inline UI editor for rapid development. @section setup Setup Add the following code to your Steinberg::Vst::EditController class: @code IPlugView* PLUGIN_API MyEditController::createView (FIDString name) { if (strcmp (name, ViewType::kEditor) == 0) { return new VST3Editor (this, "view", "myEditor.uidesc"); } return 0; } @endcode To activate the inline editor you need to define the preprocessor definition "VSTGUI_LIVE_EDITING=1". Rebuild your plug-in, start your prefered host, instanciate your plug-in, open the context menu inside your editor and choose "Enable Editing". Now you can define tags, colors, fonts, bitmaps and add views to your editor. See @ref page_uidescription_editor @n */ //----------------------------------------------------------------------------- VST3Editor::VST3Editor (Steinberg::Vst::EditController* controller, UTF8StringPtr _viewName, UTF8StringPtr _xmlFile) : VSTGUIEditor (controller), delegate (dynamic_cast (controller)) { description = new UIDescription (_xmlFile); viewName = _viewName; xmlFile = _xmlFile; init (); } //----------------------------------------------------------------------------- VST3Editor::VST3Editor (UIDescription* desc, Steinberg::Vst::EditController* controller, UTF8StringPtr _viewName, UTF8StringPtr _xmlFile) : VSTGUIEditor (controller), delegate (dynamic_cast (controller)) { description = desc; description->remember (); viewName = _viewName; if (_xmlFile) xmlFile = _xmlFile; init (); } //----------------------------------------------------------------------------- VST3Editor::~VST3Editor () { description->forget (); } //----------------------------------------------------------------------------- Steinberg::tresult PLUGIN_API VST3Editor::queryInterface (const Steinberg::TUID iid, void** obj) { #ifdef VST3_CONTENT_SCALE_SUPPORT QUERY_INTERFACE(iid, obj, Steinberg::IPlugViewContentScaleSupport::iid, Steinberg::IPlugViewContentScaleSupport) #endif QUERY_INTERFACE(iid, obj, Steinberg::Vst::IParameterFinder::iid, Steinberg::Vst::IParameterFinder) return VSTGUIEditor::queryInterface (iid, obj); } //----------------------------------------------------------------------------- void VST3Editor::init () { // we will always call CView::setDirty() on the main thread VSTGUI::CView::kDirtyCallAlwaysOnMainThread = true; setIdleRate (300); if (description->parse ()) { // get sizes const auto* attr = description->getViewAttributes (viewName.c_str ()); if (attr) { const std::string* sizeStr = attr->getAttributeValue ("size"); const std::string* minSizeStr = attr->getAttributeValue ("minSize"); const std::string* maxSizeStr = attr->getAttributeValue ("maxSize"); if (sizeStr) { CPoint p; if (VST3EditorInternal::parseSize (*sizeStr, p)) { rect.right = (Steinberg::int32)p.x; rect.bottom = (Steinberg::int32)p.y; minSize = p; maxSize = p; } } if (minSizeStr) VST3EditorInternal::parseSize (*minSizeStr, minSize); if (maxSizeStr) VST3EditorInternal::parseSize (*maxSizeStr, maxSize); } #if DEBUG else { auto* debugAttr = new UIAttributes (); debugAttr->setAttribute (UIViewCreator::kAttrClass, "CViewContainer"); debugAttr->setAttribute ("size", "300, 300"); description->addNewTemplate (viewName.c_str (), debugAttr); rect.right = 300; rect.bottom = 300; minSize (rect.right, rect.bottom); maxSize (rect.right, rect.bottom); } #endif } #if DEBUG else { auto* attr = new UIAttributes (); attr->setAttribute (UIViewCreator::kAttrClass, "CViewContainer"); attr->setAttribute ("size", "300, 300"); description->addNewTemplate (viewName.c_str (), attr); rect.right = 300; rect.bottom = 300; minSize (rect.right, rect.bottom); maxSize (rect.right, rect.bottom); } #endif } //----------------------------------------------------------------------------- bool VST3Editor::exchangeView (UTF8StringPtr newViewName) { if (viewName == newViewName) return true; const UIAttributes* attr = description->getViewAttributes (newViewName); if (attr) { viewName = newViewName; auto minSizeStr = attr->getAttributeValue ("minSize"); auto maxSizeStr = attr->getAttributeValue ("maxSize"); if (minSizeStr) VST3EditorInternal::parseSize (*minSizeStr, minSize); if (maxSizeStr) VST3EditorInternal::parseSize (*maxSizeStr, maxSize); requestRecreateView (); return true; } return false; } //----------------------------------------------------------------------------- void VST3Editor::enableTooltips (bool state) { tooltipsEnabled = state; if (getFrame ()) getFrame ()->enableTooltips (state); } //----------------------------------------------------------------------------- bool VST3Editor::setEditorSizeConstrains (const CPoint& newMinimumSize, const CPoint& newMaximumSize) { if (newMinimumSize.x <= newMaximumSize.x && newMinimumSize.y <= newMaximumSize.y) { minSize = newMinimumSize; maxSize = newMaximumSize; if (frame) { CRect currentSize, newSize; getFrame ()->getSize (currentSize); newSize = currentSize; CCoord width = currentSize.getWidth (); CCoord height = currentSize.getHeight (); double scaleFactor = getAbsScaleFactor (); if (width > maxSize.x * scaleFactor) newSize.setWidth (maxSize.x * scaleFactor); else if (width < minSize.x * scaleFactor) newSize.setWidth (minSize.x * scaleFactor); if (height > maxSize.y * scaleFactor) newSize.setHeight (maxSize.y * scaleFactor); else if (height < minSize.y * scaleFactor) newSize.setHeight (minSize.y * scaleFactor); if (newSize != currentSize) requestResize (CPoint (newSize.getWidth (), newSize.getHeight ())); } return true; } return false; } //----------------------------------------------------------------------------- double VST3Editor::getAbsScaleFactor () const { return getZoomFactor () * getContentScaleFactor (); } //----------------------------------------------------------------------------- double VST3Editor::getContentScaleFactor () const { return contentScaleFactor; } //----------------------------------------------------------------------------- double VST3Editor::getZoomFactor () const { return zoomFactor; } #ifdef VST3_CONTENT_SCALE_SUPPORT //----------------------------------------------------------------------------- Steinberg::tresult PLUGIN_API VST3Editor::setContentScaleFactor (ScaleFactor factor) { contentScaleFactor = factor; if (getFrame ()) { getFrame ()->setZoom (getAbsScaleFactor ()); } return Steinberg::kResultOk; } #endif //----------------------------------------------------------------------------- void VST3Editor::setZoomFactor (double factor) { if (getZoomFactor () == factor) return; zoomFactor = factor; if (getFrame () == nullptr) return; getFrame ()->setZoom (getAbsScaleFactor ()); if (delegate) delegate->onZoomChanged (this, zoomFactor); } //----------------------------------------------------------------------------- bool VST3Editor::beforeSizeChange (const CRect& newSize, const CRect& oldSize) { if (sizeRequest) return true; sizeRequest = {newSize.getSize ()}; bool result = requestResize (*sizeRequest); sizeRequest = {}; return result; } //----------------------------------------------------------------------------- bool VST3Editor::requestResize (const CPoint& newSize) { if (!plugFrame) return false; Steinberg::ViewRect vr; vr.right = static_cast (std::floor (newSize.x)); vr.bottom = static_cast (std::floor (newSize.y)); return plugFrame->resizeView (this, &vr) == Steinberg::kResultTrue ? true : false; } //----------------------------------------------------------------------------- void VST3Editor::getEditorSizeConstrains (CPoint& minimumSize, CPoint& maximumSize) const { minimumSize = minSize; maximumSize = maxSize; } //----------------------------------------------------------------------------- ParameterChangeListener* VST3Editor::getParameterChangeListener (int32_t tag) const { if (tag != -1) { auto it = paramChangeListeners.find (tag); if (it != paramChangeListeners.end ()) { return it->second; } } return nullptr; } //----------------------------------------------------------------------------- void VST3Editor::valueChanged (CControl* pControl) { using namespace Steinberg; if (!pControl->isEditing ()) return; ParameterChangeListener* pcl = getParameterChangeListener (pControl->getTag ()); if (pcl) { auto paramID = pcl->getParameterID (); auto normalizedValue = static_cast (pControl->getValueNormalized ()); auto* textEdit = dynamic_cast (pControl); if (textEdit && pcl->getParameter ()) { Steinberg::String str (textEdit->getText ()); str.toWideString (kCP_Utf8); if (getController ()->getParamValueByString (paramID, const_cast (str.text16 ()), normalizedValue) != kResultTrue) { pcl->update (nullptr, kChanged); return; } } pcl->performEdit (normalizedValue); } } //----------------------------------------------------------------------------- void VST3Editor::beginEdit (int32_t index) { // we don't assume that every control tag is a parameter tag handled by this editor // as sub classes could build custom CControlListeners for controls } //----------------------------------------------------------------------------- void VST3Editor::endEdit (int32_t index) { // see above } //----------------------------------------------------------------------------- void VST3Editor::controlBeginEdit (CControl* pControl) { ParameterChangeListener* pcl = getParameterChangeListener (pControl->getTag ()); if (pcl) { pcl->beginEdit (); } } //----------------------------------------------------------------------------- void VST3Editor::controlEndEdit (CControl* pControl) { ParameterChangeListener* pcl = getParameterChangeListener (pControl->getTag ()); if (pcl) { pcl->endEdit (); } } //----------------------------------------------------------------------------- void VST3Editor::controlTagWillChange (CControl* pControl) { if (pControl->getTag () != -1 && pControl->getListener () == this) { ParameterChangeListener* pcl = getParameterChangeListener (pControl->getTag ()); if (pcl) { pcl->removeControl (pControl); } } } //----------------------------------------------------------------------------- void VST3Editor::controlTagDidChange (CControl* pControl) { if (pControl->getTag () != -1 && pControl->getListener () == this) { ParameterChangeListener* pcl = getParameterChangeListener (pControl->getTag ()); if (pcl) { pcl->addControl (pControl); } else { Steinberg::Vst::EditController* editController = getController (); if (editController) { Steinberg::Vst::Parameter* parameter = editController->getParameterObject (static_cast (pControl->getTag ())); paramChangeListeners.insert (std::make_pair (pControl->getTag (), new ParameterChangeListener (editController, parameter, pControl))); } } } } //----------------------------------------------------------------------------- void VST3Editor::onViewAdded (CFrame* frame, CView* view) { } //----------------------------------------------------------------------------- void VST3Editor::onViewRemoved (CFrame* frame, CView* view) { auto* control = dynamic_cast (view); if (control && control->getTag () != -1) { ParameterChangeListener* pcl = getParameterChangeListener (control->getTag ()); if (pcl) { pcl->removeControl (control); } } // TODO: Currently when in Edit Mode in UIEditor, subcontrollers will be released, even tho the view may be added again later on. IController* controller = getViewController (view); if (controller) { VST3EditorInternal::releaseSubController (controller); view->removeAttribute (kCViewControllerAttribute); } } #if VST3_SUPPORTS_CONTEXTMENU /// @cond ignore namespace VST3EditorInternal { //----------------------------------------------------------------------------- class ContextMenuTarget : public Steinberg::FObject, public Steinberg::Vst::IContextMenuTarget { public: ContextMenuTarget (CCommandMenuItem* item) : item (item) { item->remember (); } ~ContextMenuTarget () override { item->forget (); } Steinberg::tresult PLUGIN_API executeMenuItem (Steinberg::int32 tag) override { item->execute (); return Steinberg::kResultTrue; } OBJ_METHODS(ContextMenuTarget, Steinberg::FObject) FUNKNOWN_METHODS(Steinberg::Vst::IContextMenuTarget, Steinberg::FObject) protected: CCommandMenuItem* item; }; //----------------------------------------------------------------------------- static void addCOptionMenuEntriesToIContextMenu (VST3Editor* editor, COptionMenu* menu, Steinberg::Vst::IContextMenu* contextMenu) { for (CConstMenuItemIterator it = menu->getItems ()->begin (), end = menu->getItems ()->end (); it != end; ++it) { auto* commandItem = (*it).cast(); if (commandItem) commandItem->validate (); Steinberg::Vst::IContextMenu::Item item = {}; Steinberg::String title ((*it)->getTitle ()); title.toWideString (Steinberg::kCP_Utf8); title.copyTo16 (item.name, 0, 128); if ((*it)->getSubmenu ()) { item.flags = Steinberg::Vst::IContextMenu::Item::kIsGroupStart; contextMenu->addItem (item, nullptr); addCOptionMenuEntriesToIContextMenu (editor, (*it)->getSubmenu (), contextMenu); item.flags = Steinberg::Vst::IContextMenu::Item::kIsGroupEnd; contextMenu->addItem (item, nullptr); } else if ((*it)->isSeparator ()) { item.flags = Steinberg::Vst::IContextMenu::Item::kIsSeparator; contextMenu->addItem (item, nullptr); } else { if (commandItem) { if ((*it)->isChecked ()) item.flags |= Steinberg::Vst::IContextMenu::Item::kIsChecked; if ((*it)->isEnabled () == false) item.flags |= Steinberg::Vst::IContextMenu::Item::kIsDisabled; auto* target = new ContextMenuTarget (commandItem); contextMenu->addItem (item, target); target->release (); } } } } } // namespace /// @endcond ignore #endif //----------------------------------------------------------------------------- void VST3Editor::onMouseEvent (MouseEvent& event, CFrame* frame) { if (event.type != EventType::MouseDown) return; if (event.buttonState.isRight ()) { COptionMenu* controllerMenu = (delegate && editingEnabled == false) ? delegate->createContextMenu (event.mousePosition, this) : nullptr; if (allowedZoomFactors.empty () == false && editingEnabled == false) { if (controllerMenu == nullptr) controllerMenu = new COptionMenu (); else controllerMenu->addSeparator (); auto* zoomMenu = new COptionMenu (); zoomMenu->setStyle (COptionMenu::kMultipleCheckStyle); char zoomFactorString[128]; int32_t zoomFactorTag = 0; for (std::vector::const_iterator it = allowedZoomFactors.begin (), end = allowedZoomFactors.end (); it != end; ++it, ++zoomFactorTag) { snprintf (zoomFactorString, std::size (zoomFactorString), "%d%%", static_cast ((*it) * 100)); CMenuItem* item = zoomMenu->addEntry (new CCommandMenuItem ( {zoomFactorString, zoomFactorTag, this, "Zoom", zoomFactorString})); if (getZoomFactor () == *it) item->setChecked (true); } CMenuItem* item = controllerMenu->addEntry ("UI Zoom"); item->setSubmenu (zoomMenu); } #if VSTGUI_LIVE_EDITING if (editingEnabled == false) { if (controllerMenu == nullptr) controllerMenu = new COptionMenu (); else controllerMenu->addSeparator (); CMenuItem* item = controllerMenu->addEntry (new CCommandMenuItem ( {"Open UIDescription Editor", this, "File", "Open UIDescription Editor"})); item->setKey ("e", kControl); item = controllerMenu->addEntry (new CCommandMenuItem ( {"Show 'Open UI Editor' Button", this, "File", "Show Editor Button"})); if (enableShowEditButton ()) item->setChecked (); item = controllerMenu->addEntry (new CCommandMenuItem ( {"Save Editor Screenshot", this, "File", "Save Editor Screenshot"})); } #endif CViewContainer::ViewList views; auto nonScaledPos = event.mousePosition; frame->getTransform ().transform (nonScaledPos); if (getFrame ()->getViewsAt (nonScaledPos, views, GetViewOptions ().deep ().includeViewContainer ())) { auto createOrPrepareMenu = [&] () { if (controllerMenu == nullptr) controllerMenu = new COptionMenu (); else controllerMenu->addSeparator (); }; for (const auto& view : views) { auto viewController = getViewController (view); if (!viewController) continue; if (auto ctrler = dynamic_cast (viewController)) { createOrPrepareMenu (); ctrler->appendContextMenuItems (*controllerMenu, view, view->translateToLocal (nonScaledPos)); } else if (auto contextMenuController = dynamic_cast (viewController)) { createOrPrepareMenu (); contextMenuController->appendContextMenuItems (*controllerMenu, view->translateToLocal (nonScaledPos)); } } } #if VST3_SUPPORTS_CONTEXTMENU Steinberg::FUnknownPtr handler (getController ()->getComponentHandler ()); Steinberg::Vst::ParamID paramID; if (handler) { CPoint where2 (event.mousePosition); getFrame ()->getTransform ().transform (where2); bool paramFound = findParameter ((Steinberg::int32)where2.x, (Steinberg::int32)where2.y, paramID) == Steinberg::kResultTrue; Steinberg::Vst::IContextMenu* contextMenu = handler->createContextMenu (this, paramFound ? ¶mID : nullptr); if (contextMenu) { if (controllerMenu) VST3EditorInternal::addCOptionMenuEntriesToIContextMenu (this, controllerMenu, contextMenu); getFrame ()->doAfterEventProcessing ([=] () { contextMenu->popup (static_cast (where2.x), static_cast (where2.y)); contextMenu->release (); }); event.consumed = true; } } if (!event.consumed) { #endif if (controllerMenu && controllerMenu->getNbEntries () > 0) { controllerMenu->remember (); SharedPointer blockFrame = getFrame (); getFrame ()->doAfterEventProcessing ([=, mousePosition = event.mousePosition] () { controllerMenu->setStyle (COptionMenu::kPopupStyle | COptionMenu::kMultipleCheckStyle); controllerMenu->popup (blockFrame, mousePosition); controllerMenu->forget (); }); event.consumed = true; } } if (controllerMenu) controllerMenu->forget (); } } //----------------------------------------------------------------------------- Steinberg::tresult PLUGIN_API VST3Editor::findParameter (Steinberg::int32 xPos, Steinberg::int32 yPos, Steinberg::Vst::ParamID& resultTag) { std::list > views; if (frame && getFrame ()->getViewsAt (CPoint (xPos, yPos), views)) { CControl* control = nullptr; std::list >::const_iterator it = views.begin (); while (it != views.end ()) { control = (*it).cast(); if (control) { if (control->getMouseEnabled () && control->getTag () != -1) break; control = nullptr; if ((*it)->getTransparency () == false) break; } it++; } if (control) { ParameterChangeListener* pcl = getParameterChangeListener (control->getTag ()); if (pcl && pcl->containsControl (control) && pcl->getParameter ()) { if (delegate && delegate->isPrivateParameter (pcl->getParameterID ())) return Steinberg::kResultFalse; resultTag = pcl->getParameterID (); return Steinberg::kResultTrue; } } Steinberg::Vst::ParamID pid; if (delegate && delegate->findParameter (CPoint (xPos, yPos), pid, this) && !delegate->isPrivateParameter (pid)) { resultTag = pid; return Steinberg::kResultTrue; } } return Steinberg::kResultFalse; } //----------------------------------------------------------------------------- IController* VST3Editor::createSubController (UTF8StringPtr name, const IUIDescription* desc) { return delegate ? delegate->createSubController (name, desc, this) : nullptr; } //----------------------------------------------------------------------------- CView* VST3Editor::createView (const UIAttributes& attrs, const IUIDescription* desc) { if (delegate) { const std::string* customViewName = attrs.getAttributeValue (IUIDescription::kCustomViewName); if (customViewName) { CView* view = delegate->createCustomView (customViewName->c_str (), attrs, desc, this); return view; } } return nullptr; } //----------------------------------------------------------------------------- CView* VST3Editor::verifyView (CView* view, const UIAttributes& attributes, const IUIDescription* desc) { if (delegate) view = delegate->verifyView (view, attributes, desc, this); auto* control = dynamic_cast (view); if (control && control->getTag () != -1 && control->getListener () == this) { ParameterChangeListener* pcl = getParameterChangeListener (control->getTag ()); if (pcl) { pcl->addControl (control); } else { Steinberg::Vst::EditController* editController = getController (); if (editController) { Steinberg::Vst::Parameter* parameter = editController->getParameterObject (static_cast (control->getTag ())); paramChangeListeners.insert (std::make_pair (control->getTag (), new ParameterChangeListener (editController, parameter, control))); } } } return view; } //----------------------------------------------------------------------------- void VST3Editor::recreateView () { doCreateView = false; enableEditing (editingEnabled); } //------------------------------------------------------------------------ void VST3Editor::requestRecreateView () { if (doCreateView || !frame) return; doCreateView = true; auto task = [Self = Steinberg::IPtr (this)] () { if (Self->frame) Self->recreateView (); }; if (frame->inEventProcessing ()) { frame->doAfterEventProcessing (std::move (task)); } else { task (); } } //----------------------------------------------------------------------------- bool VST3Editor::inEditMode () const { return editingEnabled; } #if LINUX // Map Steinberg Vst Interface to VSTGUI Interface class RunLoop : public IRunLoop, public AtomicReferenceCounted { public: struct EventHandler : Steinberg::Linux::IEventHandler, public Steinberg::FObject { VSTGUI::IEventHandler* handler {nullptr}; void PLUGIN_API onFDIsSet (Steinberg::Linux::FileDescriptor) override { if (handler) handler->onEvent (); } DELEGATE_REFCOUNT (Steinberg::FObject) DEFINE_INTERFACES DEF_INTERFACE (Steinberg::Linux::IEventHandler) END_DEFINE_INTERFACES (Steinberg::FObject) }; struct TimerHandler : Steinberg::Linux::ITimerHandler, public Steinberg::FObject { VSTGUI::ITimerHandler* handler {nullptr}; void PLUGIN_API onTimer () final { if (handler) handler->onTimer (); } DELEGATE_REFCOUNT (Steinberg::FObject) DEFINE_INTERFACES DEF_INTERFACE (Steinberg::Linux::ITimerHandler) END_DEFINE_INTERFACES (Steinberg::FObject) }; bool registerEventHandler (int fd, VSTGUI::IEventHandler* handler) final { if(!runLoop) return false; auto smtgHandler = Steinberg::owned (new EventHandler ()); smtgHandler->handler = handler; if (runLoop->registerEventHandler (smtgHandler, fd) == Steinberg::kResultTrue) { eventHandlers.push_back (smtgHandler); return true; } return false; } bool unregisterEventHandler (VSTGUI::IEventHandler* handler) final { if(!runLoop) return false; for (auto it = eventHandlers.begin (), end = eventHandlers.end (); it != end; ++it) { if ((*it)->handler == handler) { runLoop->unregisterEventHandler ((*it)); eventHandlers.erase (it); return true; } } return false; } bool registerTimer (uint64_t interval, VSTGUI::ITimerHandler* handler) final { if(!runLoop) return false; auto smtgHandler = Steinberg::owned (new TimerHandler ()); smtgHandler->handler = handler; if (runLoop->registerTimer (smtgHandler, interval) == Steinberg::kResultTrue) { timerHandlers.push_back (smtgHandler); return true; } return false; } bool unregisterTimer (VSTGUI::ITimerHandler* handler) final { if(!runLoop) return false; for (auto it = timerHandlers.begin (), end = timerHandlers.end (); it != end; ++it) { if ((*it)->handler == handler) { runLoop->unregisterTimer ((*it)); timerHandlers.erase (it); return true; } } return false; } RunLoop (Steinberg::FUnknown* runLoop) : runLoop (runLoop) {} private: using EventHandlers = std::vector>; using TimerHandlers = std::vector>; EventHandlers eventHandlers; TimerHandlers timerHandlers; Steinberg::FUnknownPtr runLoop; }; //----------------------------------------------------------------------------- class WaylandHost : public Wayland::IWaylandHost, public AtomicReferenceCounted { public: virtual wl_display* openWaylandConnection () final { if (!waylandHost) return nullptr; return waylandHost->openWaylandConnection (); } virtual bool closeWaylandConnection (wl_display* display) final { if (!waylandHost) return false; return waylandHost->closeWaylandConnection (display) == Steinberg::kResultOk; } WaylandHost (Steinberg::FUnknown* waylandHost) : waylandHost (waylandHost) {} private: Steinberg::FUnknownPtr waylandHost; }; //----------------------------------------------------------------------------- class WaylandFrame : public Wayland::IWaylandFrame, public AtomicReferenceCounted { public: wl_surface* getWaylandSurface (wl_display* display) final { if (!waylandFrame) return nullptr; return waylandFrame->getWaylandSurface (display); } xdg_surface* getParentSurface (CRect& parentSize, wl_display* display) final { if (!waylandFrame) return nullptr; Steinberg::ViewRect viewRect; xdg_surface* surface = waylandFrame->getParentSurface (viewRect, display); parentSize = CRect (viewRect.left, viewRect.top, viewRect.right, viewRect.bottom); return surface; } xdg_toplevel* getParentToplevel (wl_display* display) final { if (!waylandFrame) return nullptr; return waylandFrame->getParentToplevel (display); } WaylandFrame (Steinberg::FUnknown* waylandFrame) : waylandFrame (waylandFrame) {} private: Steinberg::FUnknownPtr waylandFrame; }; #endif #define kFrameEnableFocusDrawingAttr "frame-enable-focus-drawing" #define kFrameFocusColorAttr "frame-focus-color" #define kFrameFocusWidthAttr "frame-focus-width" #if VSTGUI_LIVE_EDITING // keyboard hook struct VST3Editor::KeyboardHook : public IKeyboardHook { public: using Func = std::function; KeyboardHook (Func&& keyDown, Func&& keyUp) : onKeyDownFunc (std::move (keyDown)), onKeyUpFunc (std::move (keyUp)) { } private: void onKeyboardEvent (KeyboardEvent& event, CFrame* frame) override { if (event.type == EventType::KeyDown) { onKeyDownFunc (event, frame); } else if (event.type == EventType::KeyUp) { onKeyUpFunc (event, frame); } } Func onKeyDownFunc; Func onKeyUpFunc; }; #else struct VST3Editor::KeyboardHook {}; #endif //----------------------------------------------------------------------------- bool PLUGIN_API VST3Editor::open (void* parent, const PlatformType& type) { frame = new CFrame (CRect (0, 0, 0, 0), this); getFrame ()->setViewAddedRemovedObserver (this); getFrame ()->setTransparency (true); getFrame ()->registerMouseObserver (this); #if VSTGUI_LIVE_EDITING // will delete itself when the frame will be destroyed keyboardHook = new KeyboardHook ( [this] (KeyboardEvent& event, CFrame* frame) { if (event.modifiers.is (ModifierKey::Control) && frame->getModalView () == nullptr) { if (event.character == 'e') { editingEnabled = !editingEnabled; requestRecreateView (); event.consumed = true; } } }, [] (KeyboardEvent&, CFrame*) { }); getFrame ()->registerKeyboardHook (keyboardHook); #endif getFrame ()->enableTooltips (tooltipsEnabled); if (!enableEditing (false)) { getFrame ()->forget (); frame = nullptr; return false; } IPlatformFrameConfig* config = nullptr; #if LINUX if (type == PlatformType::kWaylandSurfaceID) { if (parent != nullptr) return false; Wayland::FrameConfig* waylandConfig = new Wayland::FrameConfig; waylandConfig->runLoop = owned (new RunLoop (plugFrame)); waylandConfig->waylandHost = owned (new WaylandHost (plugFrame)); waylandConfig->waylandFrame = owned (new WaylandFrame (plugFrame)); config = waylandConfig; parent = config; } else { X11::FrameConfig* x11config = new X11::FrameConfig; x11config->runLoop = owned (new RunLoop (plugFrame)); config = x11config; } #endif getFrame ()->open (parent, type, config); delete config; config = nullptr; if (delegate) delegate->didOpen (this); Steinberg::IdleUpdateHandler::start (); return true; } //----------------------------------------------------------------------------- void PLUGIN_API VST3Editor::close () { Steinberg::IdleUpdateHandler::stop (); if (delegate) delegate->willClose (this); for (ParameterChangeListenerMap::const_iterator it = paramChangeListeners.begin (), end = paramChangeListeners.end (); it != end; ++it) it->second->release (); paramChangeListeners.clear (); if (frame) { #if LINUX wl_display* display {}; Steinberg::FUnknownPtr host (controller->getHostContext ()); if (host) { if (!frame->getAttribute ('WlDi', display)) { display = nullptr; } } #endif // LINUX #if VSTGUI_LIVE_EDITING if (keyboardHook) { getFrame ()->unregisterKeyboardHook (keyboardHook); delete keyboardHook; } keyboardHook = nullptr; openUIEditorController = nullptr; #endif getFrame ()->unregisterMouseObserver (this); getFrame ()->removeAll (true); int32_t refCount = getFrame ()->getNbReference (); if (refCount == 1) { getFrame ()->close (); frame = nullptr; } else { getFrame ()->forget (); } #if LINUX if (host && display) host->closeWaylandConnection (display); #endif } } //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API VST3Editor::onSize (Steinberg::ViewRect* newSize) { if (sizeRequest) { auto width = static_cast (std::floor (sizeRequest->x)); auto height = static_cast (std::floor (sizeRequest->y)); if (width == newSize->getWidth () && height == newSize->getHeight ()) { VSTGUIEditor::onSize (newSize); return Steinberg::kResultTrue; } return Steinberg::kResultFalse; } if (getFrame ()) { CRect frameSize; getFrame ()->getSize (frameSize); auto width = static_cast (std::floor (frameSize.getWidth ())); auto height = static_cast (std::floor (frameSize.getHeight ())); if (frameSize.left == newSize->left && frameSize.top == newSize->top && width == newSize->getWidth () && height == newSize->getHeight ()) { VSTGUIEditor::onSize (newSize); return Steinberg::kResultTrue; } } sizeRequest = {CPoint (newSize->getWidth (), newSize->getHeight ())}; auto result = VSTGUIEditor::onSize (newSize); sizeRequest = {}; return result; } //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API VST3Editor::canResize () { // always return true as this can change dynamicaly return Steinberg::kResultTrue; } //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API VST3Editor::checkSizeConstraint (Steinberg::ViewRect* rect) { #if VSTGUI_LIVE_EDITING if (editingEnabled) return Steinberg::kResultTrue; #endif double scaleFactor = getAbsScaleFactor (); CCoord width = rect->right - rect->left; CCoord height = rect->bottom - rect->top; if (width < minSize.x * scaleFactor) width = minSize.x * scaleFactor; else if (width > maxSize.x * scaleFactor) width = maxSize.x * scaleFactor; if (height < minSize.y * scaleFactor) height = minSize.y * scaleFactor; else if (height > maxSize.y * scaleFactor) height = maxSize.y * scaleFactor; if (width != rect->getWidth () || height != rect->getHeight ()) { rect->right = static_cast (std::floor (width + rect->left)); rect->bottom = static_cast (std::floor (height + rect->top)); } return Steinberg::kResultTrue; } //------------------------------------------------------------------------ bool VST3Editor::validateCommandMenuItem (CCommandMenuItem* item) { #if VSTGUI_LIVE_EDITING if (item->getCommandCategory () == "File") { if (item->getCommandName () == "Save") { bool enable = false; UIAttributes* attributes = description->getCustomAttributes ("VST3Editor", true); if (attributes) { const std::string* filePath = attributes->getAttributeValue ("Path"); if (filePath) { enable = true; } } item->setEnabled (enable); return true; } } #endif return false; } //------------------------------------------------------------------------ bool VST3Editor::onCommandMenuItemSelected (CCommandMenuItem* item) { auto& cmdCategory = item->getCommandCategory (); #if VSTGUI_LIVE_EDITING auto& cmdName = item->getCommandName (); if (cmdCategory == "Edit") { if (cmdName == "Sync Parameter Tags") { syncParameterTags (); return true; } } else if (cmdCategory == "File") { if (cmdName == "Open UIDescription Editor") { editingEnabled = true; requestRecreateView (); return true; } else if (cmdName == "Close UIDescription Editor") { editingEnabled = false; requestRecreateView (); return true; } else if (cmdName == "Save") { save (false); item->setChecked (false); return true; } else if (cmdName == "Save As") { save (true); item->setChecked (false); return true; } else if (cmdName == "Save Editor Screenshot") { saveScreenshot (); return true; } else if (cmdName == "Show Editor Button") { auto state = enableShowEditButton (); enableShowEditButton (!state); if (!editingEnabled) showEditButton (!state); return true; } } else #endif if (cmdCategory == "Zoom") { size_t index = static_cast (item->getTag ()); if (index < allowedZoomFactors.size ()) { setZoomFactor (allowedZoomFactors[index]); } return true; } return false; } namespace VST3EditorInternal { //------------------------------------------------------------------------ static int32_t getUIDescriptionSaveOptions (CFrame* frame) { int32_t flags = 0; #if VSTGUI_LIVE_EDITING auto* editController = dynamic_cast (getViewController (frame->getView (0))); if (editController) { UIAttributes* attributes = editController->getSettings (); bool val; if (attributes->getBooleanAttribute (UIEditController::kEncodeBitmapsSettingsKey, val) && val == true) { #if ((VSTGUI_VERSION_MAJOR == 4 && VSTGUI_VERSION_MINOR > 9) || (VSTGUI_VERSION_MAJOR > 4)) flags |= UIDescription::kWriteImagesIntoUIDescFile; #else flags |= UIDescription::kWriteImagesIntoXMLFile; #endif } if (attributes->getBooleanAttribute (UIEditController::kWriteWindowsRCFileSettingsKey, val) && val == true) { flags |= UIDescription::kWriteWindowsResourceFile; } } #endif return flags; } } // namespace VST3EditorInternal //------------------------------------------------------------------------ void VST3Editor::save (bool saveAs) { UIAttributes* attributes = description->getCustomAttributes ("VST3Editor", true); vstgui_assert(attributes); if (!attributes) return; std::string savePath; if (saveAs) { CNewFileSelector* fileSelector = CNewFileSelector::create (frame, CNewFileSelector::kSelectSaveFile); if (fileSelector == nullptr) return; fileSelector->setTitle ("Save UIDescription File"); fileSelector->setDefaultExtension (CFileExtension ("VSTGUI UI Description", "uidesc")); const std::string* prevFilePath = attributes->getAttributeValue ("Path"); if (prevFilePath) fileSelector->setInitialDirectory (prevFilePath->c_str ()); else if (!xmlFile.empty ()) { if (xmlFile[0] == '/') fileSelector->setInitialDirectory (xmlFile.c_str ()); else fileSelector->setDefaultSaveName (xmlFile.c_str ()); } if (fileSelector->runModal ()) { UTF8StringPtr filePath = fileSelector->getSelectedFile (0); if (filePath) { attributes->setAttribute ("Path", filePath); savePath = filePath; } } fileSelector->forget (); } else { const std::string* filePath = attributes ? attributes->getAttributeValue ("Path") : nullptr; if (filePath) savePath = *filePath; } if (savePath.empty ()) return; // filter out attributes we will always override with the values from the parameters auto filter = [] (CView* view, const std::string& name) -> bool { if (auto control = dynamic_cast (view)) { if (control->getTag () != -1) { if (name == UIViewCreator::kAttrMinValue) return false; if (name == UIViewCreator::kAttrMaxValue) return false; if (name == UIViewCreator::kAttrDefaultValue) return false; if (name == UIViewCreator::kAttrMouseEnabled) return false; if (name == UIViewCreator::kAttrTitle && dynamic_cast (control)) return false; } } return true; }; if (description->save (savePath.c_str (), VST3EditorInternal::getUIDescriptionSaveOptions (frame), filter)) description->setFilePath (savePath.c_str ()); } //------------------------------------------------------------------------ void VST3Editor::syncParameterTags () { #if VSTGUI_LIVE_EDITING CView* view = getFrame ()->getView (0); if (view) { IController* controller = getViewController (view); IActionPerformer* actionPerformer = controller ? dynamic_cast(controller) : nullptr; if (actionPerformer) { Steinberg::Vst::EditController* editController = getController (); actionPerformer->beginGroupAction ("Sync Parameter Tags"); std::map units; Steinberg::FUnknownPtr ec2 (editController->unknownCast ()); if (ec2) { Steinberg::int32 unitCount = ec2->getUnitCount (); Steinberg::Vst::UnitInfo info; for (int32_t i = 0; i < unitCount; i++) { ec2->getUnitInfo (i, info); units.insert (std::pair (info.id, info)); } } int32_t paramCount = editController->getParameterCount (); for (int32_t i = 0; i < paramCount; i++) { Steinberg::Vst::ParameterInfo info; if (editController->getParameterInfo (i, info) == Steinberg::kResultTrue) { Steinberg::String paramTitle (info.title); if (info.unitId != Steinberg::Vst::kRootUnitId) { std::map::const_iterator it = units.find (info.unitId); if (it != units.end ()) { paramTitle.insertAt (0, "::"); paramTitle.insertAt (0, it->second.name); } } else if (!units.empty ()) { paramTitle.insertAt (0, "::"); paramTitle.insertAt (0, "Root"); } paramTitle.toMultiByte (Steinberg::kCP_Utf8); paramTitle.removeChars (' '); Steinberg::String paramIDStr; paramIDStr.printInt64 (info.id); if (int32_t tag = description->getTagForName (paramTitle) != -1) { if (tag != info.id) actionPerformer->performTagChange (paramTitle, paramIDStr); } else if (UTF8StringPtr tagName = description->lookupControlTagName (static_cast (info.id))) { actionPerformer->performTagNameChange (tagName, paramTitle); } else { actionPerformer->performTagChange (paramTitle, paramIDStr); } } } actionPerformer->finishGroupAction (); } } #endif } //------------------------------------------------------------------------ void VST3Editor::saveScreenshot () { if (auto fileSelector = owned (CNewFileSelector::create (getFrame (), CNewFileSelector::kSelectDirectory))) { fileSelector->setTitle ("Select Directory where to save the screenshots"); fileSelector->run ([this] (CNewFileSelector* fs) { if (fs->getNumSelectedFiles () != 1) return; auto makeScreenshot = [] (CFrame* frame) -> SharedPointer { auto size = frame->getViewSize ().getSize (); if (auto offscreen = COffscreenContext::create (size, 1.)) { offscreen->beginDraw (); frame->draw (offscreen); offscreen->endDraw (); return shared (offscreen->getBitmap ()); } return nullptr; }; showEditButton (false); auto origZoom = getFrame ()->getZoom (); getFrame ()->setZoom (1.); auto bitmap1 = makeScreenshot (getFrame ()); getFrame ()->setZoom (2.); auto bitmap2 = makeScreenshot (getFrame ()); getFrame ()->setZoom (origZoom); auto folderPath = std::string (fs->getSelectedFile (0)); auto uidStr = std::string ("XXXXXXXX"); if (bitmap1) { auto data = getPlatformFactory ().createBitmapMemoryPNGRepresentation ( bitmap1->getPlatformBitmap ()); if (!data.empty ()) { auto filename = folderPath + "/" + uidStr + "_snapshot.png"; CFileStream stream; if (stream.open (filename.data (), CFileStream::kWriteMode | CFileStream::kTruncateMode | CFileStream::kBinaryMode)) { stream.writeRaw (data.data (), static_cast (data.size ())); } } } if (bitmap2) { auto filename = folderPath + "/" + uidStr + "_snapshot_2.0x.png"; auto data = getPlatformFactory ().createBitmapMemoryPNGRepresentation ( bitmap2->getPlatformBitmap ()); if (!data.empty ()) { CFileStream stream; if (stream.open (filename.data (), CFileStream::kWriteMode | CFileStream::kTruncateMode | CFileStream::kBinaryMode)) { stream.writeRaw (data.data (), static_cast (data.size ())); } } } if (enableShowEditButton ()) showEditButton (true); }); } } //------------------------------------------------------------------------ bool VST3Editor::enableShowEditButton () const { bool addShowEditorButton = true; if (auto attributes = description->getCustomAttributes ("VST3Editor", true)) { attributes->getBooleanAttribute ("Show Editor Button", addShowEditorButton); } return addShowEditorButton; } //------------------------------------------------------------------------ void VST3Editor::enableShowEditButton (bool state) { if (auto attributes = description->getCustomAttributes ("VST3Editor", true)) attributes->setBooleanAttribute ("Show Editor Button", state); } #if VSTGUI_LIVE_EDITING //------------------------------------------------------------------------ class EnterEditModeController : public ViewListenerAdapter , public ViewEventListenerAdapter , public IControlListener { public: using EnterEditModeFunc = std::function; static constexpr const auto strFull = "Open UI Editor"; static constexpr const auto strMinimized = "e"; EnterEditModeController (CFrame* frame, EnterEditModeFunc&& func) : enterEditMode (std::move (func)) { button = new CTextButton ({0, 0, 120, 20}); button->setTitle (strFull); button->setRoundRadius (2.); button->setFrameWidth (-1); button->registerViewListener (this); button->registerViewEventListener (this); button->registerControlListener (this); frame->addView (button); } ~EnterEditModeController () noexcept override { if (button) { unregisterButtonListeners (); if (auto parent = button->getParentView ()) parent->asViewContainer ()->removeView (button); } } void unregisterButtonListeners () { button->unregisterViewEventListener (this); button->unregisterViewListener (this); button->unregisterControlListener (this); } void valueChanged (CControl* c) override { if (c->getValue () == 1.) { enterEditMode (); } } void viewAttached (CView* view) override { view->addAnimation ("SizeAnim", new Animation::AlphaValueAnimation (1.f), new Animation::LinearTimingFunction (1000), [&] (auto, auto, auto) { close (); }); } void viewWillDelete (CView* view) override { if (button == nullptr) return; unregisterButtonListeners (); button = nullptr; delete this; } void viewOnEvent (CView* view, Event& event) override { if (event.type == EventType::MouseEnter) { open (); } else if (event.type == EventType::MouseExit) { close (); } } Animation::ITimingFunction* createDefAnimTimingFunc () const { using namespace Animation; static const constexpr auto AnimationTime = 150; return new CubicBezierTimingFunction (CubicBezierTimingFunction::easyInOut (AnimationTime)); } void open () { button->addAnimation ("SizeAnim", new Animation::ViewSizeAnimation ({0, 0, 120, 20}), createDefAnimTimingFunc (), [&] (auto view, auto, auto) { button->setTitle (strFull); }); button->addAnimation ("AlphaValue", new Animation::AlphaValueAnimation (1.f), createDefAnimTimingFunc ()); } void close () { button->addAnimation ("SizeAnim", new Animation::ViewSizeAnimation ({0, 0, 10, 20}), createDefAnimTimingFunc (), [&] (auto view, auto, auto) { button->setTitle (strMinimized); }); button->addAnimation ("AlphaValue", new Animation::AlphaValueAnimation (0.3f), createDefAnimTimingFunc ()); } EnterEditModeFunc enterEditMode; CTextButton* button {nullptr}; }; #endif //------------------------------------------------------------------------ void VST3Editor::showEditButton (bool state) { #if VSTGUI_LIVE_EDITING if ((state && openUIEditorController) || (!state && openUIEditorController == nullptr)) return; if (state) { openUIEditorController = new EnterEditModeController (getFrame (), [this] () { editingEnabled = true; requestRecreateView (); }); } else { delete openUIEditorController; openUIEditorController = nullptr; } #endif } //------------------------------------------------------------------------ bool VST3Editor::enableEditing (bool state) { if (getFrame ()) { getFrame ()->removeAll (); #if VSTGUI_LIVE_EDITING openUIEditorController = nullptr; if (state) { editingEnabled = true; // update uiDesc file path to absolute if possible if (UIAttributes* attributes = description->getCustomAttributes ("VST3Editor", true)) { const std::string* filePath = attributes->getAttributeValue ("Path"); if (filePath) { CFileStream s; if (!s.open (filePath->c_str (), CFileStream::kReadMode)) { attributes->removeAttribute ("Path"); } else description->setFilePath (filePath->c_str ()); } } getFrame ()->setTransform (CGraphicsTransform ()); nonEditRect = getFrame ()->getViewSize (); description->setController (this); auto* editController = new UIEditController (description); CView* view = editController->createEditView (); if (view) { CCoord width = view->getWidth (); CCoord height = view->getHeight (); getFrame ()->setSize (width, height); getFrame ()->addView (view); getFrame ()->setZoom (getContentScaleFactor ()); getFrame ()->enableTooltips (true); CColor focusColor = kBlueCColor; editController->getEditorDescription ()->getColor ("focus", focusColor); getFrame ()->setFocusColor (focusColor); getFrame ()->setFocusDrawingEnabled (true); getFrame ()->setFocusWidth (1); COptionMenu* fileMenu = editController->getMenuController ()->getFileMenu (); if (fileMenu) { CMenuItem* item = fileMenu->addEntry ( new CCommandMenuItem ({"Save", this, "File", "Save"}), 0); item->setKey ("s", kControl); item = fileMenu->addEntry ( new CCommandMenuItem ({"Save As..", this, "File", "Save As"}), 1); item->setKey ("s", kShift | kControl); item = fileMenu->addEntry (new CCommandMenuItem ( {"Close Editor", this, "File", "Close UIDescription Editor"})); item->setKey ("e", kControl); } COptionMenu* editMenu = editController->getMenuController ()->getEditMenu (); if (editMenu) { editMenu->addSeparator (); editMenu->addEntry (new CCommandMenuItem ( {"Sync Parameter Tags", this, "Edit", "Sync Parameter Tags"})); } return true; } editController->forget (); } else #endif { editingEnabled = false; CView* view = description->createView (viewName.c_str (), this); if (view) { double scaleFactor = getAbsScaleFactor (); CCoord width = view->getWidth () * scaleFactor; CCoord height = view->getHeight () * scaleFactor; if (canResize () == Steinberg::kResultTrue && nonEditRect.isEmpty ()) { Steinberg::ViewRect tmp; if (getRect ().getWidth () != width) tmp.right = getRect ().getWidth (); if (getRect ().getHeight () != height) tmp.bottom = getRect ().getHeight (); if (tmp.getWidth () || tmp.getHeight ()) { if (tmp.getWidth () == 0) tmp.right = width; if (tmp.getHeight () == 0) tmp.bottom = width; checkSizeConstraint (&tmp); nonEditRect.setWidth (tmp.getWidth ()); nonEditRect.setHeight (tmp.getHeight ()); } } getFrame ()->setSize (width, height); getFrame ()->addView (view); getFrame ()->setTransform (CGraphicsTransform ().scale (scaleFactor, scaleFactor)); getFrame ()->invalid (); if (nonEditRect.isEmpty () == false) { Steinberg::ViewRect tmpRect = rect; tmpRect.right = tmpRect.left + (Steinberg::int32)nonEditRect.getWidth (); tmpRect.bottom = tmpRect.top + (Steinberg::int32)nonEditRect.getHeight (); plugFrame->resizeView (this, &tmpRect); } else { rect.right = rect.left + width; rect.bottom = rect.top + height; requestResize ({width, height}); } getFrame ()->setFocusDrawingEnabled (false); // focus drawing support UIAttributes* attributes = description->getCustomAttributes ("FocusDrawing", true); // map old one UIAttributes* oldAttributes = description->getCustomAttributes ("VST3Editor"); if (oldAttributes) { const std::string* attr = oldAttributes->getAttributeValue (kFrameEnableFocusDrawingAttr); if (attr) { if (*attr == "true") { attributes->setAttribute ("enabled", "true"); attr = oldAttributes->getAttributeValue (kFrameFocusColorAttr); if (attr) { attributes->setAttribute ("color", *attr); } attr = oldAttributes->getAttributeValue (kFrameFocusWidthAttr); if (attr) { attributes->setAttribute ("width", *attr); } } oldAttributes->removeAttribute (kFrameFocusColorAttr); oldAttributes->removeAttribute (kFrameFocusWidthAttr); oldAttributes->removeAttribute (kFrameEnableFocusDrawingAttr); } } // new one const std::string* attr = attributes->getAttributeValue ("enabled"); if (attr && *attr == "true") { getFrame ()->setFocusDrawingEnabled (true); attr = attributes->getAttributeValue ("color"); if (attr) { CColor focusColor; if (description->getColor (attr->c_str (), focusColor)) getFrame ()->setFocusColor (focusColor); } attr = attributes->getAttributeValue ("width"); if (attr) { double focusWidth = UTF8StringView (attr->c_str ()).toDouble (); getFrame ()->setFocusWidth (focusWidth); } } #if VSTGUI_LIVE_EDITING if (enableShowEditButton ()) showEditButton (true); #endif return true; } } } return false; } //------------------------------------------------------------------------ void VST3Editor::setDelegate (IVST3EditorDelegate* inDelegate) { delegate = inDelegate; } //------------------------------------------------------------------------ IVST3EditorDelegate* VST3Editor::getDelegate () const { return delegate; } //------------------------------------------------------------------------ UIDescription* VST3Editor::getUIDescription () const { return description; } //------------------------------------------------------------------------ //--- AspectRatioVST3Editor //------------------------------------------------------------------------ void AspectRatioVST3Editor::setMinZoomFactor (double factor) { minZoomFactor = factor; } //------------------------------------------------------------------------ double AspectRatioVST3Editor::getMinZoomFactor () const { return minZoomFactor; } //------------------------------------------------------------------------ bool PLUGIN_API AspectRatioVST3Editor::open (void* parent, const PlatformType& type) { calcZoomFactor = getZoomFactor (); if (VST3Editor::open (parent, type) && getFrame ()) { initialSize = getFrame ()->getViewSize ().getSize (); initialSize /= calcZoomFactor; return true; } return false; } //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API AspectRatioVST3Editor::onSize (Steinberg::ViewRect* newSize) { if (newSize == nullptr) return Steinberg::kInvalidArgument; if (!canCalculateAspectRatio ()) return VST3Editor::onSize (newSize); setZoomFactor (calcZoomFactor); return Steinberg::kResultTrue; } //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API AspectRatioVST3Editor::checkSizeConstraint (Steinberg::ViewRect* rect) { if (rect == nullptr) return Steinberg::kInvalidArgument; if (!canCalculateAspectRatio ()) return VST3Editor::checkSizeConstraint (rect); const CPoint size (rect->getWidth (), rect->getHeight ()); auto diff = size - initialSize; auto anchor = initialSize.x >= initialSize.y ? initialSize.x : initialSize.y; auto sizeAnchor = initialSize.x >= initialSize.y ? size.x : size.y; auto factor = sizeAnchor / anchor; if (factor < minZoomFactor * getContentScaleFactor ()) factor = minZoomFactor * getContentScaleFactor (); factor = std::round (factor * anchor) / anchor; auto newSize = initialSize * factor; calcZoomFactor = factor / getContentScaleFactor (); if (newSize != size) { rect->right = rect->left + newSize.x; rect->bottom = rect->top + newSize.y; } return Steinberg::kResultTrue; } #ifdef VST3_CONTENT_SCALE_SUPPORT //------------------------------------------------------------------------ Steinberg::tresult PLUGIN_API AspectRatioVST3Editor::setContentScaleFactor (ScaleFactor factor) { auto res = VST3Editor::setContentScaleFactor (factor); if (res == Steinberg::kResultTrue && canCalculateAspectRatio ()) setZoomFactor (calcZoomFactor); return res; } #endif //------------------------------------------------------------------------ bool AspectRatioVST3Editor::canCalculateAspectRatio () const { auto f = getFrame (); if (inEditMode () || f == nullptr || f->hasChildren () == false) return false; return true; } //------------------------------------------------------------------------ } // VSTGUI