diff --git a/loader/src/ui/mods/ModList.cpp b/loader/src/ui/mods/ModList.cpp index e397e176..b05ee182 100644 --- a/loader/src/ui/mods/ModList.cpp +++ b/loader/src/ui/mods/ModList.cpp @@ -19,7 +19,27 @@ bool ModList::init(ModListSource* src, CCSize const& size) { // This is half the normal size for separators ->setGap(2.5f) ); - this->addChildAtPosition(m_list, Anchor::Center, -m_list->getScaledContentSize() / 2); + this->addChildAtPosition(m_list, Anchor::Bottom, ccp(-m_list->getScaledContentSize().width / 2, 0)); + + m_searchMenu = CCMenu::create(); + m_searchMenu->ignoreAnchorPointForPosition(false); + m_searchMenu->setContentSize({ size.width, 30 }); + m_searchMenu->setAnchorPoint({ .5f, 1.f }); + + auto searchBG = CCLayerColor::create({ 83, 65, 109, 255 }); + searchBG->setContentSize(m_searchMenu->getContentSize()); + searchBG->ignoreAnchorPointForPosition(false); + m_searchMenu->addChildAtPosition(searchBG, Anchor::Center); + + auto searchInput = TextInput::create(size.width, "Search Mods"); + searchInput->setScale(.75f); + searchInput->setAnchorPoint({ 0, .5f }); + searchInput->setTextAlign(TextInputAlign::Left); + m_searchMenu->addChildAtPosition(searchInput, Anchor::Left, ccp(10, 0)); + + // Do not add search menu; that's handled by onSearch + + // Paging auto pageLeftMenu = CCMenu::create(); pageLeftMenu->setContentWidth(30.f); @@ -58,23 +78,7 @@ bool ModList::init(ModListSource* src, CCSize const& size) { ); this->addChildAtPosition(pageRightMenu, Anchor::Right, ccp(5, 0)); - auto pageLabelMenu = CCMenu::create(); - pageLabelMenu->setContentWidth(200.f); - pageLabelMenu->setAnchorPoint({ .5f, 1.f }); - - // Default text is so that the button gets a proper hitbox, since it's - // based on sprite content size - m_pageLabel = CCLabelBMFont::create("Page XX/XX", "bigFont.fnt"); - m_pageLabel->setAnchorPoint({ .5f, 1.f }); - m_pageLabel->setScale(.45f); - - m_pageLabelBtn = CCMenuItemSpriteExtra::create( - m_pageLabel, this, menu_selector(ModList::onGoToPage) - ); - pageLabelMenu->addChild(m_pageLabelBtn); - - pageLabelMenu->setLayout(RowLayout::create()); - this->addChildAtPosition(pageLabelMenu, Anchor::Bottom, ccp(0, -5)); + // Status m_statusContainer = CCMenu::create(); m_statusContainer->setScale(.5f); @@ -140,15 +144,13 @@ void ModList::onPromise(typename ModListSource::PageLoadEvent* event) { m_list->m_contentLayer->addChild(item); } this->updateSize(m_bigSize); - // Auto-grow the size of the list content - m_list->m_contentLayer->updateLayout(); // Scroll list to top auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight(); m_list->m_contentLayer->setPositionY(listTopScrollPos); // Update page UI - this->updatePageUI(); + this->updatePageNumber(); } else if (auto progress = event->getProgress()) { // todo: percentage in a loading bar @@ -163,8 +165,7 @@ void ModList::onPromise(typename ModListSource::PageLoadEvent* event) { } else if (auto rejected = event->getReject()) { this->showStatus(ModListErrorStatus(), rejected->message, rejected->details); - // todo: details - this->updatePageUI(true); + this->updatePageNumber(); } if (event->isFinally()) { @@ -173,23 +174,6 @@ void ModList::onPromise(typename ModListSource::PageLoadEvent* event) { } } -void ModList::updateSize(bool big) { - m_bigSize = big; - for (auto& node : CCArrayExt(m_list->m_contentLayer->getChildren())) { - if (auto item = typeinfo_cast(node)) { - item->updateSize(m_list->getContentWidth(), big); - } - } -} - -void ModList::onGoToPage(CCObject*) { - auto popup = SetTextPopup::create("", "Page", 5, "Go to Page", "OK", true, 60.f); - popup->m_delegate = this; - popup->m_input->m_allowedChars = getCommonFilterAllowedChars(CommonFilter::Uint); - popup->setID("go-to-page"_spr); - popup->show(); -} - void ModList::onPage(CCObject* sender) { // If no page count has been loaded yet, we can't do anything if (!m_source->getPageCount()) return; @@ -214,33 +198,76 @@ void ModList::onShowStatusDetails(CCObject*) { m_statusContainer->updateLayout(); } -void ModList::updatePageUI(bool hide) { +void ModList::activateSearch(bool activate) { + // Add the menu or remove it depending on new state + if (activate) { + if (!m_searchMenu->getParent()) { + this->addChildAtPosition(m_searchMenu, Anchor::Top); + } + } + else { + m_searchMenu->removeFromParent(); + } + + // Store old relative scroll position (ensuring no divide by zero happens) + auto oldPositionArea = m_list->m_contentLayer->getContentHeight() - m_list->getContentHeight(); + auto oldPosition = oldPositionArea > 0.f ? + m_list->m_contentLayer->getPositionY() / oldPositionArea : + -1.f; + + // Update list size to account for the search menu + // (giving a little bit of extra padding for it, the same size as gap) + m_list->setContentHeight( + activate ? + this->getContentHeight() - m_searchMenu->getContentHeight() - 2.5f : + this->getContentHeight() + ); + + // Preserve relative scroll position + m_list->m_contentLayer->setPositionY(( + m_list->m_contentLayer->getContentHeight() - m_list->getContentHeight() + ) * oldPosition); + + // ModList uses an anchor layout, so this puts the list in the right place + this->updateLayout(); +} + +void ModList::updateSize(bool big) { + m_bigSize = big; + + // Update all BaseModItems that are children of the list + // There may be non-BaseModItems there (like separators) so gotta be type-safe + for (auto& node : CCArrayExt(m_list->m_contentLayer->getChildren())) { + if (auto item = typeinfo_cast(node)) { + item->updateSize(m_list->getContentWidth(), big); + } + } + + // Store old relative scroll position (ensuring no divide by zero happens) + auto oldPositionArea = m_list->m_contentLayer->getContentHeight() - m_list->getContentHeight(); + auto oldPosition = oldPositionArea > 0.f ? + m_list->m_contentLayer->getPositionY() / oldPositionArea : + -1.f; + + // Auto-grow the size of the list content + m_list->m_contentLayer->updateLayout(); + + // Preserve relative scroll position + m_list->m_contentLayer->setPositionY(( + m_list->m_contentLayer->getContentHeight() - m_list->getContentHeight() + ) * oldPosition); +} + +void ModList::updatePageNumber() { auto pageCount = m_source->getPageCount(); // Hide if page count hasn't been loaded - if (!pageCount) { - hide = true; - } - m_pagePrevBtn->setVisible(!hide && m_page > 0); - m_pageNextBtn->setVisible(!hide && m_page < pageCount.value() - 1); - m_pageLabelBtn->setVisible(!hide); - if (pageCount > 0u) { - auto fmt = fmt::format( - "Page {}/{} (Total {})", - m_page + 1, pageCount.value(), m_source->getItemCount().value() - ); - m_pageLabel->setString(fmt.c_str()); - } -} + m_pagePrevBtn->setVisible(pageCount && m_page > 0); + m_pageNextBtn->setVisible(pageCount && m_page < pageCount.value() - 1); -void ModList::setTextPopupClosed(SetTextPopup* popup, gd::string value) { - if (popup->getID() == "go-to-page"_spr) { - if (auto res = numFromString(value)) { - size_t num = res.unwrap(); - // The page indices are 0-based but people think in 1-based - if (num > 0) num -= 1; - this->gotoPage(num); - } + // Notify container about page count update + if (m_pageUpdated) { + m_pageUpdated(); } } @@ -260,7 +287,7 @@ void ModList::gotoPage(size_t page, bool update) { // Do initial eager update on page UI (to prevent user spamming arrows // to access invalid pages) - this->updatePageUI(); + this->updatePageNumber(); } void ModList::showStatus(ModListStatus status, std::string const& message, std::optional const& details) { @@ -292,6 +319,14 @@ void ModList::showStatus(ModListStatus status, std::string const& message, std:: m_statusContainer->updateLayout(); } +void ModList::onPageUpdated(ModListPageUpdated listener) { + m_pageUpdated = listener; +} + +size_t ModList::getPage() const { + return m_page; +} + ModList* ModList::create(ModListSource* src, CCSize const& size) { auto ret = new ModList(); if (ret && ret->init(src, size)) { diff --git a/loader/src/ui/mods/ModList.hpp b/loader/src/ui/mods/ModList.hpp index 79eba50a..b71601a4 100644 --- a/loader/src/ui/mods/ModList.hpp +++ b/loader/src/ui/mods/ModList.hpp @@ -15,7 +15,9 @@ struct ModListProgressStatus { }; using ModListStatus = std::variant; -class ModList : public CCNode, public SetTextPopupDelegate { +using ModListPageUpdated = MiniFunction; + +class ModList : public CCNode { protected: Ref m_source; size_t m_page = 0; @@ -29,26 +31,28 @@ protected: ModListSource::PageLoadEventListener m_listener; CCMenuItemSpriteExtra* m_pagePrevBtn; CCMenuItemSpriteExtra* m_pageNextBtn; - CCMenuItemSpriteExtra* m_pageLabelBtn; - CCLabelBMFont* m_pageLabel; + Ref m_searchMenu; + ModListPageUpdated m_pageUpdated = nullptr; bool m_bigSize = false; bool init(ModListSource* src, CCSize const& size); void onPromise(ModListSource::PageLoadEvent* event); void onPage(CCObject*); - void onGoToPage(CCObject*); void onShowStatusDetails(CCObject*); - void setTextPopupClosed(SetTextPopup*, gd::string value) override; - - void updatePageUI(bool hide = false); - public: static ModList* create(ModListSource* src, CCSize const& size); + // poor man's delegate + void onPageUpdated(ModListPageUpdated listener); + size_t getPage() const; + void reloadPage(); void gotoPage(size_t page, bool update = false); void showStatus(ModListStatus status, std::string const& message, std::optional const& details = std::nullopt); + + void updatePageNumber(); void updateSize(bool big); + void activateSearch(bool activate); }; diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index f6aef8c7..9d161d89 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -129,6 +129,56 @@ bool ModsLayer::init() { mainTabs->setLayout(RowLayout::create()); this->addChild(mainTabs); + // Actions + + auto listActionsMenu = CCMenu::create(); + listActionsMenu->setContentHeight(100); + listActionsMenu->setAnchorPoint({ 1, 0 }); + listActionsMenu->setScale(.65f); + + m_bigSizeBtnSpr = CCSprite::create("GE_button_05.png"_spr); + auto bigSizeBtnTop = CCSprite::createWithSpriteFrameName("GJ_smallModeIcon_001.png"); + limitNodeSize(bigSizeBtnTop, m_bigSizeBtnSpr->getContentSize() * .65f, 2.f, .1f); + m_bigSizeBtnSpr->addChildAtPosition(bigSizeBtnTop, Anchor::Center); + auto bigSizeBtn = CCMenuItemSpriteExtra::create( + m_bigSizeBtnSpr, this, menu_selector(ModsLayer::onBigView) + ); + listActionsMenu->addChild(bigSizeBtn); + + m_searchBtnSpr = CCSprite::create("GE_button_05.png"_spr); + auto searchBtnTop = CCSprite::createWithSpriteFrameName("search.png"_spr); + limitNodeSize(searchBtnTop, m_searchBtnSpr->getContentSize() * .65f, 2.f, .1f); + m_searchBtnSpr->addChildAtPosition(searchBtnTop, Anchor::Center); + auto searchBtn = CCMenuItemSpriteExtra::create( + m_searchBtnSpr, this, menu_selector(ModsLayer::onSearch) + ); + listActionsMenu->addChild(searchBtn); + + listActionsMenu->setLayout(ColumnLayout::create()); + m_frame->addChildAtPosition(listActionsMenu, Anchor::Left, ccp(-5, 25)); + + m_pageMenu = CCMenu::create(); + m_pageMenu->setContentWidth(200.f); + m_pageMenu->setAnchorPoint({ 1.f, 1.f }); + m_pageMenu->setScale(.65f); + + m_pageLabel = CCLabelBMFont::create("", "goldFont.fnt"); + m_pageLabel->setAnchorPoint({ .5f, 1.f }); + m_pageMenu->addChild(m_pageLabel); + + m_goToPageBtn = CCMenuItemSpriteExtra::create( + CCSprite::createWithSpriteFrameName("gj_navDotBtn_on_001.png"), + this, menu_selector(ModsLayer::onGoToPage) + ); + m_pageMenu->addChild(m_goToPageBtn); + + m_pageMenu->setLayout( + RowLayout::create() + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ); + this->addChildAtPosition(m_pageMenu, Anchor::TopRight, ccp(-5, -5), false); + this->gotoTab(ModListSourceType::Installed); this->setKeypadEnabled(true); @@ -157,7 +207,8 @@ void ModsLayer::gotoTab(ModListSourceType type) { // Lazily create new list and add it to UI if (!m_lists.contains(src)) { - auto list = ModList::create(src, m_frame->getContentSize() - ccp(24, 0)); + auto list = ModList::create(src, m_frame->getContentSize() - ccp(30, 0)); + list->onPageUpdated(std::bind(&ModsLayer::updatePageNumber, this)); list->setPosition(m_frame->getPosition()); this->addChild(list); m_lists.emplace(src, list); @@ -166,16 +217,55 @@ void ModsLayer::gotoTab(ModListSourceType type) { else { this->addChild(m_lists.at(src)); } -} -void ModsLayer::onTab(CCObject* sender) { - this->gotoTab(static_cast(sender->getTag())); + // Update the state of the current list + m_lists.at(m_currentSource)->updateSize(m_bigView); + m_lists.at(m_currentSource)->activateSearch(m_showSearch); + m_lists.at(m_currentSource)->updatePageNumber(); } void ModsLayer::keyBackClicked() { this->onBack(nullptr); } +void ModsLayer::setTextPopupClosed(SetTextPopup* popup, gd::string value) { + if (popup->getID() == "go-to-page"_spr) { + if (auto res = numFromString(value)) { + size_t num = res.unwrap(); + // The page indices are 0-based but people think in 1-based + if (num > 0) num -= 1; + if (m_currentSource) { + m_lists.at(m_currentSource)->gotoPage(num); + } + } + } +} + +void ModsLayer::updatePageNumber() { + // Show current page number if the current source has total page count loaded + if (m_currentSource && m_currentSource->getPageCount()) { + auto page = m_lists.at(m_currentSource)->getPage() + 1; + auto count = m_currentSource->getPageCount().value(); + auto total = m_currentSource->getItemCount().value(); + + // Set the page count string + auto fmt = fmt::format("Page {}/{} (Total {})", page, count, total); + m_pageLabel->setString(fmt.c_str()); + + // Make page menu visible + m_pageMenu->setVisible(true); + m_pageMenu->updateLayout(); + } + // Hide page menu otherwise + else { + m_pageMenu->setVisible(false); + } +} + +void ModsLayer::onTab(CCObject* sender) { + this->gotoTab(static_cast(sender->getTag())); +} + void ModsLayer::onRefreshList(CCObject*) { m_lists.at(m_currentSource)->reloadPage(); } @@ -184,6 +274,41 @@ void ModsLayer::onBack(CCObject*) { CCDirector::get()->replaceScene(CCTransitionFade::create(.5f, MenuLayer::scene(false))); } +void ModsLayer::onGoToPage(CCObject*) { + auto popup = SetTextPopup::create("", "Page", 5, "Go to Page", "OK", true, 60.f); + popup->m_delegate = this; + popup->m_input->m_allowedChars = getCommonFilterAllowedChars(CommonFilter::Uint); + popup->setID("go-to-page"_spr); + popup->show(); +} + +void ModsLayer::onBigView(CCObject*) { + m_bigView = !m_bigView; + + // Make sure to avoid a crash + if (m_currentSource) { + m_lists.at(m_currentSource)->updateSize(m_bigView); + } + + // Update the background on the size button + m_bigSizeBtnSpr->setTexture(CCTextureCache::get()->addImage( + (m_bigView ? "GJ_button_02.png" : "GE_button_05.png"_spr), true + )); +} + +void ModsLayer::onSearch(CCObject*) { + m_showSearch = !m_showSearch; + + // Make sure to avoid a crash + if (m_currentSource) { + m_lists.at(m_currentSource)->activateSearch(m_showSearch); + } + // Update the background on the search button + m_searchBtnSpr->setTexture(CCTextureCache::get()->addImage( + (m_showSearch ? "GJ_button_02.png" : "GE_button_05.png"_spr), true + )); +} + ModsLayer* ModsLayer::create() { auto ret = new ModsLayer(); if (ret && ret->init()) { diff --git a/loader/src/ui/mods/ModsLayer.hpp b/loader/src/ui/mods/ModsLayer.hpp index 8b6466a0..fd071093 100644 --- a/loader/src/ui/mods/ModsLayer.hpp +++ b/loader/src/ui/mods/ModsLayer.hpp @@ -9,24 +9,37 @@ using namespace geode::prelude; -class ModsLayer : public CCLayer { +class ModsLayer : public CCLayer, public SetTextPopupDelegate { protected: CCNode* m_frame; std::vector m_tabs; ModListSource* m_currentSource = nullptr; std::unordered_map> m_lists; + CCSprite* m_bigSizeBtnSpr; + CCSprite* m_searchBtnSpr; + CCMenu* m_pageMenu; + CCLabelBMFont* m_pageLabel; + CCMenuItemSpriteExtra* m_goToPageBtn; + bool m_showSearch = false; + bool m_bigView = false; bool init(); void keyBackClicked() override; + void setTextPopupClosed(SetTextPopup*, gd::string value) override; + void onTab(CCObject* sender); + void onBigView(CCObject*); + void onSearch(CCObject*); + void onGoToPage(CCObject*); + void onBack(CCObject*); + void onRefreshList(CCObject*); + + void updatePageNumber(); public: static ModsLayer* create(); static ModsLayer* scene(); - void onBack(CCObject*); - void onRefreshList(CCObject*); - void gotoTab(ModListSourceType type); };