diff --git a/loader/include/Geode/ui/Dropdown.hpp b/loader/include/Geode/ui/Dropdown.hpp new file mode 100644 index 000000000..8b249eebb --- /dev/null +++ b/loader/include/Geode/ui/Dropdown.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +class DropdownOverlay; + +namespace geode { + class GEODE_DLL Dropdown : public cocos2d::CCNode { + class Impl; + std::unique_ptr m_impl; + + friend class ::DropdownOverlay; + + protected: + Dropdown(); + ~Dropdown() override; + + bool init( + float width, std::vector options, + Function callback + ); + + void onOpen(cocos2d::CCObject*); + + public: + static Dropdown* create( + float width, std::vector options, + Function callback + ); + + void setSelected(size_t index); + void setSelected(std::string_view value); + size_t getSelectedIndex() const; + std::string getSelectedValue() const; + + void setEnabled(bool enabled); + bool isEnabled() const; + + void setItems(std::vector options); + }; +} diff --git a/loader/src/ui/mods/settings/SettingNodeV3.cpp b/loader/src/ui/mods/settings/SettingNodeV3.cpp index 5979e907c..b51932c04 100644 --- a/loader/src/ui/mods/settings/SettingNodeV3.cpp +++ b/loader/src/ui/mods/settings/SettingNodeV3.cpp @@ -1,12 +1,13 @@ #include "SettingNodeV3.hpp" -#include -#include + +#include "KeybindEditPopup.hpp" + #include +#include #include #include -#include -#include -#include "KeybindEditPopup.hpp" +#include +#include class SettingNodeV3::Impl final { public: @@ -26,16 +27,15 @@ class SettingNodeV3::Impl final { }; bool SettingNodeV3::init(std::shared_ptr setting, float width) { - if (!CCNode::init()) - return false; + if (!CCNode::init()) return false; // note: setting may be null due to UnresolvedCustomSettingNodeV3 m_impl = std::make_shared(); m_impl->setting = setting; - m_impl->bg = CCLayerColor::create({ 0, 0, 0, 0 }); - m_impl->bg->setContentSize({ width, 0 }); + m_impl->bg = CCLayerColor::create({0, 0, 0, 0}); + m_impl->bg->setContentSize({width, 0}); m_impl->bg->ignoreAnchorPointForPosition(false); m_impl->bg->setAnchorPoint(ccp(.5f, .5f)); this->addChildAtPosition(m_impl->bg, Anchor::Center); @@ -43,8 +43,11 @@ bool SettingNodeV3::init(std::shared_ptr setting, float width) { m_impl->nameMenu = CCMenu::create(); m_impl->nameMenu->setContentWidth(width / 2 + 25); - m_impl->nameLabel = CCLabelBMFont::create(setting ? setting->getDisplayName().c_str() : "", "bigFont.fnt"); - m_impl->nameLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .4f)->setScalePriority(1)); + m_impl->nameLabel = + CCLabelBMFont::create(setting ? setting->getDisplayName().c_str() : "", "bigFont.fnt"); + m_impl->nameLabel->setLayoutOptions( + AxisLayoutOptions::create()->setScaleLimits(.1f, .4f)->setScalePriority(1) + ); m_impl->nameMenu->addChild(m_impl->nameLabel); m_impl->statusLabel = CCLabelBMFont::create("", "bigFont.fnt"); @@ -54,29 +57,27 @@ bool SettingNodeV3::init(std::shared_ptr setting, float width) { if (setting && setting->getDescription()) { auto descSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); descSpr->setScale(.5f); - m_impl->descButton = CCMenuItemSpriteExtra::create( - descSpr, this, menu_selector(SettingNodeV3::onDescription) - ); + m_impl->descButton = + CCMenuItemSpriteExtra::create(descSpr, this, menu_selector(SettingNodeV3::onDescription)); m_impl->nameMenu->addChild(m_impl->descButton); } auto resetSpr = CCSprite::createWithSpriteFrameName("reset-gold.png"_spr); resetSpr->setScale(.5f); - m_impl->resetButton = CCMenuItemSpriteExtra::create( - resetSpr, this, menu_selector(SettingNodeV3::onReset) - ); + m_impl->resetButton = + CCMenuItemSpriteExtra::create(resetSpr, this, menu_selector(SettingNodeV3::onReset)); m_impl->nameMenu->addChild(m_impl->resetButton); m_impl->nameMenu->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::Start)); this->addChildAtPosition(m_impl->nameMenu, Anchor::Left, ccp(10, 0), ccp(0, .5f)); m_impl->buttonMenu = CCMenu::create(); - m_impl->buttonMenu->setContentSize({ width / 2 - 55, 30 }); + m_impl->buttonMenu->setContentSize({width / 2 - 55, 30}); m_impl->buttonMenu->setLayout(AnchorLayout::create()); this->addChildAtPosition(m_impl->buttonMenu, Anchor::Right, ccp(-10, 0), ccp(1, .5f)); - this->setAnchorPoint({ .5f, .5f }); - this->setContentSize({ width, 30 }); + this->setAnchorPoint({.5f, .5f}); + this->setContentSize({width, 30}); return true; } @@ -106,7 +107,9 @@ void SettingNodeV3::updateState(CCNode* invoker) { m_impl->bg->setOpacity(75); } - m_impl->nameMenu->setContentWidth(this->getContentWidth() - m_impl->buttonMenu->getContentWidth() - 25); + m_impl->nameMenu->setContentWidth( + this->getContentWidth() - m_impl->buttonMenu->getContentWidth() - 25 + ); m_impl->nameMenu->updateLayout(); } @@ -117,11 +120,13 @@ void SettingNodeV3::updateState2(CCNode* invoker) { void SettingNodeV3::onDescription(CCObject*) { if (!m_impl->setting) return; auto title = m_impl->setting->getDisplayName(); - MDPopup::create(true, + MDPopup::create( + true, title.c_str(), m_impl->setting->getDescription().value_or("No description provided"), "OK" - )->show(); + ) + ->show(); } void SettingNodeV3::onReset(CCObject*) { @@ -131,7 +136,8 @@ void SettingNodeV3::onReset(CCObject*) { "Are you sure you want to reset {} to default?", this->getSetting()->getDisplayName() ), - "Cancel", "Reset", + "Cancel", + "Reset", [this](auto, bool btn2) { if (btn2) { this->resetToDefault(); @@ -150,7 +156,8 @@ void SettingNodeV3::markChanged(CCNode* invoker) { SettingNodeValueChangeEventV3( m_impl->setting ? m_impl->setting->getModID() : "", m_impl->setting ? m_impl->setting->getKey() : "" - ).send(this, false); + ) + .send(this, false); } void SettingNodeV3::commit() { @@ -167,7 +174,8 @@ void SettingNodeV3::resetToDefault() { m_impl->committed = true; this->onResetToDefault(); this->updateState(nullptr); - SettingNodeValueChangeEventV3(m_impl->setting->getModID(), m_impl->setting->getKey()).send(this, false); + SettingNodeValueChangeEventV3(m_impl->setting->getModID(), m_impl->setting->getKey()) + .send(this, false); } void SettingNodeV3::overrideDescription(std::optional description) { @@ -213,8 +221,7 @@ std::shared_ptr SettingNodeV3::getSetting() const { // TitleSettingNodeV3 bool TitleSettingNodeV3::init(std::shared_ptr setting, float width) { - if (!SettingNodeV3::init(setting, width)) - return false; + if (!SettingNodeV3::init(setting, width)) return false; // note: setting may be null @@ -235,8 +242,7 @@ bool TitleSettingNodeV3::init(std::shared_ptr setting, float wid uncollapseSprBG->setScale(.2f); m_collapseToggle = CCMenuItemToggler::create( - collapseSprBG, uncollapseSprBG, - this, menu_selector(TitleSettingNodeV3::onCollapse) + collapseSprBG, uncollapseSprBG, this, menu_selector(TitleSettingNodeV3::onCollapse) ); m_collapseToggle->m_notClickable = true; this->getButtonMenu()->setContentWidth(20); @@ -291,7 +297,9 @@ TitleSettingNodeV3* TitleSettingNodeV3::create(std::shared_ptr s return nullptr; } -TitleSettingNodeV3* TitleSettingNodeV3::create(ZStringView title, std::optional description, float width) { +TitleSettingNodeV3* TitleSettingNodeV3::create( + ZStringView title, std::optional description, float width +) { auto ret = TitleSettingNodeV3::create(nullptr, width); ret->getNameLabel()->setString(title.c_str()); ret->overrideDescription(description); @@ -302,8 +310,7 @@ TitleSettingNodeV3* TitleSettingNodeV3::create(ZStringView title, std::optional< // InfoSettingNodeV3 bool InfoSettingNodeV3::init(std::shared_ptr setting, float width) { - if (!SettingNodeV3::init(setting, width)) - return false; + if (!SettingNodeV3::init(setting, width)) return false; this->setContentHeight(22); this->getNameLabel()->setVisible(false); @@ -318,8 +325,12 @@ bool InfoSettingNodeV3::init(std::shared_ptr setting, float width auto infoLabel = TextArea::create( setting->getDescription().value_or(""), - "chatFont.fnt", .65f, bg->getContentWidth() - 45, - ccp(.4999f, .4999f), 12, false + "chatFont.fnt", + .65f, + bg->getContentWidth() - 45, + ccp(.4999f, .4999f), + 12, + false ); this->setContentHeight(30 + infoLabel->getContentHeight()); @@ -356,21 +367,18 @@ InfoSettingNodeV3* InfoSettingNodeV3::create(std::shared_ptr sett // ButtonSettingNodeV3 bool ButtonSettingNodeV3::init(std::shared_ptr setting, float width) { - if (!SettingNodeV3::init(setting, width)) - return false; + if (!SettingNodeV3::init(setting, width)) return false; this->getNameLabel()->setVisible(false); - for (const auto& [k, v] : setting->getButtons()) { + for (auto const& [k, v] : setting->getButtons()) { auto spr = createGeodeButton(v); spr->setScale(.5f); spr->setCascadeColorEnabled(true); spr->setCascadeOpacityEnabled(true); - auto button = Button::createWithNode( - spr, [setting, k] (auto sender) { - ButtonSettingPressedEventV3(setting->getMod(), setting->getKey()).send(k); - } - ); + auto button = Button::createWithNode(spr, [setting, k](auto sender) { + ButtonSettingPressedEventV3(setting->getMod(), setting->getKey()).send(k); + }); button->setCascadeColorEnabled(true); button->setCascadeOpacityEnabled(true); button->setID(fmt::format("{}-button", k)); @@ -378,7 +386,7 @@ bool ButtonSettingNodeV3::init(std::shared_ptr setting, float w this->getButtonMenu()->addChild(button); } - this->getButtonMenu()->setAnchorPoint({ 0.f, .5f }); + this->getButtonMenu()->setAnchorPoint({0.f, .5f}); this->getButtonMenu()->setContentWidth(setting->getDescription() ? width - 45 : width - 20); auto buttonLayout = RowLayout::create(); @@ -406,7 +414,7 @@ bool ButtonSettingNodeV3::init(std::shared_ptr setting, float w } void ButtonSettingNodeV3::enableButtons(bool enabled) { - for (const auto& button : buttons) { + for (auto const& button : buttons) { button->setEnabled(enabled); button->setOpacity(enabled ? 255 : 175); button->setColor(enabled ? ccc3(255, 255, 255) : ccc3(166, 166, 166)); @@ -455,17 +463,16 @@ ButtonSettingNodeV3* ButtonSettingNodeV3::create(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; + if (!SettingValueNodeV3::init(setting, width)) return false; this->getButtonMenu()->setContentWidth(20); m_toggle = CCMenuItemToggler::createWithStandardSprites( this, menu_selector(BoolSettingNodeV3::onToggle), .55f ); - m_toggle->m_onButton->setContentSize({ 25, 25 }); + m_toggle->m_onButton->setContentSize({25, 25}); m_toggle->m_onButton->getNormalImage()->setPosition(ccp(25, 25) / 2); - m_toggle->m_offButton->setContentSize({ 25, 25 }); + m_toggle->m_offButton->setContentSize({25, 25}); m_toggle->m_offButton->getNormalImage()->setPosition(ccp(25, 25) / 2); m_toggle->m_notClickable = true; m_toggle->toggle(setting->getValue()); @@ -505,43 +512,28 @@ BoolSettingNodeV3* BoolSettingNodeV3::create(std::shared_ptr sett // StringSettingNodeV3 bool StringSettingNodeV3::init(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; - - m_input = TextInput::create(setting->getEnumOptions() ? width / 2 - 50 : width / 2, "Text"); - m_input->setCallback([this](auto const& str) { - this->setValue(str, m_input); - }); - m_input->setScale(.7f); - m_input->setString(this->getSetting()->getValue()); - if (auto filter = this->getSetting()->getAllowedCharacters()) { - m_input->setFilter(*filter); - } - - this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center); + if (!SettingValueNodeV3::init(setting, width)) return false; if (setting->getEnumOptions()) { - m_input->getBGSprite()->setVisible(false); - m_input->setEnabled(false); - m_input->getInputNode()->m_textLabel->setOpacity(255); - m_input->getInputNode()->m_textLabel->setColor(ccWHITE); - - m_arrowLeftSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - m_arrowLeftSpr->setFlipX(true); - m_arrowLeftSpr->setScale(.4f); - auto arrowLeftBtn = CCMenuItemSpriteExtra::create( - m_arrowLeftSpr, this, menu_selector(StringSettingNodeV3::onArrow) - ); - arrowLeftBtn->setTag(-1); - this->getButtonMenu()->addChildAtPosition(arrowLeftBtn, Anchor::Left, ccp(5, 0)); - - m_arrowRightSpr = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); - m_arrowRightSpr->setScale(.4f); - auto arrowRightBtn = CCMenuItemSpriteExtra::create( - m_arrowRightSpr, this, menu_selector(StringSettingNodeV3::onArrow) - ); - arrowRightBtn->setTag(1); - this->getButtonMenu()->addChildAtPosition(arrowRightBtn, Anchor::Right, ccp(-5, 0)); + auto options = *setting->getEnumOptions(); + m_dropdown = Dropdown::create(width / 2, options, [this](std::string const& value, size_t) { + this->setValue(value, m_dropdown); + }); + m_dropdown->setSelected(this->getSetting()->getValue()); + m_dropdown->setScale(.7f); + this->getButtonMenu()->addChildAtPosition(m_dropdown, Anchor::Center); + } + else { + m_input = TextInput::create(width / 2, "Text"); + m_input->setCallback([this](auto const& str) { + this->setValue(str, m_input); + }); + m_input->setScale(.7f); + m_input->setString(this->getSetting()->getValue()); + if (auto filter = this->getSetting()->getAllowedCharacters()) { + m_input->setFilter(*filter); + } + this->getButtonMenu()->addChildAtPosition(m_input, Anchor::Center); } this->updateState(nullptr); @@ -552,32 +544,19 @@ bool StringSettingNodeV3::init(std::shared_ptr setting, float w void StringSettingNodeV3::updateState(CCNode* invoker) { SettingValueNodeV3::updateState(invoker); - if (invoker != m_input) { - m_input->setString(this->getValue()); - } - auto enable = this->getSetting()->shouldEnable(); - if (!this->getSetting()->getEnumOptions()) { - m_input->setEnabled(enable); - } - else { - m_arrowRightSpr->setOpacity(enable ? 255 : 155); - m_arrowRightSpr->setColor(enable ? ccWHITE : ccGRAY); - m_arrowLeftSpr->setOpacity(enable ? 255 : 155); - m_arrowLeftSpr->setColor(enable ? ccWHITE : ccGRAY); - } -} - -void StringSettingNodeV3::onArrow(CCObject* sender) { - auto options = *this->getSetting()->getEnumOptions(); - auto index = ranges::indexOf(options, this->getValue()).value_or(0); - if (sender->getTag() > 0) { - index = index < options.size() - 1 ? index + 1 : 0; + if (m_dropdown) { + if (invoker != m_dropdown) { + m_dropdown->setSelected(this->getValue()); + } + m_dropdown->setEnabled(enable); } - else { - index = index > 0 ? index - 1 : options.size() - 1; + else if (m_input) { + if (invoker != m_input) { + m_input->setString(this->getValue()); + } + m_input->setEnabled(enable); } - this->setValue(options.at(index), static_cast(sender)); } StringSettingNodeV3* StringSettingNodeV3::create(std::shared_ptr setting, float width) { @@ -593,14 +572,13 @@ StringSettingNodeV3* StringSettingNodeV3::create(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; + if (!SettingValueNodeV3::init(setting, width)) return false; - auto labelBG = NineSlice::create("square02b_001.png", { 0, 0, 80, 80 }); + auto labelBG = NineSlice::create("square02b_001.png", {0, 0, 80, 80}); labelBG->setScale(.25f); - labelBG->setColor({ 0, 0, 0 }); + labelBG->setColor({0, 0, 0}); labelBG->setOpacity(90); - labelBG->setContentSize({ 420, 80 }); + labelBG->setContentSize({420, 80}); this->getButtonMenu()->addChildAtPosition(labelBG, Anchor::Center, ccp(-10, 0)); m_fileIcon = CCSprite::create(); @@ -624,7 +602,7 @@ bool FileSettingNodeV3::init(std::shared_ptr setting, float width void FileSettingNodeV3::updateState(CCNode* invoker) { // This is because people tend to put `"default": "Please pick a good file"` // which is clever and good UX but also a hack so I also need to hack to support that - const auto isTextualDefaultValue = [this, setting = this->getSetting()]() { + auto const isTextualDefaultValue = [this, setting = this->getSetting()]() { if (this->hasNonDefaultValue()) return false; if (utils::string::pathToString(setting->getDefaultValue()).size() > 20) return false; std::error_code ec; @@ -634,16 +612,22 @@ void FileSettingNodeV3::updateState(CCNode* invoker) { }(); SettingValueNodeV3::updateState(invoker); - m_fileIcon->setDisplayFrame(CCSpriteFrameCache::get()->spriteFrameByName( - this->getSetting()->isFolder() ? "folderIcon_001.png" : "file.png"_spr - )); + m_fileIcon->setDisplayFrame( + CCSpriteFrameCache::get()->spriteFrameByName( + this->getSetting()->isFolder() ? "folderIcon_001.png" : "file.png"_spr + ) + ); limitNodeSize(m_fileIcon, ccp(10, 10), 1.f, .1f); if (this->getValue().empty() || isTextualDefaultValue) { if (isTextualDefaultValue) { - m_nameLabel->setString(utils::string::pathToString(this->getSetting()->getDefaultValue()).c_str()); + m_nameLabel->setString( + utils::string::pathToString(this->getSetting()->getDefaultValue()).c_str() + ); } else { - m_nameLabel->setString(this->getSetting()->isFolder() ? "No Folder Selected" : "No File Selected"); + m_nameLabel->setString( + this->getSetting()->isFolder() ? "No Folder Selected" : "No File Selected" + ); } m_nameLabel->setColor(ccGRAY); m_nameLabel->setOpacity(155); @@ -666,14 +650,14 @@ void FileSettingNodeV3::onPickFile(CCObject*) { m_pickListener.spawn( file::pick( - this->getSetting()->isFolder() ? - file::PickMode::OpenFolder : - this->getSetting()->useSaveDialog() ? file::PickMode::SaveFile : file::PickMode::OpenFile, - { - // Prefer opening the current path directly if possible - this->getValue().empty() || !std::filesystem::exists(this->getValue().parent_path(), ec) - ? dirs::getGameDir() : this->getValue(), - this->getSetting()->getFilters().value_or(std::vector()) + this->getSetting()->isFolder() ? file::PickMode::OpenFolder : + this->getSetting()->useSaveDialog() ? file::PickMode::SaveFile : + file::PickMode::OpenFile, + {// Prefer opening the current path directly if possible + this->getValue().empty() || !std::filesystem::exists(this->getValue().parent_path(), ec) ? + dirs::getGameDir() : + this->getValue(), + this->getSetting()->getFilters().value_or(std::vector()) } ), [this](Result> path) { @@ -682,10 +666,9 @@ void FileSettingNodeV3::onPickFile(CCObject*) { } else if (path.isErr()) { FLAlertLayer::create( - "Failed", - fmt::format("Failed to pick file: {}", path.unwrapErr()), - "Ok" - )->show(); + "Failed", fmt::format("Failed to pick file: {}", path.unwrapErr()), "Ok" + ) + ->show(); } } ); @@ -704,8 +687,7 @@ FileSettingNodeV3* FileSettingNodeV3::create(std::shared_ptr sett // Color3BSettingNodeV3 bool Color3BSettingNodeV3::init(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; + if (!SettingValueNodeV3::init(setting, width)) return false; m_colorSprite = ColorChannelSprite::create(); m_colorSprite->setScale(.65f); @@ -731,9 +713,12 @@ void Color3BSettingNodeV3::updateState(CCNode* invoker) { void Color3BSettingNodeV3::onSelectColor(CCObject*) { auto popup = ColorPickPopup::create(this->getValue()); - popup->setCallback([this](ccColor4B const& color) { this->setValue(to3B(color), nullptr); }); + popup->setCallback([this](ccColor4B const& color) { + this->setValue(to3B(color), nullptr); + }); popup->show(); } + Color3BSettingNodeV3* Color3BSettingNodeV3::create(std::shared_ptr setting, float width) { auto ret = new Color3BSettingNodeV3(); if (ret->init(setting, width)) { @@ -747,8 +732,7 @@ Color3BSettingNodeV3* Color3BSettingNodeV3::create(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; + if (!SettingValueNodeV3::init(setting, width)) return false; m_colorSprite = ColorChannelSprite::create(); m_colorSprite->setScale(.65f); @@ -775,7 +759,9 @@ void Color4BSettingNodeV3::updateState(CCNode* invoker) { void Color4BSettingNodeV3::onSelectColor(CCObject*) { auto popup = ColorPickPopup::create(this->getValue()); - popup->setCallback([this](ccColor4B const& color) { this->setValue(color, nullptr); }); + popup->setCallback([this](ccColor4B const& color) { + this->setValue(color, nullptr); + }); popup->show(); } @@ -802,14 +788,13 @@ KeybindSettingNodeV3* KeybindSettingNodeV3::create(std::shared_ptr setting, float width) { - if (!SettingNodeV3::init(setting, width)) - return false; + if (!SettingNodeV3::init(setting, width)) return false; m_currentValue = setting->getValue(); this->getButtonMenu()->setLayout(RowLayout::create()->setAxisAlignment(AxisAlignment::End)); if (auto category = setting->getCategory()) { - const char* catSpr; + char const* catSpr; switch (*category) { default: case KeybindCategory::Editor: { @@ -825,12 +810,11 @@ bool KeybindSettingNodeV3::init(std::shared_ptr setting, float } break; } auto categoryLabel = createTagLabelWithIcon( - CCSprite::createWithSpriteFrameName(catSpr), "", + CCSprite::createWithSpriteFrameName(catSpr), + "", std::make_pair(ccWHITE, "keybinds-list-category-label"_cc3b) ); - categoryLabel->setLayoutOptions( - AxisLayoutOptions::create()->setScaleLimits(.1f, .35f) - ); + categoryLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(.1f, .35f)); this->getNameMenu()->addChild(categoryLabel); } @@ -853,14 +837,17 @@ void KeybindSettingNodeV3::updateState(CCNode* invoker) { for (auto& keybind : m_currentValue) { auto bspr = createKeybindButton(keybind); bspr->setScale(.5f); - auto button = CCMenuItemSpriteExtra::create(bspr, this, menu_selector(KeybindSettingNodeV3::onKeybind)); + auto button = + CCMenuItemSpriteExtra::create(bspr, this, menu_selector(KeybindSettingNodeV3::onKeybind)); button->setTag(index); buttonMenu->addChild(button); index += 1; } auto plusSprite = createGeodeButton("+", true); plusSprite->setScale(.5f); - auto plusButton = CCMenuItemSpriteExtra::create(plusSprite, this, menu_selector(KeybindSettingNodeV3::onKeybind)); + auto plusButton = CCMenuItemSpriteExtra::create( + plusSprite, this, menu_selector(KeybindSettingNodeV3::onKeybind) + ); buttonMenu->addChild(plusButton); buttonMenu->updateLayout(); @@ -872,7 +859,9 @@ void KeybindSettingNodeV3::updateState(CCNode* invoker) { auto moreSprite = createGeodeButton("...", true); moreSprite->setScale(.5f); - auto moreButton = CCMenuItemSpriteExtra::create(moreSprite, this, menu_selector(KeybindSettingNodeV3::onExtra)); + auto moreButton = CCMenuItemSpriteExtra::create( + moreSprite, this, menu_selector(KeybindSettingNodeV3::onExtra) + ); buttonMenu->addChild(moreButton); buttonMenu->updateLayout(); @@ -880,15 +869,11 @@ void KeybindSettingNodeV3::updateState(CCNode* invoker) { } void KeybindSettingNodeV3::onExtra(CCObject* sender) { - KeybindListPopup::create( - getSetting(), - m_currentValue, - [this](std::vector newKeybinds) { - if (m_currentValue == newKeybinds) return; - m_currentValue = std::move(newKeybinds); - this->markChanged(nullptr); - } - )->show(); + KeybindListPopup::create(getSetting(), m_currentValue, [this](std::vector newKeybinds) { + if (m_currentValue == newKeybinds) return; + m_currentValue = std::move(newKeybinds); + this->markChanged(nullptr); + })->show(); } void KeybindSettingNodeV3::onKeybind(CCObject* sender) { @@ -935,18 +920,16 @@ void KeybindSettingNodeV3::onResetToDefault() { // UnresolvedCustomSettingNodeV3 bool UnresolvedCustomSettingNodeV3::init(std::string_view key, Mod* mod, float width) { - if (!SettingNodeV3::init(nullptr, width)) - return false; + if (!SettingNodeV3::init(nullptr, width)) return false; m_mod = mod; this->setContentHeight(30); auto label = CCLabelBMFont::create( - (mod && mod->isLoaded() ? - fmt::format("Missing setting '{}'", key) : - fmt::format("Enable the Mod to Edit '{}'", key) - ).c_str(), + (mod && mod->isLoaded() ? fmt::format("Missing setting '{}'", key) : + fmt::format("Enable the Mod to Edit '{}'", key)) + .c_str(), "bigFont.fnt" ); label->setColor(mod && mod->isLoaded() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b); @@ -958,7 +941,9 @@ bool UnresolvedCustomSettingNodeV3::init(std::string_view key, Mod* mod, float w void UnresolvedCustomSettingNodeV3::updateState(CCNode* invoker) { SettingNodeV3::updateState(invoker); - this->getBG()->setColor(m_mod && m_mod->isLoaded() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b); + this->getBG()->setColor( + m_mod && m_mod->isLoaded() ? "mod-list-errors-found-2"_cc3b : "mod-list-gray"_cc3b + ); this->getBG()->setOpacity(75); } @@ -974,7 +959,9 @@ bool UnresolvedCustomSettingNodeV3::hasNonDefaultValue() const { void UnresolvedCustomSettingNodeV3::onResetToDefault() {} -UnresolvedCustomSettingNodeV3* UnresolvedCustomSettingNodeV3::create(std::string_view key, Mod* mod, float width) { +UnresolvedCustomSettingNodeV3* UnresolvedCustomSettingNodeV3::create( + std::string_view key, Mod* mod, float width +) { auto ret = new UnresolvedCustomSettingNodeV3(); if (ret->init(key, mod, width)) { ret->autorelease(); diff --git a/loader/src/ui/mods/settings/SettingNodeV3.hpp b/loader/src/ui/mods/settings/SettingNodeV3.hpp index adf1a6dc0..60a1d4a4a 100644 --- a/loader/src/ui/mods/settings/SettingNodeV3.hpp +++ b/loader/src/ui/mods/settings/SettingNodeV3.hpp @@ -1,12 +1,13 @@ #pragma once -#include #include #include #include +#include +#include #include +#include #include -#include #include using namespace geode::prelude; @@ -27,7 +28,9 @@ class TitleSettingNodeV3 : public SettingNodeV3 { public: // `setting` may be null here static TitleSettingNodeV3* create(std::shared_ptr setting, float width); - static TitleSettingNodeV3* create(ZStringView title, std::optional description, float width); + static TitleSettingNodeV3* create( + ZStringView title, std::optional description, float width + ); bool isCollapsed() const; void setCollapsed(bool collapsed); @@ -106,6 +109,7 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { auto range = max - min; return static_cast(std::clamp(static_cast(value - min) / range, 0.0, 1.0)); } + ValueType valueFromSlider(float num) { auto min = this->getSetting()->getMinValue().value_or(-100); auto max = this->getSetting()->getMaxValue().value_or(+100); @@ -119,8 +123,7 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { } bool init(std::shared_ptr setting, float width) { - if (!SettingValueNodeV3::init(setting, width)) - return false; + if (!SettingValueNodeV3::init(setting, width)) return false; m_bigArrowLeftBtnSpr = CCSprite::create(); m_bigArrowLeftBtnSpr->setCascadeColorEnabled(true); @@ -136,7 +139,9 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { m_bigArrowLeftBtn = CCMenuItemSpriteExtra::create( m_bigArrowLeftBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); - m_bigArrowLeftBtn->setUserObject(ObjWrapper::create(-setting->getBigArrowStepSize())); + m_bigArrowLeftBtn->setUserObject( + ObjWrapper::create(-setting->getBigArrowStepSize()) + ); m_bigArrowLeftBtn->setVisible(setting->isBigArrowsEnabled()); this->getButtonMenu()->addChildAtPosition(m_bigArrowLeftBtn, Anchor::Left, ccp(5, 0)); @@ -151,7 +156,9 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { m_input = TextInput::create(this->getButtonMenu()->getContentWidth() - 40, "Num"); m_input->setScale(.7f); - m_input->setCommonFilter(std::is_floating_point_v ? CommonFilter::Float : CommonFilter::Int); + m_input->setCommonFilter( + std::is_floating_point_v ? CommonFilter::Float : CommonFilter::Int + ); m_input->setCallback([this, setting](auto const& str) { this->setValue(numFromString(str).unwrapOr(setting->getDefaultValue()), m_input); }); @@ -189,7 +196,9 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { m_bigArrowRightBtn = CCMenuItemSpriteExtra::create( m_bigArrowRightBtnSpr, this, menu_selector(NumberSettingNodeV3::onArrow) ); - m_bigArrowRightBtn->setUserObject(ObjWrapper::create(setting->getBigArrowStepSize())); + m_bigArrowRightBtn->setUserObject( + ObjWrapper::create(setting->getBigArrowStepSize()) + ); m_bigArrowRightBtn->setVisible(setting->isBigArrowsEnabled()); this->getButtonMenu()->addChildAtPosition(m_bigArrowRightBtn, Anchor::Right, ccp(-5, 0)); @@ -248,9 +257,9 @@ class NumberSettingNodeV3 : public SettingValueNodeV3 { } void onArrow(CCObject* sender) { - auto value = this->getValue() + static_cast*>( - static_cast(sender)->getUserObject() - )->getValue(); + auto value = this->getValue() + + static_cast*>(static_cast(sender)->getUserObject()) + ->getValue(); if (auto min = this->getSetting()->getMinValue()) { value = std::max(*min, value); } @@ -292,12 +301,10 @@ using FloatSettingNodeV3 = NumberSettingNodeV3; class StringSettingNodeV3 : public SettingValueNodeV3 { protected: TextInput* m_input; - CCSprite* m_arrowLeftSpr = nullptr; - CCSprite* m_arrowRightSpr = nullptr; + geode::Dropdown* m_dropdown = nullptr; bool init(std::shared_ptr setting, float width); void updateState(CCNode* invoker) override; - void onArrow(CCObject* sender); public: static StringSettingNodeV3* create(std::shared_ptr setting, float width); @@ -358,6 +365,7 @@ class KeybindSettingNodeV3 : public SettingNodeV3 { bool hasUncommittedChanges() const override; bool hasNonDefaultValue() const override; void onResetToDefault() override; + public: static KeybindSettingNodeV3* create(std::shared_ptr setting, float width); diff --git a/loader/src/ui/nodes/Dropdown.cpp b/loader/src/ui/nodes/Dropdown.cpp new file mode 100644 index 000000000..f751feee2 --- /dev/null +++ b/loader/src/ui/nodes/Dropdown.cpp @@ -0,0 +1,383 @@ +#include "ui/mods/GeodeStyle.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace geode; +using namespace geode::cocos; +using namespace cocos2d; + +class DropdownOverlay; + +class Dropdown::Impl { +public: + Dropdown* m_self; + std::vector m_options; + Function m_callback; + size_t m_selectedIndex = 0; + float m_width; + bool m_enabled = true; + int m_savedZOrder = 0; + + CCLabelBMFont* m_label = nullptr; + NineSlice* m_bg = nullptr; + CCSprite* m_arrow = nullptr; + CCMenuItemSpriteExtra* m_button = nullptr; + CCMenu* m_menu = nullptr; + DropdownOverlay* m_overlay = nullptr; + + bool init( + float width, std::vector options, + Function callback + ); + + void updateLabel(); + void openOverlay(); + void closeOverlay(); + void selectOption(size_t index); + void setEnabled(bool enabled); +}; + +class DropdownOverlay : public CCLayerColor { + Dropdown::Impl* m_dropdown; + ScrollLayer* m_scrollLayer = nullptr; + CCRect m_panelRect; + + float m_itemHeight; + float m_itemWidth; + float m_panelPadding; + +public: + static DropdownOverlay* create(Dropdown::Impl* dropdown) { + auto ret = new DropdownOverlay(); + + if (ret->init(dropdown)) { + ret->autorelease(); + return ret; + } + + delete ret; + return nullptr; + } + + bool init(Dropdown::Impl* dropdown) { + if (!CCLayerColor::initWithColor({0, 0, 0, 0})) return false; + + m_dropdown = dropdown; + this->setTouchEnabled(true); + this->setKeypadEnabled(true); + + bool geodeTheme = isGeodeTheme(); + + auto winSize = CCDirector::get()->getWinSize(); + auto buttonWorldPos = m_dropdown->m_self->convertToWorldSpace(ccp(0, 0)); + auto buttonSize = m_dropdown->m_self->getContentSize(); + + float dropdownWidth = m_dropdown->m_width; + m_itemHeight = 28.f; + float itemSpacing = 2.f; + float totalListHeight = m_itemHeight * m_dropdown->m_options.size() + + itemSpacing * (m_dropdown->m_options.size() - 1); + m_panelPadding = 6.f; + + float maxPanelHeight = 200.f; + float naturalPanelHeight = totalListHeight + m_panelPadding * 2; + float panelHeight = std::min(naturalPanelHeight, maxPanelHeight); + + float screenMidY = winSize.height / 2.f; + float panelY; + if (buttonWorldPos.y + buttonSize.height / 2.f > screenMidY) { + panelY = buttonWorldPos.y - panelHeight - 2.f; + if (panelY < 5.f) panelY = 5.f; + } + else { + panelY = buttonWorldPos.y + buttonSize.height + 2.f; + if (panelY + panelHeight > winSize.height - 5.f) { + panelY = winSize.height - panelHeight - 5.f; + } + } + + float panelX = buttonWorldPos.x + buttonSize.width - dropdownWidth; + if (panelX + dropdownWidth > winSize.width - 5.f) { + panelX = winSize.width - dropdownWidth - 5.f; + } + if (panelX < 5.f) panelX = 5.f; + + m_panelRect = CCRect(panelX, panelY, dropdownWidth, panelHeight); + + auto panelBG = NineSlice::create(geodeTheme ? "GE_square02.png"_spr : "GJ_square02.png"); + panelBG->setID("dropdown-panel-bg"); + panelBG->setContentSize({dropdownWidth, panelHeight}); + panelBG->setAnchorPoint({0, 0}); + panelBG->setPosition(panelX, panelY); + this->addChild(panelBG); + + m_itemWidth = dropdownWidth - m_panelPadding * 2; + float scrollAreaWidth = m_itemWidth; + float scrollAreaHeight = panelHeight - m_panelPadding * 2; + + m_scrollLayer = ScrollLayer::create({scrollAreaWidth, scrollAreaHeight}, true, true); + m_scrollLayer->setAnchorPoint({0, 0}); + m_scrollLayer->setPosition(panelX + m_panelPadding, panelY + m_panelPadding); + this->addChild(m_scrollLayer); + + for (size_t i = 0; i < m_dropdown->m_options.size(); i++) { + float itemY = totalListHeight - (m_itemHeight + itemSpacing) * i - m_itemHeight; + + auto itemBG = NineSlice::createWithSpriteFrameName("tab-bg.png"_spr); + itemBG->setScale(.5f); + itemBG->setContentSize({m_itemWidth / .5f, m_itemHeight / .5f}); + + if (i == m_dropdown->m_selectedIndex) { + itemBG->setColor(to3B(ColorProvider::get()->color("mod-list-tab-selected-bg"_spr))); + } + else { + itemBG->setColor(to3B(ColorProvider::get()->color("mod-list-tab-deselected-bg"_spr))); + } + + auto label = CCLabelBMFont::create(m_dropdown->m_options[i].c_str(), "bigFont.fnt"); + label->setScale(0.35f / .5f); + label->setAnchorPoint({0.f, 0.5f}); + label->limitLabelWidth((m_itemWidth - 14.f) / .5f, 0.35f / .5f, 0.1f / .5f); + + if (i == m_dropdown->m_selectedIndex) { + label->setColor(ccWHITE); + } + else { + label->setColor(ccc3(200, 200, 200)); + } + + itemBG->addChildAtPosition(label, Anchor::Left, ccp(8.f / .5f, 0)); + + if (i == m_dropdown->m_selectedIndex) { + auto check = CCSprite::createWithSpriteFrameName("GJ_completesIcon_001.png"); + check->setScale(0.4f / .5f); + itemBG->addChildAtPosition(check, Anchor::Right, ccp(-10.f / .5f, 0)); + } + + size_t index = i; + auto btn = Button::createWithNode(itemBG, [this, index](Button*) { + m_dropdown->selectOption(index); + }); + btn->setContentSize({m_itemWidth, m_itemHeight}); + btn->setAnchorPoint({0, 0}); + btn->setPosition(0, itemY); + btn->setTouchPriority(-501); + btn->setAnimationType(Button::AnimationType::None); + m_scrollLayer->m_contentLayer->addChild(btn); + } + + m_scrollLayer->m_contentLayer->setContentSize({scrollAreaWidth, totalListHeight}); + m_scrollLayer->scrollToTop(); + + return true; + } + + void registerWithTouchDispatcher() override { + CCTouchDispatcher::get()->addTargetedDelegate(this, -500, true); + } + + bool ccTouchBegan(CCTouch* touch, CCEvent* event) override { + auto loc = touch->getLocation(); + + if (m_panelRect.containsPoint(loc)) { + return false; + } + + m_dropdown->closeOverlay(); + return true; + } + + void ccTouchEnded(CCTouch*, CCEvent*) override {} + + void ccTouchCancelled(CCTouch*, CCEvent*) override {} + + void ccTouchMoved(CCTouch*, CCEvent*) override {} + + void keyBackClicked() override { + m_dropdown->closeOverlay(); + } +}; + +bool Dropdown::Impl::init( + float width, std::vector options, Function callback +) { + m_width = width; + m_options = std::move(options); + m_callback = std::move(callback); + + bool geodeTheme = isGeodeTheme(); + + float height = 30.f; + m_self->setContentSize({width, height}); + m_self->setAnchorPoint({0.5f, 0.5f}); + + m_bg = NineSlice::createWithSpriteFrameName("tab-bg.png"_spr); + m_bg->setScale(.5f); + m_bg->setContentSize({width / .5f, height / .5f}); + m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-search-bg"_spr))); + m_self->addChildAtPosition(m_bg, Anchor::Center); + + m_arrow = CCSprite::createWithSpriteFrameName("navArrowBtn_001.png"); + m_arrow->setScale(0.25f); + m_arrow->setRotation(90.f); + m_self->addChildAtPosition(m_arrow, Anchor::Right, ccp(-12, 0)); + + m_label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label->setScale(0.35f); + m_label->setAnchorPoint({0.f, 0.5f}); + m_self->addChildAtPosition(m_label, Anchor::Left, ccp(8, 0)); + + auto clickArea = CCSprite::create(); + clickArea->setContentSize({width, height}); + clickArea->setOpacity(0); + m_button = CCMenuItemSpriteExtra::create(clickArea, m_self, menu_selector(Dropdown::onOpen)); + m_button->setContentSize({width, height}); + + m_menu = CCMenu::create(); + m_menu->setContentSize({width, height}); + m_menu->addChildAtPosition(m_button, Anchor::Center); + m_self->addChildAtPosition(m_menu, Anchor::Center); + + if (!m_options.empty()) { + updateLabel(); + } + + return true; +} + +void Dropdown::Impl::updateLabel() { + if (m_options.empty()) { + m_label->setString(""); + return; + } + m_label->setString(m_options[m_selectedIndex].c_str()); + m_label->limitLabelWidth(m_width - 30.f, 0.35f, 0.1f); +} + +void Dropdown::Impl::openOverlay() { + if (!m_enabled || m_overlay) return; + + auto scene = CCDirector::get()->getRunningScene(); + if (!scene) return; + + m_savedZOrder = m_self->getZOrder(); + m_self->setZOrder(9998); + + m_overlay = DropdownOverlay::create(this); + if (m_overlay) { + CCTouchDispatcher::get()->registerForcePrio(m_overlay, 2); + scene->addChild(m_overlay, 9999); + } +} + +void Dropdown::Impl::closeOverlay() { + if (m_overlay) { + CCTouchDispatcher::get()->unregisterForcePrio(m_overlay); + m_overlay->removeFromParentAndCleanup(true); + m_overlay = nullptr; + + m_self->setZOrder(m_savedZOrder); + } +} + +void Dropdown::Impl::selectOption(size_t index) { + if (index >= m_options.size()) return; + m_selectedIndex = index; + updateLabel(); + closeOverlay(); + if (m_callback) { + m_callback(m_options[m_selectedIndex], m_selectedIndex); + } +} + +void Dropdown::Impl::setEnabled(bool enabled) { + m_enabled = enabled; + m_button->setEnabled(enabled); + GLubyte opacity = enabled ? 255 : 155; + auto color = enabled ? ccWHITE : ccGRAY; + m_label->setOpacity(opacity); + m_label->setColor(color); + m_arrow->setOpacity(opacity); + m_arrow->setColor(color); + m_bg->setOpacity(enabled ? 255 : 155); +} + +Dropdown::Dropdown() : m_impl(std::make_unique()) { + m_impl->m_self = this; +} + +Dropdown::~Dropdown() { + if (m_impl->m_overlay) { + m_impl->closeOverlay(); + } +} + +void Dropdown::onOpen(CCObject*) { + m_impl->openOverlay(); +} + +bool Dropdown::init( + float width, std::vector options, Function callback +) { + if (!CCNode::init()) return false; + return m_impl->init(width, std::move(options), std::move(callback)); +} + +Dropdown* Dropdown::create( + float width, std::vector options, Function callback +) { + auto ret = new Dropdown(); + if (ret->init(width, std::move(options), std::move(callback))) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +void Dropdown::setSelected(size_t index) { + if (index < m_impl->m_options.size()) { + m_impl->m_selectedIndex = index; + m_impl->updateLabel(); + } +} + +void Dropdown::setSelected(std::string_view value) { + for (size_t i = 0; i < m_impl->m_options.size(); i++) { + if (m_impl->m_options[i] == value) { + m_impl->m_selectedIndex = i; + m_impl->updateLabel(); + return; + } + } +} + +size_t Dropdown::getSelectedIndex() const { + return m_impl->m_selectedIndex; +} + +std::string Dropdown::getSelectedValue() const { + if (m_impl->m_options.empty()) return ""; + return m_impl->m_options[m_impl->m_selectedIndex]; +} + +void Dropdown::setEnabled(bool enabled) { + m_impl->setEnabled(enabled); +} + +bool Dropdown::isEnabled() const { + return m_impl->m_enabled; +} + +void Dropdown::setItems(std::vector options) { + m_impl->m_options = std::move(options); + m_impl->m_selectedIndex = 0; + m_impl->updateLabel(); +}