// 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 "coptionmenu.h" #include "../cbitmap.h" #include "../cframe.h" #include "../cstring.h" #include "../events.h" #include "../platform/iplatformoptionmenu.h" #include "../platform/iplatformframe.h" namespace VSTGUI { //------------------------------------------------------------------------ struct CMenuItem::Impl { UTF8String title; UTF8String keyCode; SharedPointer submenu; SharedPointer icon; int32_t flags {0}; int32_t keyModifiers {0}; VirtualKey virtualKey {VirtualKey::None}; int32_t tag {-1}; }; //------------------------------------------------------------------------ // CMenuItem //------------------------------------------------------------------------ /*! @class CMenuItem Defines an item of a VSTGUI::COptionMenu */ //------------------------------------------------------------------------ CMenuItem::CMenuItem () { impl = std::make_unique (); } //------------------------------------------------------------------------ CMenuItem::~CMenuItem () noexcept = default; //------------------------------------------------------------------------ /** * CMenuItem constructor. * @param inTitle title of item * @param inFlags CMenuItem::Flags of item * @param inKeycode keycode of item * @param inKeyModifiers keymodifiers of item * @param inIcon icon of item */ //------------------------------------------------------------------------ CMenuItem::CMenuItem (const UTF8String& inTitle, const UTF8String& inKeycode, int32_t inKeyModifiers, CBitmap* inIcon, int32_t inFlags) : CMenuItem () { impl->flags = inFlags; setTitle (inTitle); setKey (inKeycode, inKeyModifiers); setIcon (inIcon); } //------------------------------------------------------------------------ /** * CMenuItem constructor. * @param inTitle title of item * @param inSubmenu submenu of item * @param inIcon icon of item */ //------------------------------------------------------------------------ CMenuItem::CMenuItem (const UTF8String& inTitle, COptionMenu* inSubmenu, CBitmap* inIcon) : CMenuItem () { setTitle (inTitle); setSubmenu (inSubmenu); setIcon (inIcon); } //------------------------------------------------------------------------ /** * CMenuItem constructor. * @param inTitle title of item * @param inTag tag of item */ //------------------------------------------------------------------------ CMenuItem::CMenuItem (const UTF8String& inTitle, int32_t inTag) : CMenuItem () { setTitle (inTitle); setTag (inTag); } //------------------------------------------------------------------------ /** * CMenuItem copy constructor. * @param item item to copy */ //------------------------------------------------------------------------ CMenuItem::CMenuItem (const CMenuItem& item) : CMenuItem () { impl->flags = item.impl->flags; setTitle (item.getTitle ()); setIcon (item.getIcon ()); if (item.getVirtualKey () != VirtualKey::None) setVirtualKey (item.getVirtualKey (), item.getKeyModifiers ()); else setKey (item.getKeycode (), item.getKeyModifiers ()); setTag (item.getTag ()); setSubmenu (item.getSubmenu ()); } //------------------------------------------------------------------------ void CMenuItem::setTitle (const UTF8String& inTitle) { impl->title = inTitle; } //------------------------------------------------------------------------ void CMenuItem::setKey (const UTF8String& inKeycode, int32_t inKeyModifiers) { impl->keyCode = inKeycode; impl->keyModifiers = inKeyModifiers; impl->virtualKey = VirtualKey::None; } #if VSTGUI_ENABLE_DEPRECATED_METHODS //------------------------------------------------------------------------ void CMenuItem::setVirtualKey (int32_t inVirtualKeyCode, int32_t inKeyModifiers) { setKey (nullptr, inKeyModifiers); impl->virtualKey = fromVstVirtualKey (inVirtualKeyCode); } //------------------------------------------------------------------------ int32_t CMenuItem::getVirtualKeyCode () const { return toVstVirtualKey (impl->virtualKey); } #endif //------------------------------------------------------------------------ void CMenuItem::setVirtualKey (VirtualKey inVirtualKey, int32_t inKeyModifiers) { setKey (nullptr, inKeyModifiers); impl->virtualKey = inVirtualKey; } //------------------------------------------------------------------------ void CMenuItem::setSubmenu (COptionMenu* inSubmenu) { impl->submenu = inSubmenu; } //------------------------------------------------------------------------ void CMenuItem::setIcon (CBitmap* inIcon) { impl->icon = inIcon; } //------------------------------------------------------------------------ void CMenuItem::setTag (int32_t t) { impl->tag = t; } //------------------------------------------------------------------------ void CMenuItem::setEnabled (bool state) { setBit (impl->flags, kDisabled, !state); } //------------------------------------------------------------------------ void CMenuItem::setChecked (bool state) { setBit (impl->flags, kChecked, state); } //------------------------------------------------------------------------ void CMenuItem::setIsTitle (bool state) { setBit (impl->flags, kTitle, state); } //------------------------------------------------------------------------ void CMenuItem::setIsSeparator (bool state) { setBit (impl->flags, kSeparator, state); } //------------------------------------------------------------------------ bool CMenuItem::isEnabled () const { return !hasBit (impl->flags, kDisabled); } //------------------------------------------------------------------------ bool CMenuItem::isChecked () const { return hasBit (impl->flags, kChecked); } //------------------------------------------------------------------------ bool CMenuItem::isTitle () const { return hasBit (impl->flags, kTitle); } //------------------------------------------------------------------------ bool CMenuItem::isSeparator () const { return hasBit (impl->flags, kSeparator); } //------------------------------------------------------------------------ const UTF8String& CMenuItem::getTitle () const { return impl->title; } //------------------------------------------------------------------------ int32_t CMenuItem::getKeyModifiers () const { return impl->keyModifiers; } //------------------------------------------------------------------------ const UTF8String& CMenuItem::getKeycode () const { return impl->keyCode; } //------------------------------------------------------------------------ VirtualKey CMenuItem::getVirtualKey () const { return impl->virtualKey; } //------------------------------------------------------------------------ COptionMenu* CMenuItem::getSubmenu () const { return impl->submenu; } //------------------------------------------------------------------------ CBitmap* CMenuItem::getIcon () const { return impl->icon; } //------------------------------------------------------------------------ int32_t CMenuItem::getTag () const { return impl->tag; } //------------------------------------------------------------------------ /*! @class CCommandMenuItem The CCommandMenuItem supports setting a category, name and a target. The target will get a @link CBaseObject::notify notify()@endlink call before the item is displayed and after it was selected. @see CCommandMenuItem::kMsgMenuItemValidate and @see CCommandMenuItem::kMsgMenuItemSelected */ //------------------------------------------------------------------------ CCommandMenuItem::CCommandMenuItem (Desc&& args) : CMenuItem (args.title, args.keycode, args.keyModifiers, args.icon, args.flags) , commandCategory (std::move (args.commandCategory)) , commandName (std::move (args.commandName)) , itemTarget (std::move (args.target)) { setTag (args.tag); } //------------------------------------------------------------------------ CCommandMenuItem::CCommandMenuItem (const Desc& args) : CMenuItem (args.title, args.keycode, args.keyModifiers, args.icon, args.flags) , commandCategory (args.commandCategory) , commandName (args.commandName) , itemTarget (args.target) { setTag (args.tag); } //------------------------------------------------------------------------ CCommandMenuItem::CCommandMenuItem (const CCommandMenuItem& item) : CMenuItem (item) , validateFunc (item.validateFunc) , selectedFunc (item.selectedFunc) , commandCategory (item.commandCategory) , commandName (item.commandName) { setItemTarget (item.itemTarget); } //------------------------------------------------------------------------ void CCommandMenuItem::setItemTarget (ICommandMenuItemTarget* target) { itemTarget = target; } //------------------------------------------------------------------------ void CCommandMenuItem::setCommandCategory (const UTF8String& category) { commandCategory = category; } //------------------------------------------------------------------------ bool CCommandMenuItem::isCommandCategory (const UTF8String& category) const { return commandCategory == category; } //------------------------------------------------------------------------ void CCommandMenuItem::setCommandName (const UTF8String& name) { commandName = name; } //------------------------------------------------------------------------ bool CCommandMenuItem::isCommandName (const UTF8String& name) const { return commandName == name; } //------------------------------------------------------------------------ void CCommandMenuItem::setActions (SelectedCallbackFunction&& selected, ValidateCallbackFunction&& validate) { selectedFunc = std::move (selected); validateFunc = std::move (validate); } //------------------------------------------------------------------------ void CCommandMenuItem::execute () { if (selectedFunc) selectedFunc (this); if (itemTarget) itemTarget->onCommandMenuItemSelected (this); } //------------------------------------------------------------------------ void CCommandMenuItem::validate () { if (validateFunc) validateFunc (this); if (itemTarget) itemTarget->validateCommandMenuItem (this); } //------------------------------------------------------------------------ // COptionMenu //------------------------------------------------------------------------ /*! @class COptionMenu Define a rectangle view where a text-value can be displayed with a given font and color. The text-value is centered in the given rect. A bitmap can be used as background, a second bitmap can be used when the option menu is popuped. There are 2 styles with or without a shadowed text. When a mouse click occurs, a popup menu is displayed. */ //------------------------------------------------------------------------ /** * COptionMenu constructor. * @param size the size of this view * @param listener the listener * @param tag the control tag * @param background the background bitmap * @param bgWhenClick the background bitmap if the option menu is displayed * @param style the style of the display (see CParamDisplay for styles) */ //------------------------------------------------------------------------ COptionMenu::COptionMenu (const CRect& size, IControlListener* listener, int32_t tag, CBitmap* background, CBitmap* bgWhenClick, const int32_t style) : CParamDisplay (size, background, style) , bgWhenClick (bgWhenClick) { this->listener = listener; this->tag = tag; lastButton = kRButton; menuItems = new CMenuItemList; setWantsFocus (true); } //------------------------------------------------------------------------ COptionMenu::COptionMenu () : CParamDisplay (CRect (0, 0, 0, 0)) { menuItems = new CMenuItemList; setWantsFocus (true); } //------------------------------------------------------------------------ COptionMenu::COptionMenu (const COptionMenu& v) : CParamDisplay (v) , menuItems (new CMenuItemList (*v.menuItems)) , nbItemsPerColumn (v.nbItemsPerColumn) , bgWhenClick (v.bgWhenClick) { setWantsFocus (true); } //------------------------------------------------------------------------ COptionMenu::~COptionMenu () noexcept { removeAllEntry (); delete menuItems; } //------------------------------------------------------------------------ void COptionMenu::registerOptionMenuListener (IOptionMenuListener* listener) { if (!listeners) listeners = std::unique_ptr (new MenuListenerList ()); listeners->add (listener); } //------------------------------------------------------------------------ void COptionMenu::unregisterOptionMenuListener (IOptionMenuListener* listener) { if (listeners) listeners->remove (listener); } //------------------------------------------------------------------------ void COptionMenu::onKeyboardEvent (KeyboardEvent& event) { if (event.type != EventType::KeyUp && event.modifiers.empty () && event.character == 0) { if (event.virt == VirtualKey::Return) { auto self = shared (this); getFrame ()->doAfterEventProcessing ([self] () { self->doPopup (); }); event.consumed = true; return; } if (!(style & (kMultipleCheckStyle & ~kCheckStyle))) { if (event.virt == VirtualKey::Up) { int32_t value = (int32_t)getValue ()-1; if (value >= 0) { CMenuItem* entry = getEntry (value); while (entry && (entry->isSeparator () || entry->isTitle () || !entry->isEnabled () || entry->getSubmenu ())) entry = getEntry (--value); if (entry) { beginEdit (); setValue ((float)value); lastResult = (int32_t)getValue (); valueChanged (); endEdit (); invalid (); } } event.consumed = true; return; } if (event.virt == VirtualKey::Down) { int32_t value = (int32_t)getValue ()+1; if (value < getNbEntries ()) { CMenuItem* entry = getEntry (value); while (entry && (entry->isSeparator () || entry->isTitle () || !entry->isEnabled () || entry->getSubmenu ())) entry = getEntry (++value); if (entry) { beginEdit (); setValue ((float)value); lastResult = (int32_t)getValue (); valueChanged (); endEdit (); invalid (); } } event.consumed = true; return; } } } CParamDisplay::onKeyboardEvent (event); } //------------------------------------------------------------------------ void COptionMenu::beforePopup () { if (listeners) listeners->forEach ([this] (IOptionMenuListener* l) { l->onOptionMenuPrePopup (this); }); for (auto& menuItem : *menuItems) { if (auto* commandItem = menuItem.cast ()) commandItem->validate (); if (menuItem->getSubmenu ()) menuItem->getSubmenu ()->beforePopup (); } } //------------------------------------------------------------------------ void COptionMenu::afterPopup () { for (auto& menuItem : *menuItems) { if (menuItem->getSubmenu ()) menuItem->getSubmenu ()->afterPopup (); } if (listeners) listeners->forEach ([this] (IOptionMenuListener* l) { l->onOptionMenuPostPopup (this); }); } //------------------------------------------------------------------------ bool COptionMenu::doPopup () { if (bgWhenClick) invalid (); auto result = popup (); if (bgWhenClick) invalid (); return result; } //------------------------------------------------------------------------ bool COptionMenu::popup (const PopupCallback& callback) { if (!getFrame ()) return false; beforePopup (); lastResult = -1; lastMenu = nullptr; if (!menuItems->empty ()) { getFrame ()->onStartLocalEventLoop (); if (auto platformMenu = getFrame ()->getPlatformFrame ()->createPlatformOptionMenu ()) { inPopup = true; auto self = shared (this); platformMenu->popup (this, [self, callback] (COptionMenu* menu, PlatformOptionMenuResult result) { if (result.menu != nullptr) { bool preventSettingValue = false; if (self->listeners) { self->listeners->forEach ( [self, &result] (IOptionMenuListener* l) { return l->onOptionMenuSetPopupResult (self, result.menu, result.index); }, [&preventSettingValue] (bool result) { if (result) preventSettingValue = true; return result; }); } if (!preventSettingValue) { self->beginEdit (); self->lastMenu = result.menu; self->lastResult = result.index; self->lastMenu->setValue (static_cast (self->lastResult)); self->valueChanged (); self->invalid (); if (auto commandItem = dynamic_cast ( self->lastMenu->getEntry (self->lastResult))) commandItem->execute (); self->endEdit (); } } self->afterPopup (); if (callback) callback (self); self->inPopup = false; }); } } return true; } //------------------------------------------------------------------------ bool COptionMenu::popup (CFrame* frame, const CPoint& frameLocation, const PopupCallback& callback) { if (frame == nullptr || menuItems->empty ()) return false; if (isAttached ()) return false; CView* oldFocusView = frame->getFocusView (); CRect size (frameLocation, CPoint (0, 0)); setViewSize (size); frame->addView (this); auto prevFocusView = shared (oldFocusView); popup ([prevFocusView, callback] (COptionMenu* menu) { if (auto frame = menu->getFrame ()) { frame->removeView (menu, false); frame->setFocusView (prevFocusView); } else { // if the selected menu item is a command menu and the command menu has removed this // option menu from the view hierarchy then we have to make sure the reference count is // corrected menu->remember (); } if (callback) callback (menu); }); return true; } //------------------------------------------------------------------------ void COptionMenu::cleanupSeparators (bool deep) { if (getItems ()->empty ()) return; std::listindicesToRemove; bool lastEntryWasSeparator = true; for (auto i = 0; i < getNbEntries () - 1; ++i) { auto entry = getEntry (i); vstgui_assert (entry); if (!entry) continue; if (entry->isSeparator ()) { if (lastEntryWasSeparator) { indicesToRemove.push_front (i); } lastEntryWasSeparator = true; } else lastEntryWasSeparator = false; if (deep) { if (auto subMenu = entry->getSubmenu ()) { subMenu->cleanupSeparators (deep); } } } auto lastIndex = getNbEntries () - 1; if (getEntry (lastIndex)->isSeparator ()) { indicesToRemove.push_front (lastIndex); } for (auto index : indicesToRemove) { removeEntry (index); } } //------------------------------------------------------------------------ void COptionMenu::setPrefixNumbers (int32_t preCount) { if (preCount >= 0 && preCount <= 4) prefixNumbers = preCount; } /** * @param item menu item to add. Takes ownership of item. * @param index position of insertation. -1 appends the item */ //----------------------------------------------------------------------------- CMenuItem* COptionMenu::addEntry (CMenuItem* item, int32_t index) { if (index < 0 || index > getNbEntries ()) menuItems->emplace_back (owned (item)); else { menuItems->insert (menuItems->begin () + index, owned (item)); } return item; } //----------------------------------------------------------------------------- CMenuItem* COptionMenu::addEntry (COptionMenu* submenu, const UTF8String& title) { auto* item = new CMenuItem (title, submenu); return addEntry (item); } //----------------------------------------------------------------------------- CMenuItem* COptionMenu::addEntry (const UTF8String& title, int32_t index, int32_t itemFlags) { if (title == "-") return addSeparator (index); auto* item = new CMenuItem (title, nullptr, 0, nullptr, itemFlags); return addEntry (item, index); } //----------------------------------------------------------------------------- CMenuItem* COptionMenu::addSeparator (int32_t index) { auto* item = new CMenuItem ("", nullptr, 0, nullptr, CMenuItem::kSeparator); return addEntry (item, index); } //----------------------------------------------------------------------------- CMenuItem* COptionMenu::getCurrent () const { return getEntry (currentIndex); } //----------------------------------------------------------------------------- CMenuItem* COptionMenu::getEntry (int32_t index) const { if (index < 0 || menuItems->empty () || index >= getNbEntries ()) return nullptr; return (*menuItems)[static_cast (index)]; } //----------------------------------------------------------------------------- int32_t COptionMenu::getNbEntries () const { return static_cast (menuItems->size ()); } //------------------------------------------------------------------------ COptionMenu* COptionMenu::getSubMenu (int32_t idx) const { CMenuItem* item = getEntry (idx); if (item) return item->getSubmenu (); return nullptr; } //------------------------------------------------------------------------ int32_t COptionMenu::getCurrentIndex (bool countSeparator) const { if (countSeparator) return currentIndex; int32_t i = 0; int32_t numSeparators = 0; for (auto& item : *menuItems) { if (item->isSeparator ()) numSeparators++; if (i == currentIndex) break; i++; } return currentIndex - numSeparators; } //------------------------------------------------------------------------ bool COptionMenu::setCurrent (int32_t index, bool countSeparator) { CMenuItem* item = nullptr; if (countSeparator) { item = getEntry (index); if (!item || item->isSeparator ()) return false; currentIndex = index; } else { int32_t i = 0; for (auto& menuItem : *menuItems) { if (i > index) break; if (menuItem->isSeparator ()) index++; i++; } currentIndex = index; item = getEntry (currentIndex); } if (item && style & (kMultipleCheckStyle & ~kCheckStyle)) item->setChecked (!item->isChecked ()); // to force the redraw setDirty (); return true; } //------------------------------------------------------------------------ bool COptionMenu::removeEntry (int32_t index) { if (index < 0 || menuItems->empty () || index >= getNbEntries ()) return false; menuItems->erase (menuItems->begin () + index); return true; } //------------------------------------------------------------------------ bool COptionMenu::removeAllEntry () { menuItems->clear (); return true; } //------------------------------------------------------------------------ bool COptionMenu::checkEntry (int32_t index, bool state) { CMenuItem* item = getEntry (index); if (item) { item->setChecked (state); return true; } return false; } //------------------------------------------------------------------------ bool COptionMenu::checkEntryAlone (int32_t index) { int32_t pos = 0; for (auto& item : *menuItems) { item->setChecked (pos == index); pos++; } return true; } //------------------------------------------------------------------------ bool COptionMenu::isCheckEntry (int32_t index) const { CMenuItem* item = getEntry (index); if (item && item->isChecked ()) return true; return false; } //------------------------------------------------------------------------ void COptionMenu::draw (CDrawContext *pContext) { CMenuItem* item = getEntry (currentIndex); drawBack (pContext, inPopup ? bgWhenClick : nullptr); if (item) drawPlatformText (pContext, item->getTitle ()); setDirty (false); } //------------------------------------------------------------------------ CMouseEventResult COptionMenu::onMouseDown (CPoint& where, const CButtonState& buttons) { lastButton = buttons; if (lastButton & (kLButton|kRButton|kApple)) { auto self = shared (this); getFrame ()->doAfterEventProcessing ([self] () { self->doPopup (); }); return kMouseDownEventHandledButDontNeedMovedOrUpEvents; } return kMouseEventNotHandled; } //------------------------------------------------------------------------ COptionMenu *COptionMenu::getLastItemMenu (int32_t &idxInMenu) const { idxInMenu = lastMenu ? (int32_t)lastMenu->getValue (): -1; return lastMenu; } //------------------------------------------------------------------------ void COptionMenu::setValue (float val) { auto newIndex = static_cast (std::round (val)); if (newIndex < 0 || newIndex >= getNbEntries ()) return; currentIndex = newIndex; if (style & (kMultipleCheckStyle & ~kCheckStyle)) { CMenuItem* item = getCurrent (); if (item) item->setChecked (!item->isChecked ()); } CParamDisplay::setValue (static_cast (newIndex)); // to force the redraw setDirty (); } //------------------------------------------------------------------------ float COptionMenu::getMax () const { if (menuItems->empty ()) return 0.f; return static_cast (menuItems->size () - 1); } //------------------------------------------------------------------------ void COptionMenu::takeFocus () { CParamDisplay::takeFocus (); } //------------------------------------------------------------------------ void COptionMenu::looseFocus () { CView* receiver = getParentView () ? getParentView () : getFrame (); while (receiver) { if (receiver->notify (this, kMsgLooseFocus) == kMessageNotified) break; receiver = receiver->getParentView (); } CParamDisplay::looseFocus (); } } // VSTGUI