diff --git a/CHANGELOG.md b/CHANGELOG.md index a2df2e025e..e3b281b909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Feature #8580: Sort characters in the save loading menu Feature #8597: Lua: Add more built-in event handlers Feature #8629: Expose path grid data to Lua + Feature #8654: Allow lua world.createRecord to create NPC records 0.49.0 ------ diff --git a/CMakeLists.txt b/CMakeLists.txt index 57ebeefcfd..201cc600c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 87) +set(OPENMW_LUA_API_REVISION 90) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 849f13076d..39396f1273 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -303,6 +303,9 @@ bool Launcher::SettingsPage::loadSettings() loadSettingBool(Settings::gui().mColorTopicEnable, *changeDialogTopicsCheckBox); showOwnedComboBox->setCurrentIndex(Settings::game().mShowOwned); loadSettingBool(Settings::gui().mStretchMenuBackground, *stretchBackgroundCheckBox); + connect(controllerMenusCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotControllerMenusToggled); + loadSettingBool(Settings::gui().mControllerMenus, *controllerMenusCheckBox); + loadSettingBool(Settings::gui().mControllerTooltips, *controllerMenuTooltipsCheckBox); loadSettingBool(Settings::map().mAllowZooming, *useZoomOnMapCheckBox); loadSettingBool(Settings::game().mGraphicHerbalism, *graphicHerbalismCheckBox); scalingSpinBox->setValue(Settings::gui().mScalingFactor); @@ -496,6 +499,8 @@ void Launcher::SettingsPage::saveSettings() saveSettingBool(*changeDialogTopicsCheckBox, Settings::gui().mColorTopicEnable); saveSettingInt(*showOwnedComboBox, Settings::game().mShowOwned); saveSettingBool(*stretchBackgroundCheckBox, Settings::gui().mStretchMenuBackground); + saveSettingBool(*controllerMenusCheckBox, Settings::gui().mControllerMenus); + saveSettingBool(*controllerMenuTooltipsCheckBox, Settings::gui().mControllerTooltips); saveSettingBool(*useZoomOnMapCheckBox, Settings::map().mAllowZooming); saveSettingBool(*graphicHerbalismCheckBox, Settings::game().mGraphicHerbalism); Settings::gui().mScalingFactor.set(scalingSpinBox->value()); @@ -554,6 +559,11 @@ void Launcher::SettingsPage::slotAnimSourcesToggled(bool checked) } } +void Launcher::SettingsPage::slotControllerMenusToggled(bool checked) +{ + controllerMenuTooltipsCheckBox->setEnabled(checked); +} + void Launcher::SettingsPage::slotPostProcessToggled(bool checked) { postprocessTransparentPostpassCheckBox->setEnabled(checked); diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index d2bb80d86a..2c6eca477a 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -34,6 +34,7 @@ namespace Launcher void slotSkyBlendingToggled(bool checked); void slotShadowDistLimitToggled(bool checked); void slotDistantLandToggled(bool checked); + void slotControllerMenusToggled(bool checked); private: Config::GameSettings& mGameSettings; diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui index d7e1a4b3ab..a591c196c9 100644 --- a/apps/launcher/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -1433,6 +1433,29 @@ + + + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + + + Enable Controller Menus + + + + + + + false + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + + + Show Controller Tooltips By Default + + + diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 12fa6e811f..492807eb93 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -291,8 +291,7 @@ int CSMDoc::WriteCellCollectionStage::setup() return mDocument.getData().getCells().getSize(); } -void CSMDoc::WriteCellCollectionStage::writeReferences( - const std::deque& references, bool interior, unsigned int& newRefNum) +void CSMDoc::WriteCellCollectionStage::writeReferences(const std::deque& references, bool interior) { ESM::ESMWriter& writer = mState.getWriter(); @@ -304,6 +303,8 @@ void CSMDoc::WriteCellCollectionStage::writeReferences( { CSMWorld::CellRef refRecord = ref.get(); + const bool isLocal = refRecord.mRefNum.mContentFile == -1; + // -1 is the current file, saved indices are 1-based refRecord.mRefNum.mContentFile++; @@ -316,12 +317,7 @@ void CSMDoc::WriteCellCollectionStage::writeReferences( } ESM::RefId streamId = ESM::RefId::stringRefId(stream.str()); - if (refRecord.mNew || refRecord.mRefNum.mIndex == 0 - || (!interior && ref.mState == CSMWorld::RecordBase::State_ModifiedOnly && refRecord.mCell != streamId)) - { - refRecord.mRefNum.mIndex = newRefNum++; - } - else if ((refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) != streamId + if (!isLocal && (refRecord.mOriginalCell.empty() ? refRecord.mCell : refRecord.mOriginalCell) != streamId && !interior) { // An empty mOriginalCell is meant to indicate that it is the same as @@ -362,9 +358,6 @@ void CSMDoc::WriteCellCollectionStage::perform(int stage, Messages& messages) CSMWorld::Cell cellRecord = cell.get(); const bool interior = !cellRecord.mId.startsWith("#"); - // count new references and adjust RefNumCount accordingsly - unsigned int newRefNum = cellRecord.mRefNumCounter; - if (references != nullptr) { for (std::deque::const_iterator iter(references->begin()); iter != references->end(); ++iter) @@ -390,9 +383,6 @@ void CSMDoc::WriteCellCollectionStage::perform(int stage, Messages& messages) ESM::RefId::stringRefId(CSMWorld::CellCoordinates(refRecord.getCellIndex()).getId("")) != refRecord.mCell)) ++cellRecord.mRefNumCounter; - - if (refRecord.mRefNum.mIndex >= newRefNum) - newRefNum = refRecord.mRefNum.mIndex + 1; } } @@ -415,9 +405,9 @@ void CSMDoc::WriteCellCollectionStage::perform(int stage, Messages& messages) // write references if (references != nullptr) { - writeReferences(persistentRefs, interior, newRefNum); + writeReferences(persistentRefs, interior); cellRecord.saveTempMarker(writer, static_cast(references->size()) - persistentRefs.size()); - writeReferences(tempRefs, interior, newRefNum); + writeReferences(tempRefs, interior); } writer.endRecord(cellRecord.sRecordId); diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index 5423b8f504..1f86af10a2 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -167,7 +167,7 @@ namespace CSMDoc Document& mDocument; SavingState& mState; - void writeReferences(const std::deque& references, bool interior, unsigned int& newRefNum); + void writeReferences(const std::deque& references, bool interior); public: WriteCellCollectionStage(Document& document, SavingState& state); diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index fa42ee0f09..18fb73330c 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -273,15 +273,6 @@ namespace CSMWorld copy->mState = RecordBase::State_ModifiedOnly; setRecordId(destination, copy->get()); - if constexpr (std::is_same_v) - { - if (type == UniversalId::Type_Reference) - { - CSMWorld::CellRef* ptr = (CSMWorld::CellRef*)©->mModified; - ptr->mRefNum.mIndex = 0; - } - } - if constexpr (std::is_same_v) { copy->mModified.mStringId = copy->mModified.mId.getRefIdString(); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 124e697de8..b2b6cec245 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -175,6 +175,9 @@ void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool b ref.mIdNum = mNextId; // FIXME: fragile ref.mId = ESM::RefId::stringRefId(getNewId()); + if (!base && ref.mRefNum.mIndex > mHighestUsedRefNum) + mHighestUsedRefNum = ref.mRefNum.mIndex; + cache.emplace(ref.mRefNum, ref.mIdNum); auto record = std::make_unique>(); @@ -222,6 +225,11 @@ std::string CSMWorld::RefCollection::getNewId() return "ref#" + std::to_string(mNextId++); } +uint32_t CSMWorld::RefCollection::getNextRefNum() +{ + return ++mHighestUsedRefNum; +} + unsigned int CSMWorld::RefCollection::extractIdNum(std::string_view id) const { std::string::size_type separator = id.find_last_of('#'); @@ -283,6 +291,7 @@ void CSMWorld::RefCollection::appendBlankRecord(const ESM::RefId& id, UniversalI record->get().mId = id; record->get().mIdNum = extractIdNum(id.getRefIdString()); + record->get().mRefNum.mIndex = getNextRefNum(); Collection::appendRecord(std::move(record)); } @@ -298,15 +307,13 @@ void CSMWorld::RefCollection::cloneRecord( copy->get().mId = destination; copy->get().mIdNum = extractIdNum(destination.getRefIdString()); + copy->get().mRefNum.mIndex = getNextRefNum(); if (copy->get().mRefNum.hasContentFile()) { mRefIndex.insert(std::make_pair(static_cast*>(copy.get())->get().mIdNum, index)); copy->get().mRefNum.mContentFile = -1; - copy->get().mRefNum.mIndex = index; } - else - copy->get().mRefNum.mIndex = copy->get().mIdNum; insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() } diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index d3d200e6c2..f70167f922 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -1,6 +1,7 @@ #ifndef CSM_WOLRD_REFCOLLECTION_H #define CSM_WOLRD_REFCOLLECTION_H +#include #include #include #include @@ -40,9 +41,12 @@ namespace CSMWorld std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum int mNextId; + uint32_t mHighestUsedRefNum = 0; unsigned int extractIdNum(std::string_view id) const; + uint32_t getNextRefNum(); + int getIntIndex(unsigned int id) const; int searchId(unsigned int id) const; diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index e100a69a7c..b8bbc89c7b 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -1277,9 +1277,8 @@ void CSVRender::InstanceMode::cloneSelectedInstances() if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { macro.push(new CSMWorld::CloneCommand(referencesTable, objectTag->mObject->getReferenceId(), - "ref#" + std::to_string(referencesTable.rowCount()), CSMWorld::UniversalId::Type_Reference)); + document.getData().getReferences().getNewId(), CSMWorld::UniversalId::Type_Reference)); } - // getWorldspaceWidget().clearSelection(Mask_Reference); } void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight) diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 2a5ad2d18d..df6bf27eeb 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -44,7 +44,7 @@ add_openmw_dir (mwgui tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview draganddrop timeadvancer jailscreen itemchargeview keyboardnavigation textcolours statswatcher - postprocessorhud settings worlditemmodel itemtransfer + postprocessorhud settings worlditemmodel itemtransfer controllerbuttonsoverlay inventorytabsoverlay ) add_openmw_dir (mwdialogue diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index de6cf91f4e..2861ab88e9 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -19,6 +19,11 @@ namespace ESM class ESMWriter; } +namespace MyGUI +{ + class Widget; +} + namespace MWBase { /// \brief Interface for input manager (implemented in MWInput) @@ -61,6 +66,7 @@ namespace MWBase virtual float getControllerAxisValue(SDL_GameControllerAxis axis) const = 0; // returns value in range [-1, 1] virtual int getMouseMoveX() const = 0; virtual int getMouseMoveY() const = 0; + virtual void warpMouseToWidget(MyGUI::Widget* widget) = 0; /// Actions available for binding to keyboard buttons virtual const std::initializer_list& getActionKeySorting() = 0; @@ -77,6 +83,8 @@ namespace MWBase /// @return true if joystick, false otherwise virtual bool joystickLastUsed() = 0; virtual void setJoystickLastUsed(bool enabled) = 0; + virtual std::string getControllerButtonIcon(int button) = 0; + virtual std::string getControllerAxisIcon(int axis) = 0; virtual int countSavedGameRecords() const = 0; virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 037f719e6d..2783fd21d5 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -78,6 +78,8 @@ namespace MWGui class MessageBox; class PostProcessorHud; class SettingsWindow; + class HUD; + class WindowBase; enum ShowInDialogueMode { @@ -157,7 +159,9 @@ namespace MWBase virtual MWGui::CountDialog* getCountDialog() = 0; virtual MWGui::ConfirmationDialog* getConfirmationDialog() = 0; virtual MWGui::TradeWindow* getTradeWindow() = 0; + virtual MWGui::HUD* getHud() = 0; virtual MWGui::PostProcessorHud* getPostProcessorHud() = 0; + virtual std::vector getGuiModeWindows(MWGui::GuiMode mode) = 0; /// Make the player use an item, while updating GUI state accordingly virtual void useItem(const MWWorld::Ptr& item, bool force = false) = 0; @@ -381,6 +385,17 @@ namespace MWBase /// Same as viewer->getCamera()->getCullMask(), provided for consistency. virtual uint32_t getCullMask() = 0; + /// Return the window that should receive controller events + virtual MWGui::WindowBase* getActiveControllerWindow() = 0; + /// Return the available height for menus accounting for visible controller overlays + virtual int getControllerMenuHeight() = 0; + /// Cycle to the next window to receive controller events + virtual void cycleActiveControllerWindow(bool next) = 0; + virtual void setActiveControllerWindow(MWGui::GuiMode mode, int activeIndex) = 0; + virtual bool getControllerTooltip() const = 0; + virtual void setControllerTooltip(bool enabled) = 0; + virtual void updateControllerButtonsOverlay() = 0; + // Used in Lua bindings virtual const std::vector& getGuiModeStack() const = 0; virtual void setDisabledByLua(std::string_view windowId, bool disabled) = 0; diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index 5a6245fca0..66461e6e6a 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -91,6 +92,15 @@ namespace MWGui mFilterValue->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); mFilterType->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::switchFilterType); + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mX = "#{sCreate}"; + mControllerButtons.mY = "#{sMagicEffects}"; + mControllerButtons.mR3 = "#{sInfo}"; + } + center(); } @@ -165,7 +175,12 @@ namespace MWGui std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); if (mFilterType->getCaption() == ingredient) - mCurrentFilter = FilterType::ByName; + { + if (Settings::gui().mControllerMenus) + switchFilterType(mFilterType); + else + mCurrentFilter = FilterType::ByName; + } else mCurrentFilter = FilterType::ByEffect; updateFilters(); @@ -291,6 +306,9 @@ namespace MWGui initFilter(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); + + if (Settings::gui().mControllerMenus) + mItemView->setActiveControllerWindow(true); } void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) @@ -528,4 +546,86 @@ namespace MWGui if (currentCount > 1) mBrewCountEdit->setValue(currentCount - 1); } + + void AlchemyWindow::filterListButtonHandler(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A || arg.button == SDL_CONTROLLER_BUTTON_Y) + { + // Select the highlighted entry in the combo box and close it. List is closed by focusing on another + // widget. + size_t index = mFilterValue->getIndexSelected(); + mFilterValue->setIndexSelected(index); + onFilterChanged(mFilterValue, index); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); + + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + // Close the list without selecting anything. List is closed by focusing on another widget. + mFilterValue->clearIndexSelected(); + onFilterEdited(mFilterValue); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + + bool AlchemyWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + bool isFilterListOpen + = focus != nullptr && focus->getParent() != nullptr && focus->getParent()->getParent() == mFilterValue; + + if (isFilterListOpen) + { + // When the filter list combo box is open, send all inputs to it. + filterListButtonHandler(arg); + return true; + } + + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + // Remove active ingredients or close the window, starting with right-most slot. + for (int i = mIngredients.size() - 1; i >= 0; --i) + { + if (mIngredients[i]->isUserString("ToolTipType")) + { + onIngredientSelected(mIngredients[i]); + return true; + } + } + // If the ingredients list is empty, B closes the menu. + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + onCreateButtonClicked(mCreateButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_Y && mFilterValue->getItemCount() > 0) + { + // Magical effects/ingredients filter + if (mFilterValue->getIndexSelected() != MyGUI::ITEM_NONE) + { + // Clear the active filter + mFilterValue->clearIndexSelected(); + onFilterEdited(mFilterValue); + } + else + { + // Open the combo box to choose the a filter + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mFilterValue); + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + onDecreaseButtonTriggered(); + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + onIncreaseButtonTriggered(); + else + mItemView->onControllerButton(arg.button); + + return true; + } } diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 82e5c3f583..4c5faa86d4 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -99,6 +99,9 @@ namespace MWGui std::vector mApparatus; std::vector mIngredients; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void filterListButtonHandler(const SDL_ControllerButtonEvent& arg); }; } diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 3dfdd17627..97405e7cc0 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -50,15 +50,20 @@ namespace MWGui mBirthList->eventListSelectAccept += MyGUI::newDelegate(this, &BirthDialog::onAccept); mBirthList->eventListChangePosition += MyGUI::newDelegate(this, &BirthDialog::onSelectBirth); - MyGUI::Button* backButton; - getWidget(backButton, "BackButton"); - backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onBackClicked); + getWidget(mBackButton, "BackButton"); + mBackButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onBackClicked); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - okButton->setCaption( + getWidget(mOkButton, "OKButton"); + mOkButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); - okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mLStick = "#{sMouse}"; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + } updateBirths(); updateSpells(); @@ -70,8 +75,17 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) + { okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + mControllerButtons.mX = "#{sNext}"; + } + else if (Settings::gui().mControllerMenus) + { + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + mControllerButtons.mX = "#{sDone}"; + } else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); @@ -271,4 +285,30 @@ namespace MWGui mSpellArea->setVisibleVScroll(true); mSpellArea->setViewOffset(MyGUI::IntPoint(0, 0)); } + + bool BirthDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mBackButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkClicked(mOkButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mBirthList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mBirthList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + + return true; + } } diff --git a/apps/openmw/mwgui/birth.hpp b/apps/openmw/mwgui/birth.hpp index db9e997b6c..b41b1fbb9a 100644 --- a/apps/openmw/mwgui/birth.hpp +++ b/apps/openmw/mwgui/birth.hpp @@ -53,8 +53,12 @@ namespace MWGui MyGUI::ScrollView* mSpellArea; MyGUI::ImageBox* mBirthImage; std::vector mSpellItems; + MyGUI::Button* mBackButton; + MyGUI::Button* mOkButton; ESM::RefId mCurrentBirthId; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } #endif diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 47e85b1f4b..ef296534c4 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -105,6 +105,18 @@ namespace MWGui Styles mStyles; MyGUI::IntRect mRect; + void setColour(size_t section, size_t line, size_t run, const MyGUI::Colour& colour) const override + { + if (section >= mSections.size()) + return; + if (line >= mSections[section].mLines.size()) + return; + if (run >= mSections[section].mLines[line].mRuns.size()) + return; + + mSections[section].mLines[line].mRuns[run].mStyle->mNormalColour = colour; + } + virtual ~TypesetBookImpl() {} Range addContent(const BookTypesetter::Utf8Span& text) @@ -1288,6 +1300,12 @@ namespace MWGui void unadviseLinkClicked() override { mPageDisplay->mLinkClicked = std::function(); } + void setFocusItem(BookTypesetter::Style* itemStyle) override + { + mPageDisplay->mFocusItem = static_cast(itemStyle); + mPageDisplay->dirtyFocusItem(); + } + protected: void initialiseOverride() override { diff --git a/apps/openmw/mwgui/bookpage.hpp b/apps/openmw/mwgui/bookpage.hpp index d42fb4783f..bb85130b7f 100644 --- a/apps/openmw/mwgui/bookpage.hpp +++ b/apps/openmw/mwgui/bookpage.hpp @@ -31,6 +31,9 @@ namespace MWGui /// text combined prior to pagination. virtual std::pair getSize() const = 0; + /// Used to highlight journal indices + virtual void setColour(size_t section, size_t line, size_t run, const MyGUI::Colour& colour) const = 0; + virtual ~TypesetBook() = default; }; @@ -164,6 +167,8 @@ namespace MWGui /// Register the widget and associated sub-widget with MyGUI. Should be /// called once near the beginning of the program. static void registerMyGUIComponents(); + + virtual void setFocusItem(BookTypesetter::Style* itemStyle) = 0; }; } diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index ef875a18b9..056f1abc89 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -66,6 +66,10 @@ namespace MWGui MyGUI::IntCoord(0, 0, (64 - 7) * scale, mNextPageButton->getSize().height * scale)); } + mControllerButtons.mL1 = "#{sPrev}"; + mControllerButtons.mR1 = "#{sNext}"; + mControllerButtons.mB = "#{Interface:Close}"; + center(); } @@ -218,4 +222,26 @@ namespace MWGui } } + ControllerButtons* BookWindow::getControllerButtons() + { + mControllerButtons.mA = mTakeButton->getVisible() ? "#{sTake}" : ""; + return &mControllerButtons; + } + + bool BookWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mTakeButton->getVisible()) + onTakeButtonClicked(mTakeButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCloseButtonClicked(mCloseButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + prevPage(); + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + nextPage(); + + return true; + } } diff --git a/apps/openmw/mwgui/bookwindow.hpp b/apps/openmw/mwgui/bookwindow.hpp index 5a3dfdf584..7d3a0e30c7 100644 --- a/apps/openmw/mwgui/bookwindow.hpp +++ b/apps/openmw/mwgui/bookwindow.hpp @@ -18,8 +18,10 @@ namespace MWGui void setInventoryAllowed(bool allowed); void onResChange(int, int) override { center(); } + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; std::string_view getWindowIdForLua() const override { return "Book"; } + ControllerButtons* getControllerButtons() override; protected: void onNextPageButtonClicked(MyGUI::Widget* sender); diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index 839f0f5072..9573d22377 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "tooltips.hpp" @@ -46,15 +47,21 @@ namespace MWGui getWidget(mClassImage, "ClassImage"); getWidget(mClassName, "ClassName"); - MyGUI::Button* backButton; - getWidget(backButton, "BackButton"); - backButton->setCaptionWithReplacing("#{sMessageQuestionAnswer3}"); - backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onBackClicked); + getWidget(mBackButton, "BackButton"); + mBackButton->setCaptionWithReplacing("#{sMessageQuestionAnswer3}"); + mBackButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onBackClicked); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - okButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); - okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); + getWidget(mOkButton, "OKButton"); + mOkButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); + + if (Settings::gui().mControllerMenus) + { + mOkButton->setStateSelected(true); + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + } center(); } @@ -71,6 +78,30 @@ namespace MWGui center(); } + bool GenerateClassResultDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mOkButtonFocus) + onOkClicked(mOkButton); + else + onBackClicked(mBackButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mBackButton); + } + else if ((arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT && mOkButtonFocus) + || (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT && !mOkButtonFocus)) + { + mOkButtonFocus = !mOkButtonFocus; + mOkButton->setStateSelected(mOkButtonFocus); + mBackButton->setStateSelected(!mOkButtonFocus); + } + + return true; + } + // widget controls void GenerateClassResultDialog::onOkClicked(MyGUI::Widget* _sender) @@ -110,13 +141,18 @@ namespace MWGui getWidget(mClassImage, "ClassImage"); - MyGUI::Button* backButton; - getWidget(backButton, "BackButton"); - backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onBackClicked); + getWidget(mBackButton, "BackButton"); + mBackButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onBackClicked); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onOkClicked); + getWidget(mOkButton, "OKButton"); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onOkClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mLStick = "#{sMouse}"; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + } updateClasses(); updateStats(); @@ -128,8 +164,17 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) + { okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + mControllerButtons.mX = "#{sNext}"; + } + else if (Settings::gui().mControllerMenus) + { + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + mControllerButtons.mX = "#{sDone}"; + } else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); @@ -278,6 +323,32 @@ namespace MWGui setClassImage(mClassImage, mCurrentClassId); } + bool PickClassDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mBackButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkClicked(mOkButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mClassList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mClassList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + + return true; + } + /* InfoBoxDialog */ void InfoBoxDialog::fitToText(MyGUI::TextBox* widget) @@ -312,6 +383,7 @@ namespace MWGui InfoBoxDialog::InfoBoxDialog() : WindowModal("openmw_infobox.layout") + , mControllerFocus(0) { getWidget(mTextBox, "TextBox"); getWidget(mText, "Text"); @@ -319,6 +391,9 @@ namespace MWGui getWidget(mButtonBar, "ButtonBar"); center(); + + mDisableGamepadCursor = Settings::gui().mControllerMenus; + mControllerButtons.mA = "#{sSelect}"; } void InfoBoxDialog::setText(const std::string& str) @@ -353,6 +428,13 @@ namespace MWGui fitToText(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InfoBoxDialog::onButtonClicked); coord.top += button->getHeight(); + + if (Settings::gui().mControllerMenus && buttons.size() > 1 && this->mButtons.empty()) + { + // First button is selected by default + button->setStateSelected(true); + } + this->mButtons.push_back(button); } } @@ -382,6 +464,44 @@ namespace MWGui } } + bool InfoBoxDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mButtons.size()) + onButtonClicked(mButtons[mControllerFocus]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + if (mButtons.size() == 1) + onButtonClicked(mButtons[0]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mButtons.size() <= 1) + return true; + if (mButtons.size() == 2 && mControllerFocus == 0) + return true; + + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mButtons.size() <= 1) + return true; + if (mButtons.size() == 2 && mControllerFocus == 1) + return true; + + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + + return true; + } + /* ClassChoiceDialog */ ClassChoiceDialog::ClassChoiceDialog() @@ -405,6 +525,7 @@ namespace MWGui : WindowModal("openmw_chargen_create_class.layout") , mAffectedAttribute(nullptr) , mAffectedSkill(nullptr) + , mControllerFocus(2) { // Centre dialog center(); @@ -450,14 +571,25 @@ namespace MWGui MyGUI::Button* descriptionButton; getWidget(descriptionButton, "DescriptionButton"); descriptionButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionClicked); + mButtons.push_back(descriptionButton); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onBackClicked); + mButtons.push_back(backButton); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onOkClicked); + mButtons.push_back(okButton); + + if (Settings::gui().mControllerMenus) + { + okButton->setStateSelected(true); + mControllerButtons.mLStick = "#{sMouse}"; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + } // Set default skills, attributes @@ -545,13 +677,56 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) + { okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + mControllerButtons.mX = "#{sNext}"; + } + else if (Settings::gui().mControllerMenus) + { + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + mControllerButtons.mX = "#{sDone}"; + } else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } + bool CreateClassDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus == 0) + onDescriptionClicked(mButtons[0]); + else if (mControllerFocus == 1) + onBackClicked(mButtons[1]); + else + onOkClicked(mButtons[2]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mButtons[1]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkClicked(mButtons[2]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + return true; + } + // widget controls void CreateClassDialog::onDialogCancel() @@ -708,6 +883,9 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; } SelectSpecializationDialog::~SelectSpecializationDialog() {} @@ -739,6 +917,16 @@ namespace MWGui return true; } + bool SelectSpecializationDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelClicked(nullptr); + return true; + } + return false; + } + /* SelectAttributeDialog */ SelectAttributeDialog::SelectAttributeDialog() @@ -760,6 +948,7 @@ namespace MWGui widget->setAttributeId(attribute.mId); widget->eventClicked += MyGUI::newDelegate(this, &SelectAttributeDialog::onAttributeClicked); ToolTips::createAttributeToolTip(widget, attribute.mId); + mAttributeButtons.emplace_back(widget); } attributes->setVisibleVScroll(false); @@ -770,6 +959,16 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectAttributeDialog::onCancelClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mAttributeButtons.size() > 0) + mAttributeButtons[0]->setStateSelected(true); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } // widget controls @@ -791,6 +990,33 @@ namespace MWGui return true; } + bool SelectAttributeDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mAttributeButtons.size()) + onAttributeClicked(mAttributeButtons[mControllerFocus]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelClicked(nullptr); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + mAttributeButtons[mControllerFocus]->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus - 1, mAttributeButtons.size()); + mAttributeButtons[mControllerFocus]->setStateSelected(true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + mAttributeButtons[mControllerFocus]->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus + 1, mAttributeButtons.size()); + mAttributeButtons[mControllerFocus]->setStateSelected(true); + } + + return true; + } + /* SelectSkillDialog */ SelectSkillDialog::SelectSkillDialog() @@ -820,6 +1046,8 @@ namespace MWGui skillWidget->setSkillId(skill.mId); skillWidget->eventClicked += MyGUI::newDelegate(this, &SelectSkillDialog::onSkillClicked); ToolTips::createSkillToolTip(skillWidget, skill.mId); + mSkillButtons.emplace_back(skillWidget); + mNumSkillsPerSpecialization[skill.mData.mSpecialization]++; } for (const auto& [widget, coord] : specializations) { @@ -832,6 +1060,16 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mSkillButtons.size() > 0) + mSkillButtons[0]->setStateSelected(true); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } SelectSkillDialog::~SelectSkillDialog() {} @@ -855,6 +1093,75 @@ namespace MWGui return true; } + bool SelectSkillDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mSkillButtons.size()) + onSkillClicked(mSkillButtons[mControllerFocus]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelClicked(nullptr); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + mSkillButtons[mControllerFocus]->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus - 1, mSkillButtons.size()); + mSkillButtons[mControllerFocus]->setStateSelected(true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + mSkillButtons[mControllerFocus]->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus + 1, mSkillButtons.size()); + mSkillButtons[mControllerFocus]->setStateSelected(true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + mSkillButtons[mControllerFocus]->setStateSelected(false); + selectNextColumn(arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT ? -1 : 1); + mSkillButtons[mControllerFocus]->setStateSelected(true); + } + + return true; + } + + void SelectSkillDialog::selectNextColumn(int direction) + { + // Find which column (specialization) the current index is in. + size_t specialization = 0; + size_t nextSpecializationIndex = 0; + for (; specialization < mNumSkillsPerSpecialization.size(); ++specialization) + { + nextSpecializationIndex += mNumSkillsPerSpecialization[specialization]; + if (mControllerFocus < nextSpecializationIndex) + break; + } + + if (direction < 0) + { + if (mControllerFocus < mNumSkillsPerSpecialization[0]) + { + // Wrap around to the right column + for (size_t i = 0; i < mNumSkillsPerSpecialization.size() - 1; ++i) + mControllerFocus += mNumSkillsPerSpecialization[i]; + } + else + mControllerFocus -= mNumSkillsPerSpecialization[specialization]; + } + else + { + if (mControllerFocus + mNumSkillsPerSpecialization.back() >= mSkillButtons.size()) + { + // Wrap around to the left column + for (size_t i = 0; i < mNumSkillsPerSpecialization.size() - 1; ++i) + mControllerFocus -= mNumSkillsPerSpecialization[i]; + } + else + mControllerFocus += mNumSkillsPerSpecialization[specialization]; + } + } + /* DescriptionDialog */ DescriptionDialog::DescriptionDialog() @@ -873,6 +1180,8 @@ namespace MWGui // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); + + mControllerButtons.mA = "#{Interface:OK}"; } DescriptionDialog::~DescriptionDialog() {} @@ -904,4 +1213,13 @@ namespace MWGui imageBox->setImageTexture(classImage); } + bool DescriptionDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A || arg.button == SDL_CONTROLLER_BUTTON_B) + { + onOkClicked(nullptr); + return true; + } + return false; + } } diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index f89a0c7d88..5a769e15bd 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -42,6 +42,7 @@ namespace MWGui protected: void onButtonClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: void fitToText(MyGUI::TextBox* widget); @@ -50,6 +51,7 @@ namespace MWGui MyGUI::TextBox* mText; MyGUI::Widget* mButtonBar; std::vector mButtons; + size_t mControllerFocus; }; // Lets the player choose between 3 ways of creating a class @@ -92,10 +94,14 @@ namespace MWGui protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + bool mOkButtonFocus = true; private: MyGUI::ImageBox* mClassImage; MyGUI::TextBox* mClassName; + MyGUI::Button* mBackButton; + MyGUI::Button* mOkButton; ESM::RefId mCurrentClassId; }; @@ -140,11 +146,15 @@ namespace MWGui MyGUI::ImageBox* mClassImage; MyGUI::ListBox* mClassList; MyGUI::TextBox* mSpecializationName; + MyGUI::Button* mBackButton; + MyGUI::Button* mOkButton; Widgets::MWAttributePtr mFavoriteAttribute[2]; Widgets::MWSkillPtr mMajorSkill[5]; Widgets::MWSkillPtr mMinorSkill[5]; ESM::RefId mCurrentClassId; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; class SelectSpecializationDialog : public WindowModal @@ -173,6 +183,7 @@ namespace MWGui protected: void onSpecializationClicked(MyGUI::Widget* _sender); void onCancelClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: MyGUI::TextBox *mSpecialization0, *mSpecialization1, *mSpecialization2; @@ -206,6 +217,9 @@ namespace MWGui protected: void onAttributeClicked(Widgets::MWAttributePtr _sender); void onCancelClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; + std::vector mAttributeButtons; private: ESM::RefId mAttributeId; @@ -237,9 +251,15 @@ namespace MWGui protected: void onSkillClicked(Widgets::MWSkillPtr _sender); void onCancelClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; + std::vector mSkillButtons; private: ESM::RefId mSkillId; + std::array mNumSkillsPerSpecialization; + + void selectNextColumn(int direction); }; class DescriptionDialog : public WindowModal @@ -258,6 +278,7 @@ namespace MWGui protected: void onOkClicked(MyGUI::Widget* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: MyGUI::EditBox* mTextEdit; @@ -314,6 +335,7 @@ namespace MWGui private: MyGUI::EditBox* mEditName; MyGUI::TextBox* mSpecializationName; + std::vector mButtons; Widgets::MWAttributePtr mFavoriteAttribute0, mFavoriteAttribute1; std::array mMajorSkill; std::array mMinorSkill; @@ -329,6 +351,9 @@ namespace MWGui Widgets::MWAttributePtr mAffectedAttribute; Widgets::MWSkillPtr mAffectedSkill; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; }; } #endif diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 52fc4cc4ce..0972f8e57e 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -61,6 +63,11 @@ namespace MWGui mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CompanionWindow::onCloseButtonClicked); setCoord(200, 0, 600, 300); + + mControllerButtons.mA = "#{sTake}"; + mControllerButtons.mB = "#{Interface:Close}"; + mControllerButtons.mR3 = "#{sInfo}"; + mControllerButtons.mL2 = "#{sInventory}"; } void CompanionWindow::onItemSelected(int index) @@ -96,13 +103,12 @@ namespace MWGui name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); - - if (MyGUI::InputManager::getInstance().isAltPressed()) + if (Settings::gui().mControllerMenus || MyGUI::InputManager::getInstance().isAltPressed()) dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::transferItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); } - else if (MyGUI::InputManager::getInstance().isAltPressed()) + else if (Settings::gui().mControllerMenus || MyGUI::InputManager::getInstance().isAltPressed()) transferItem(nullptr, count); else dragItem(nullptr, count); @@ -243,4 +249,32 @@ namespace MWGui { mItemTransfer->removeTarget(*mItemView); } + + bool CompanionWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + int index = mItemView->getControllerFocus(); + if (index >= 0 && index < mItemView->getItemCount()) + onItemSelected(index); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCloseButtonClicked(mCloseButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK || arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN || arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + mItemView->onControllerButton(arg.button); + } + + return true; + } + + void CompanionWindow::setActiveControllerWindow(bool active) + { + mItemView->setActiveControllerWindow(active); + WindowBase::setActiveControllerWindow(active); + } } diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index 5e78d17334..1b5a772684 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -40,6 +40,12 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Companion"; } + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; + + MWGui::ItemView* getItemView() { return mItemView; } + CompanionItemModel* getModel() { return mModel; } + private: ItemView* mItemView; SortFilterItemModel* mSortModel; diff --git a/apps/openmw/mwgui/confirmationdialog.cpp b/apps/openmw/mwgui/confirmationdialog.cpp index 48b209f17e..f23237c896 100644 --- a/apps/openmw/mwgui/confirmationdialog.cpp +++ b/apps/openmw/mwgui/confirmationdialog.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -17,6 +19,13 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onOkButtonClicked); + + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{Interface:OK}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } void ConfirmationDialog::askForConfirmation(const std::string& message) @@ -35,6 +44,13 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); + if (Settings::gui().mControllerMenus) + { + mOkButtonFocus = true; + mOkButton->setStateSelected(true); + mCancelButton->setStateSelected(false); + } + center(); } @@ -56,4 +72,28 @@ namespace MWGui eventOkClicked(); } + + bool ConfirmationDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mOkButtonFocus) + onOkButtonClicked(mOkButton); + else + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if ((arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT && !mOkButtonFocus) + || (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT && mOkButtonFocus)) + { + mOkButtonFocus = !mOkButtonFocus; + mOkButton->setStateSelected(mOkButtonFocus); + mCancelButton->setStateSelected(!mOkButtonFocus); + } + + return true; + } } diff --git a/apps/openmw/mwgui/confirmationdialog.hpp b/apps/openmw/mwgui/confirmationdialog.hpp index 1344f2a501..9b26e3a3c9 100644 --- a/apps/openmw/mwgui/confirmationdialog.hpp +++ b/apps/openmw/mwgui/confirmationdialog.hpp @@ -27,6 +27,9 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onOkButtonClicked(MyGUI::Widget* _sender); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + bool mOkButtonFocus = true; }; } diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 937bab0851..126b6ea1d3 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" @@ -56,6 +58,12 @@ namespace MWGui mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ContainerWindow::onTakeAllButtonClicked); setCoord(200, 0, 600, 300); + + mControllerButtons.mA = "#{sTake}"; + mControllerButtons.mB = "#{Interface:Close}"; + mControllerButtons.mX = "#{sTakeAll}"; + mControllerButtons.mR3 = "#{sInfo}"; + mControllerButtons.mL2 = "#{sInventory}"; } void ContainerWindow::onItemSelected(int index) @@ -90,13 +98,12 @@ namespace MWGui name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); - - if (MyGUI::InputManager::getInstance().isAltPressed()) + if (Settings::gui().mControllerMenus || MyGUI::InputManager::getInstance().isAltPressed()) dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::transferItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::dragItem); } - else if (MyGUI::InputManager::getInstance().isAltPressed()) + else if (Settings::gui().mControllerMenus || MyGUI::InputManager::getInstance().isAltPressed()) transferItem(nullptr, count); else dragItem(nullptr, count); @@ -346,6 +353,49 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } + ControllerButtons* ContainerWindow::getControllerButtons() + { + mControllerButtons.mR1 = mDisposeCorpseButton->getVisible() ? "#{sDisposeofCorpse}" : ""; + return &mControllerButtons; + } + + bool ContainerWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + int index = mItemView->getControllerFocus(); + if (index >= 0 && index < mItemView->getItemCount()) + onItemSelected(index); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCloseButtonClicked(mCloseButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onTakeAllButtonClicked(mTakeButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + if (mDisposeCorpseButton->getVisible()) + onDisposeCorpseButtonClicked(mDisposeCorpseButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK || arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN || arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + mItemView->onControllerButton(arg.button); + } + + return true; + } + + void ContainerWindow::setActiveControllerWindow(bool active) + { + mItemView->setActiveControllerWindow(active); + WindowBase::setActiveControllerWindow(active); + } + void ContainerWindow::onFrame(float dt) { checkReferenceAvailable(); diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 86ded2ff75..d40507bd82 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -48,6 +48,13 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Container"; } + ControllerButtons* getControllerButtons() override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; + + MWGui::ItemView* getItemView() { return mItemView; } + ItemModel* getModel() { return mModel; } + private: Misc::NotNullPtr mDragAndDrop; Misc::NotNullPtr mItemTransfer; diff --git a/apps/openmw/mwgui/controllerbuttonsoverlay.cpp b/apps/openmw/mwgui/controllerbuttonsoverlay.cpp new file mode 100644 index 0000000000..1d937c1e63 --- /dev/null +++ b/apps/openmw/mwgui/controllerbuttonsoverlay.cpp @@ -0,0 +1,106 @@ +#include "controllerbuttonsoverlay.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" + +namespace MWGui +{ + static constexpr ControllerButtonsOverlay::ButtonDefinition sButtonDefs[] = { + { ControllerButtonsOverlay::Button::Button_A, "A", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_A }, &ControllerButtons::mA }, + { ControllerButtonsOverlay::Button::Button_B, "B", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_B }, &ControllerButtons::mB }, + { ControllerButtonsOverlay::Button::Button_Dpad, "Dpad", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_DPAD_UP }, &ControllerButtons::mDpad }, + { ControllerButtonsOverlay::Button::Button_L1, "L1", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_LEFTSHOULDER }, &ControllerButtons::mL1 }, + { ControllerButtonsOverlay::Button::Button_L2, "L2", ControllerButtonsOverlay::InputType_Axis, + { .mAxis = SDL_CONTROLLER_AXIS_TRIGGERLEFT }, &ControllerButtons::mL2 }, + { ControllerButtonsOverlay::Button::Button_L3, "L3", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_LEFTSTICK }, &ControllerButtons::mL3 }, + { ControllerButtonsOverlay::Button::Button_LStick, "LStick", ControllerButtonsOverlay::InputType_Axis, + { .mAxis = SDL_CONTROLLER_AXIS_LEFTY }, &ControllerButtons::mLStick }, + { ControllerButtonsOverlay::Button::Button_Menu, "Menu", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_BACK }, &ControllerButtons::mMenu }, + { ControllerButtonsOverlay::Button::Button_R1, "R1", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER }, &ControllerButtons::mR1 }, + { ControllerButtonsOverlay::Button::Button_R2, "R2", ControllerButtonsOverlay::InputType_Axis, + { .mAxis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT }, &ControllerButtons::mR2 }, + { ControllerButtonsOverlay::Button::Button_R3, "R3", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_RIGHTSTICK }, &ControllerButtons::mR3 }, + { ControllerButtonsOverlay::Button::Button_RStick, "RStick", ControllerButtonsOverlay::InputType_Axis, + { .mAxis = SDL_CONTROLLER_AXIS_RIGHTY }, &ControllerButtons::mRStick }, + { ControllerButtonsOverlay::Button::Button_View, "View", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_START }, &ControllerButtons::mView }, + { ControllerButtonsOverlay::Button::Button_X, "X", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_X }, &ControllerButtons::mX }, + { ControllerButtonsOverlay::Button::Button_Y, "Y", ControllerButtonsOverlay::InputType_Button, + { .mButton = SDL_CONTROLLER_BUTTON_Y }, &ControllerButtons::mY }, + }; + + ControllerButtonsOverlay::ControllerButtonsOverlay() + : WindowBase("openmw_controllerbuttons.layout") + { + MWBase::InputManager* inputMgr = MWBase::Environment::get().getInputManager(); + + for (size_t i = 0; i < mButtons.size(); i++) + { + getWidget(mButtons[i].mImage, "Btn" + sButtonDefs[i].mName + "Image"); + getWidget(mButtons[i].mText, "Btn" + sButtonDefs[i].mName + "Text"); + getWidget(mButtons[i].mHBox, "Btn" + sButtonDefs[i].mName); + + if (sButtonDefs[i].mInputType == InputType_Axis) + setIcon(mButtons[i].mImage, inputMgr->getControllerAxisIcon(sButtonDefs[i].mId.mAxis)); + else + setIcon(mButtons[i].mImage, inputMgr->getControllerButtonIcon(sButtonDefs[i].mId.mButton)); + } + + getWidget(mHBox, "ButtonBox"); + } + + int ControllerButtonsOverlay::getHeight() + { + MyGUI::Window* window = mMainWidget->castType(); + return window->getHeight(); + } + + void ControllerButtonsOverlay::setButtons(ControllerButtons* buttons) + { + int buttonCount = 0; + if (buttons != nullptr) + { + for (const auto& row : sButtonDefs) + buttonCount += updateButton(row.mButton, buttons->*(row.mField)); + + mHBox->notifyChildrenSizeChanged(); + } + + setVisible(buttonCount > 0); + } + + void ControllerButtonsOverlay::setIcon(MyGUI::ImageBox* image, const std::string& imagePath) + { + if (!imagePath.empty()) + image->setImageTexture(imagePath); + } + + int ControllerButtonsOverlay::updateButton(ControllerButtonsOverlay::Button button, const std::string& buttonStr) + { + if (buttonStr.empty()) + { + mButtons[button].mHBox->setVisible(false); + mButtons[button].mHBox->setUserString("Hidden", "true"); + return 0; + } + else + { + mButtons[button].mHBox->setVisible(true); + mButtons[button].mHBox->setUserString("Hidden", "false"); + mButtons[button].mText->setCaptionWithReplacing(buttonStr); + return 1; + } + } +} diff --git a/apps/openmw/mwgui/controllerbuttonsoverlay.hpp b/apps/openmw/mwgui/controllerbuttonsoverlay.hpp new file mode 100644 index 0000000000..cf38505bc2 --- /dev/null +++ b/apps/openmw/mwgui/controllerbuttonsoverlay.hpp @@ -0,0 +1,83 @@ +#ifndef MWGUI_CONTROLLERBUTTONSOVERLAY_H +#define MWGUI_CONTROLLERBUTTONSOVERLAY_H + +#include +#include + +#include + +#include "windowbase.hpp" + +namespace MWGui +{ + class ControllerButtonsOverlay : public WindowBase + { + public: + ControllerButtonsOverlay(); + + int getHeight(); + void setButtons(ControllerButtons* buttons); + + enum Button + { + Button_A = 0, + Button_B, + Button_Dpad, + Button_L1, + Button_L2, + Button_L3, + Button_LStick, + Button_Menu, + Button_R1, + Button_R2, + Button_R3, + Button_RStick, + Button_View, + Button_X, + Button_Y, + Button_Max, + }; + + enum InputType + { + InputType_Button, + InputType_Axis + }; + + struct ButtonDefinition + { + Button mButton; + std::string mName; + InputType mInputType; + union + { + SDL_GameControllerButton mButton; + SDL_GameControllerAxis mAxis; + } mId; + std::string MWGui::ControllerButtons::*mField; + }; + + private: + struct ButtonWidgets + { + MyGUI::ImageBox* mImage; + MyGUI::TextBox* mText; + Gui::HBox* mHBox; + + ButtonWidgets() + : mImage(nullptr) + , mText(nullptr) + , mHBox(nullptr) + { + } + }; + + std::array mButtons; + Gui::HBox* mHBox; + + void setIcon(MyGUI::ImageBox* image, const std::string& imagePath); + int updateButton(Button button, const std::string& buttonStr); + }; +} + +#endif diff --git a/apps/openmw/mwgui/countdialog.cpp b/apps/openmw/mwgui/countdialog.cpp index 2ca6657a17..0214d34e8a 100644 --- a/apps/openmw/mwgui/countdialog.cpp +++ b/apps/openmw/mwgui/countdialog.cpp @@ -27,6 +27,9 @@ namespace MWGui mSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &CountDialog::onSliderMoved); // make sure we read the enter key being pressed to accept multiple items mItemEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &CountDialog::onEnterKeyPressed); + + mControllerButtons.mA = "#{Interface:OK}"; + mControllerButtons.mB = "#{Interface:Cancel}"; } void CountDialog::openCountDialog(const std::string& item, const std::string& message, const int maxCount) @@ -38,7 +41,7 @@ namespace MWGui MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); mSlider->setScrollRange(maxCount); - mItemText->setCaption(item); + mItemText->setCaptionWithReplacing(item); int width = std::max(mItemText->getTextSize().width + 160, 320); setCoord(viewSize.width / 2 - width / 2, viewSize.height / 2 - mMainWidget->getHeight() / 2, width, @@ -54,6 +57,13 @@ namespace MWGui mItemEdit->setValue(maxCount); } + void CountDialog::setCount(int count) + { + count = std::clamp(count, 1, static_cast(mSlider->getScrollRange())); + mSlider->setScrollPosition(count - 1); + mItemEdit->setValue(count); + } + void CountDialog::onCancelButtonClicked(MyGUI::Widget* _sender) { setVisible(false); @@ -61,17 +71,16 @@ namespace MWGui void CountDialog::onOkButtonClicked(MyGUI::Widget* _sender) { - eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); - + // The order here matters. Hide the dialog first so the OK event tooltips reappear. setVisible(false); + eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); } // essentially duplicating what the OK button does if user presses // Enter key void CountDialog::onEnterKeyPressed(MyGUI::EditBox* _sender) { - eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); - setVisible(false); + onOkButtonClicked(_sender); // To do not spam onEnterKeyPressed() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); @@ -86,4 +95,22 @@ namespace MWGui { mItemEdit->setValue(_position + 1); } + + bool CountDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + onOkButtonClicked(mOkButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + setCount(1); + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + setCount(static_cast(mSlider->getScrollRange())); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + + return true; + } } diff --git a/apps/openmw/mwgui/countdialog.hpp b/apps/openmw/mwgui/countdialog.hpp index 70fc820899..0b1e53d167 100644 --- a/apps/openmw/mwgui/countdialog.hpp +++ b/apps/openmw/mwgui/countdialog.hpp @@ -15,6 +15,7 @@ namespace MWGui public: CountDialog(); void openCountDialog(const std::string& item, const std::string& message, const int maxCount); + void setCount(int count); /** Event : Ok button was clicked.\n signature : void method(MyGUI::Widget* sender, std::size_t count)\n @@ -34,6 +35,7 @@ namespace MWGui void onEditValueChanged(int value); void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); void onEnterKeyPressed(MyGUI::EditBox* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 6b1e007770..ec7d726570 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -88,6 +88,10 @@ namespace MWGui mBribe10Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe100Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe1000Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); + + mDisableGamepadCursor = Settings::gui().mControllerMenus; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; } void PersuasionDialog::adjustAction(MyGUI::Widget* action, int& totalHeight) @@ -144,6 +148,24 @@ namespace MWGui else mMainWidget->setSize(mInitialMainWidgetWidth, mMainWidget->getSize().height); + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + mButtons.clear(); + mButtons.push_back(mAdmireButton); + mButtons.push_back(mIntimidateButton); + mButtons.push_back(mTauntButton); + if (mBribe10Button->getEnabled()) + mButtons.push_back(mBribe10Button); + if (mBribe100Button->getEnabled()) + mButtons.push_back(mBribe100Button); + if (mBribe1000Button->getEnabled()) + mButtons.push_back(mBribe1000Button); + + for (size_t i = 0; i < mButtons.size(); i++) + mButtons[i]->setStateSelected(i == 0); + } + WindowModal::onOpen(); } @@ -152,6 +174,31 @@ namespace MWGui return mAdmireButton; } + bool PersuasionDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + onPersuade(mButtons[mControllerFocus]); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancel(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + + return true; + } + // -------------------------------------------------------------------------------------------------- Response::Response(std::string_view text, std::string_view title, bool needMargin) @@ -300,6 +347,9 @@ namespace MWGui // -------------------------------------------------------------------------------------------------- + // Morrowind uses 3 px invisible borders for padding topics + static constexpr int sVerticalPadding = 3; + DialogueWindow::DialogueWindow() : WindowBase("openmw_dialogue_window.layout") , mIsCompanion(false) @@ -335,6 +385,11 @@ namespace MWGui mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); + + mControllerScrollWidget = mHistory->getParent(); + mControllerButtons.mA = "#{sAsk}"; + mControllerButtons.mB = "#{sGoodbye}"; + mControllerButtons.mRStick = "#{sScrollup}"; } void DialogueWindow::onTradeComplete() @@ -488,6 +543,14 @@ namespace MWGui updateTopics(); updateTopicsPane(); // force update for new services + if (Settings::gui().mControllerMenus && !sameActor) + { + setControllerFocus(mControllerFocus, false); + // Reset focus to very top. Maybe change this to mTopicsList->getItemCount() - mKeywords.size()? + mControllerFocus = 0; + setControllerFocus(mControllerFocus, true); + } + updateDisposition(); restock(); } @@ -543,6 +606,10 @@ namespace MWGui void DialogueWindow::updateTopicsPane() { + std::string focusedTopic; + if (Settings::gui().mControllerMenus && mControllerFocus < static_cast(mTopicsList->getItemCount())) + focusedTopic = mTopicsList->getItemNameAt(mControllerFocus); + mTopicsList->clear(); for (auto& linkPair : mTopicLinks) mDeleteLater.push_back(std::move(linkPair.second)); @@ -588,22 +655,25 @@ namespace MWGui if (mTopicsList->getItemCount() > 0) mTopicsList->addSeparator(); - // Morrowind uses 3 px invisible borders for padding topics - constexpr int verticalPadding = 3; - for (const auto& keyword : mKeywords) { std::string topicId = Misc::StringUtils::lowerCase(keyword); - mTopicsList->addItem(keyword, verticalPadding); + mTopicsList->addItem(keyword, sVerticalPadding); auto t = std::make_unique(keyword); mKeywordSearch.seed(topicId, intptr_t(t.get())); t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); mTopicLinks[topicId] = std::move(t); + + if (keyword == focusedTopic) + mControllerFocus = mTopicsList->getItemCount() - 1; } redrawTopicsList(); updateHistory(); + + if (Settings::gui().mControllerMenus) + setControllerFocus(mControllerFocus, true); } void DialogueWindow::updateHistory(bool scrollbar) @@ -630,6 +700,8 @@ namespace MWGui // choices const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); mChoices = MWBase::Environment::get().getDialogueManager()->getChoices(); + mChoiceStyles.clear(); + mControllerChoice = -1; // -1 so you must make a choice (and can't accidentally pick the first answer) for (std::pair& choice : mChoices) { auto link = std::make_unique(choice.second); @@ -641,6 +713,7 @@ namespace MWGui BookTypesetter::Style* questionStyle = typesetter->createHotStyle( body, textColours.answer, textColours.answerOver, textColours.answerPressed, interactiveId); typesetter->write(questionStyle, to_utf8_span(choice.first)); + mChoiceStyles.push_back(questionStyle); } mGoodbye = MWBase::Environment::get().getDialogueManager()->isGoodbye(); @@ -850,4 +923,120 @@ namespace MWGui && actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"); } + void DialogueWindow::setControllerFocus(size_t index, bool focused) + { + // List is mTopicsList + "Goodbye" button below the list. + if (index > mTopicsList->getItemCount()) + return; + + if (index == mTopicsList->getItemCount()) + { + mGoodbyeButton->setStateSelected(focused); + } + else + { + const std::string& keyword = mTopicsList->getItemNameAt(mControllerFocus); + if (keyword.empty()) + return; + + MyGUI::Button* button = mTopicsList->getItemWidget(keyword); + button->setStateSelected(focused); + } + + if (focused) + { + // Scroll the side bar to keep the active item in view + int offset = 0; + for (int i = 6; i < static_cast(index); i++) + { + const std::string& keyword = mTopicsList->getItemNameAt(i); + if (keyword.empty()) + offset += 18 + sVerticalPadding * 2; + else + offset += mTopicsList->getItemWidget(keyword)->getHeight() + sVerticalPadding * 2; + } + mTopicsList->setViewOffset(-offset); + } + } + + bool DialogueWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mChoices.size() > 0) + { + if (mChoices.size() == 1) + onChoiceActivated(mChoices[0].second); + else if (mControllerChoice >= 0 && mControllerChoice < static_cast(mChoices.size())) + onChoiceActivated(mChoices[mControllerChoice].second); + } + else if (mControllerFocus == static_cast(mTopicsList->getItemCount())) + onGoodbyeActivated(); + else + onSelectListItem(mTopicsList->getItemNameAt(mControllerFocus), mControllerFocus); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B && mChoices.empty()) + { + onGoodbyeActivated(); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mChoices.size() > 0) + { + // In-dialogue choice (red text) + mControllerChoice = std::clamp(mControllerChoice - 1, 0, static_cast(mChoices.size()) - 1); + mHistory->setFocusItem(mChoiceStyles.at(mControllerChoice)); + } + else + { + // Number of items is mTopicsList.length+1 because of "Goodbye" button. + setControllerFocus(mControllerFocus, false); + if (mControllerFocus <= 0) + mControllerFocus = mTopicsList->getItemCount(); // "Goodbye" button + else if (mTopicsList->getItemNameAt(mControllerFocus - 1).empty()) + mControllerFocus -= 2; // Skip separator + else + mControllerFocus--; + setControllerFocus(mControllerFocus, true); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mChoices.size() > 0) + { + // In-dialogue choice (red text) + mControllerChoice = std::clamp(mControllerChoice + 1, 0, static_cast(mChoices.size()) - 1); + mHistory->setFocusItem(mChoiceStyles.at(mControllerChoice)); + } + else + { + // Number of items is mTopicsList.length+1 because of "Goodbye" button. + setControllerFocus(mControllerFocus, false); + if (mControllerFocus >= static_cast(mTopicsList->getItemCount())) + mControllerFocus = 0; + else if (mControllerFocus == static_cast(mTopicsList->getItemCount()) - 1) + mControllerFocus = mTopicsList->getItemCount(); // "Goodbye" button + else if (mTopicsList->getItemNameAt(mControllerFocus + 1).empty()) + mControllerFocus += 2; // Skip separator + else + mControllerFocus++; + setControllerFocus(mControllerFocus, true); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER && mChoices.size() == 0) + { + setControllerFocus(mControllerFocus, false); + mControllerFocus = std::max(mControllerFocus - 5, 0); + setControllerFocus(mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER && mChoices.size() == 0) + { + setControllerFocus(mControllerFocus, false); + mControllerFocus = std::min(mControllerFocus + 5, static_cast(mTopicsList->getItemCount())); + setControllerFocus(mControllerFocus, true); + } + + return true; + } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index 8a8b309401..6f03076e92 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -49,6 +49,9 @@ namespace MWGui MyGUI::Widget* getDefaultKeyFocus() override; + protected: + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + private: std::unique_ptr mCallback; @@ -65,6 +68,9 @@ namespace MWGui MyGUI::Widget* mActionsBox; Gui::AutoSizedTextBox* mGoldLabel; + std::vector mButtons; + int mControllerFocus = 0; + void adjustAction(MyGUI::Widget* action, int& totalHeight); void onCancel(MyGUI::Widget* sender); @@ -186,6 +192,8 @@ namespace MWGui void onReferenceUnavailable() override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + private: void updateDisposition(); void restock(); @@ -197,6 +205,7 @@ namespace MWGui std::vector> mHistoryContents; std::vector> mChoices; + std::vector mChoiceStyles; bool mGoodbye; std::vector> mLinks; @@ -220,6 +229,10 @@ namespace MWGui std::unique_ptr mCallback; std::unique_ptr mGreetingCallback; + void setControllerFocus(size_t index, bool focused); + int mControllerFocus = 0; + int mControllerChoice = -1; + void updateTopicFormat(); }; } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index af4a3e8ce3..d6a42f8ea0 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -59,6 +59,12 @@ namespace MWGui mBuyButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onBuyButtonClicked); mTypeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onTypeButtonClicked); mName->eventEditSelectAccept += MyGUI::newDelegate(this, &EnchantingDialog::onAccept); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mY = "#{OMWEngine:EnchantType}"; + mControllerButtons.mL1 = "#{sItem}"; + mControllerButtons.mR1 = "#{sSoulGem}"; } void EnchantingDialog::onOpen() @@ -152,6 +158,7 @@ namespace MWGui mEnchanting.setSelfEnchanting(false); mEnchanting.setEnchanter(ptr); mBuyButton->setCaptionWithReplacing("#{sBuy}"); + mControllerButtons.mX = "#{sBuy}"; mChanceLayout->setVisible(false); mPtr = ptr; setSoulGem(MWWorld::Ptr()); @@ -163,6 +170,7 @@ namespace MWGui mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(MWMechanics::getPlayer()); mBuyButton->setCaptionWithReplacing("#{sCreate}"); + mControllerButtons.mX = "#{sCreate}"; mChanceLayout->setVisible(Settings::game().mShowEnchantChance); mPtr = MWMechanics::getPlayer(); setSoulGem(ptr); @@ -382,4 +390,22 @@ namespace MWGui } } } + + bool EnchantingDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + onBuyButtonClicked(mBuyButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) + onTypeButtonClicked(mTypeButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + onSelectItem(mItemBox); + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + onSelectSoul(mSoulBox); + else + return EffectEditorBase::onControllerButtonEvent(arg); + + return true; + } } diff --git a/apps/openmw/mwgui/enchantingdialog.hpp b/apps/openmw/mwgui/enchantingdialog.hpp index 4c720a11fc..3cda350152 100644 --- a/apps/openmw/mwgui/enchantingdialog.hpp +++ b/apps/openmw/mwgui/enchantingdialog.hpp @@ -73,6 +73,8 @@ namespace MWGui MWMechanics::Enchanting mEnchanting; ESM::EffectList mEffectList; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index e5adce7624..4983836551 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -188,6 +188,17 @@ namespace MWGui mDrowningBar->setVisible(visible); } + void HUD::dropDraggedItem(float mouseX, float mouseY) + { + if (!mDragAndDrop->mIsOnDragAndDrop) + return; + + MWBase::Environment::get().getWorld()->breakInvisibility(MWMechanics::getPlayer()); + + WorldItemModel drop(mouseX, mouseY); + mDragAndDrop->drop(&drop, nullptr); + } + void HUD::onWorldClicked(MyGUI::Widget* _sender) { if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 8dd98628c4..1a1076ff68 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -61,6 +61,8 @@ namespace MWGui void clear() override; + void dropDraggedItem(float mouseX, float mouseY); + private: MyGUI::ProgressBar *mHealth, *mMagicka, *mStamina, *mEnemyHealth, *mDrowning; MyGUI::Widget* mHealthFrame; diff --git a/apps/openmw/mwgui/inventorytabsoverlay.cpp b/apps/openmw/mwgui/inventorytabsoverlay.cpp new file mode 100644 index 0000000000..d95079ab53 --- /dev/null +++ b/apps/openmw/mwgui/inventorytabsoverlay.cpp @@ -0,0 +1,63 @@ +#include "inventorytabsoverlay.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" + +namespace MWGui +{ + InventoryTabsOverlay::InventoryTabsOverlay() + : WindowBase("openmw_inventory_tabs.layout") + { + MyGUI::Button* tab; + static const char* kTabIds[] = { "TabMap", "TabInventory", "TabSpells", "TabStats" }; + + for (const char* id : kTabIds) + { + getWidget(tab, id); + tab->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryTabsOverlay::onTabClicked); + mTabs.push_back(tab); + } + + MyGUI::ImageBox* image; + getWidget(image, "BtnL2Image"); + image->setImageTexture( + MWBase::Environment::get().getInputManager()->getControllerAxisIcon(SDL_CONTROLLER_AXIS_TRIGGERLEFT)); + + getWidget(image, "BtnR2Image"); + image->setImageTexture( + MWBase::Environment::get().getInputManager()->getControllerAxisIcon(SDL_CONTROLLER_AXIS_TRIGGERRIGHT)); + } + + int InventoryTabsOverlay::getHeight() + { + MyGUI::Window* window = mMainWidget->castType(); + return window->getHeight(); + } + + void InventoryTabsOverlay::onTabClicked(MyGUI::Widget* sender) + { + if (!MWBase::Environment::get().getWindowManager()->getJournalAllowed()) + return; + + for (int i = 0; i < static_cast(mTabs.size()); i++) + { + if (mTabs[i] == sender) + { + MWBase::Environment::get().getWindowManager()->setActiveControllerWindow(GM_Inventory, i); + setTab(i); + break; + } + } + } + + void InventoryTabsOverlay::setTab(int index) + { + for (int i = 0; i < static_cast(mTabs.size()); i++) + mTabs[i]->setStateSelected(i == index); + } +} diff --git a/apps/openmw/mwgui/inventorytabsoverlay.hpp b/apps/openmw/mwgui/inventorytabsoverlay.hpp new file mode 100644 index 0000000000..26544aa310 --- /dev/null +++ b/apps/openmw/mwgui/inventorytabsoverlay.hpp @@ -0,0 +1,28 @@ +#ifndef MWGUI_INVENTORYTABSSOVERLAY_H +#define MWGUI_INVENTORYTABSSOVERLAY_H + +#include "windowbase.hpp" + +namespace MyGUI +{ + class Button; +} + +namespace MWGui +{ + class InventoryTabsOverlay : public WindowBase + { + public: + InventoryTabsOverlay(); + + int getHeight(); + void setTab(int index); + + private: + std::vector mTabs; + + void onTabClicked(MyGUI::Widget* sender); + }; +} + +#endif diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index da30fa86ff..97dbe64e76 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -19,6 +19,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -31,13 +32,17 @@ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" +#include "companionwindow.hpp" +#include "container.hpp" #include "countdialog.hpp" #include "draganddrop.hpp" +#include "hud.hpp" #include "inventoryitemmodel.hpp" #include "itemtransfer.hpp" #include "itemview.hpp" #include "settings.hpp" #include "sortfilteritemmodel.hpp" +#include "statswindow.hpp" #include "tooltips.hpp" #include "tradeitemmodel.hpp" #include "tradewindow.hpp" @@ -89,6 +94,7 @@ namespace MWGui , mPreview(std::make_unique(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) , mUpdateNextFrame(false) + , mPendingControllerAction(ControllerAction::None) { mPreviewTexture = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); @@ -129,6 +135,25 @@ namespace MWGui setGuiMode(mGuiMode); + if (Settings::gui().mControllerMenus) + { + // Show L1 and R1 buttons next to tabs + MyGUI::ImageBox* image; + getWidget(image, "BtnL1Image"); + image->setVisible(true); + image->setUserString("Hidden", "false"); + image->setImageTexture(MWBase::Environment::get().getInputManager()->getControllerButtonIcon( + SDL_CONTROLLER_BUTTON_LEFTSHOULDER)); + + getWidget(image, "BtnR1Image"); + image->setVisible(true); + image->setUserString("Hidden", "false"); + image->setImageTexture(MWBase::Environment::get().getInputManager()->getControllerButtonIcon( + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)); + + mControllerButtons.mR3 = "#{sInfo}"; + } + adjustPanes(); } @@ -208,9 +233,13 @@ namespace MWGui void InventoryWindow::setGuiMode(GuiMode mode) { + if (Settings::gui().mControllerMenus && mGuiMode == mode && isVisible()) + return; + mGuiMode = mode; const WindowSettingValues settings = getModeSettings(mGuiMode); - setPinButtonVisible(mode != GM_Container && mode != GM_Companion && mode != GM_Barter); + setPinButtonVisible( + mode != GM_Container && mode != GM_Companion && mode != GM_Barter && !Settings::gui().mControllerMenus); const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; @@ -306,18 +335,26 @@ namespace MWGui } } - if (count > 1 && !shift) + // Show a dialog to select a count of items, but not when using an item from the inventory + // in controller mode. In that case, we skip the dialog and just use one item immediately. + if (count > 1 && !shift && mPendingControllerAction != ControllerAction::Use) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); - std::string message = mTrading ? "#{sQuanityMenuMessage01}" : "#{sTake}"; + std::string message = "#{sTake}"; + if (mTrading || mPendingControllerAction == ControllerAction::Sell) + message = "#{sQuanityMenuMessage01}"; + else if (mPendingControllerAction == ControllerAction::Drop) + message = "#{sDrop}"; std::string name{ object.getClass().getName(object) }; name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); - - if (mTrading) + if (mTrading || mPendingControllerAction == ControllerAction::Sell) dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::sellItem); - else if (MyGUI::InputManager::getInstance().isAltPressed()) + else if (mPendingControllerAction == ControllerAction::Drop) + dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::dropItem); + else if (MyGUI::InputManager::getInstance().isAltPressed() + || mPendingControllerAction == ControllerAction::Transfer) dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::transferItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::dragItem); @@ -328,13 +365,35 @@ namespace MWGui { mSelectedItem = index; - if (mTrading) + if (mTrading || mPendingControllerAction == ControllerAction::Sell) sellItem(nullptr, count); - else if (MyGUI::InputManager::getInstance().isAltPressed()) + else if (mPendingControllerAction == ControllerAction::Use) + { + dragItem(nullptr, count); + if (item.mType == ItemStack::Type_Equipped) + { + // Drop the item on the inventory background to unequip it. + onBackgroundSelected(); + } + else + { + // Drop the item on the avatar to activate or equip it. + onAvatarClicked(nullptr); + // Drop any remaining items back in inventory. This is needed when clicking on a + // stack of items; we only want to use the first item. + onBackgroundSelected(); + } + } + else if (mPendingControllerAction == ControllerAction::Drop) + dropItem(nullptr, count); + else if (MyGUI::InputManager::getInstance().isAltPressed() + || mPendingControllerAction == ControllerAction::Transfer) transferItem(nullptr, count); else dragItem(nullptr, count); } + + mPendingControllerAction = ControllerAction::None; } void InventoryWindow::ensureSelectedItemUnequipped(int count) @@ -408,6 +467,19 @@ namespace MWGui notifyContentChanged(); } + void InventoryWindow::dropItem(MyGUI::Widget* sender, size_t count) + { + if (mGuiMode != MWGui::GM_Inventory) + return; + + if (!mDragAndDrop->mIsOnDragAndDrop) + dragItem(sender, count); + + // Drop the item into the gameworld + if (mDragAndDrop->mIsOnDragAndDrop) + MWBase::Environment::get().getWindowManager()->getHud()->dropDraggedItem(0.5f, 0.5f); + } + void InventoryWindow::updateItemView() { MWBase::Environment::get().getWindowManager()->updateSpellWindow(); @@ -523,7 +595,9 @@ namespace MWGui void InventoryWindow::onTitleDoubleClicked() { - if (MyGUI::InputManager::getInstance().isShiftPressed()) + if (Settings::gui().mControllerMenus && mGuiMode == GM_Inventory) + return; + else if (MyGUI::InputManager::getInstance().isShiftPressed()) toggleMaximized(); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); @@ -890,4 +964,151 @@ namespace MWGui const MyGUI::IntSize viewport = getPreviewViewportSize(); return osg::Vec2f(normalisedX * float(viewport.width - 1), (1.0 - normalisedY) * float(viewport.height - 1)); } + + ControllerButtons* InventoryWindow::getControllerButtons() + { + switch (mGuiMode) + { + case MWGui::GM_Companion: + mControllerButtons.mA = "#{OMWEngine:InventorySelect}"; + mControllerButtons.mB = "#{Interface:Close}"; + mControllerButtons.mX.clear(); + mControllerButtons.mR2 = "#{sCompanionShare}"; + break; + case MWGui::GM_Container: + mControllerButtons.mA = "#{OMWEngine:InventorySelect}"; + mControllerButtons.mB = "#{Interface:Close}"; + mControllerButtons.mX = "#{sTakeAll}"; + mControllerButtons.mR2 = "#{sContainer}"; + break; + case MWGui::GM_Barter: + mControllerButtons.mA = "#{sSell}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mX = "#{sOffer}"; + mControllerButtons.mR2 = "#{sBarter}"; + break; + case MWGui::GM_Inventory: + default: + mControllerButtons.mA = "#{sEquip}"; + mControllerButtons.mB = "#{sBack}"; + mControllerButtons.mX = "#{sDrop}"; + mControllerButtons.mR2.clear(); + break; + } + return &mControllerButtons; + } + + bool InventoryWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + mPendingControllerAction = ControllerAction::None; // Clear any pending controller actions + + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mGuiMode == MWGui::GM_Inventory) + mPendingControllerAction = ControllerAction::Use; + else if (mGuiMode == MWGui::GM_Companion || mGuiMode == MWGui::GM_Container) + mPendingControllerAction = ControllerAction::Transfer; + else if (mGuiMode == MWGui::GM_Barter) + mPendingControllerAction = ControllerAction::Sell; + + mItemView->onControllerButton(SDL_CONTROLLER_BUTTON_A); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + if (mGuiMode == MWGui::GM_Inventory) + { + mPendingControllerAction = ControllerAction::Drop; + mItemView->onControllerButton(SDL_CONTROLLER_BUTTON_A); + } + else if (mGuiMode == MWGui::GM_Container) + { + // Take all. Pass the button press to the container window and let it do the + // logic of taking all. + MWGui::ContainerWindow* containerWindow = static_cast( + MWBase::Environment::get().getWindowManager()->getGuiModeWindows(mGuiMode).at(0)); + containerWindow->onControllerButtonEvent(arg); + } + else if (mGuiMode == MWGui::GM_Barter) + { + // Offer. Pass the button press to the barter window and let it do the logic + // of making an offer. + MWGui::TradeWindow* tradeWindow = static_cast( + MWBase::Environment::get().getWindowManager()->getGuiModeWindows(mGuiMode).at(1)); + tradeWindow->onControllerButtonEvent(arg); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + if (mFilterAll->getStateSelected()) + onFilterChanged(mFilterMisc); + else if (mFilterWeapon->getStateSelected()) + onFilterChanged(mFilterAll); + else if (mFilterApparel->getStateSelected()) + onFilterChanged(mFilterWeapon); + else if (mFilterMagic->getStateSelected()) + onFilterChanged(mFilterApparel); + else if (mFilterMisc->getStateSelected()) + onFilterChanged(mFilterMagic); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + if (mFilterAll->getStateSelected()) + onFilterChanged(mFilterWeapon); + else if (mFilterWeapon->getStateSelected()) + onFilterChanged(mFilterApparel); + else if (mFilterApparel->getStateSelected()) + onFilterChanged(mFilterMagic); + else if (mFilterMagic->getStateSelected()) + onFilterChanged(mFilterMisc); + else if (mFilterMisc->getStateSelected()) + onFilterChanged(mFilterAll); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else + { + mItemView->onControllerButton(arg.button); + } + + return true; + } + + void InventoryWindow::setActiveControllerWindow(bool active) + { + if (!Settings::gui().mControllerMenus) + return; + + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + if (winMgr->getMode() == MWGui::GM_Inventory) + { + // Fill the screen, or limit to a certain size on large screens. Size chosen to + // match the size of the stats window. + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + int width = std::min(viewSize.width, 1600); + int height = std::min(winMgr->getControllerMenuHeight(), StatsWindow::getIdealHeight()); + int x = (viewSize.width - width) / 2; + int y = (viewSize.height - height) / 2; + + MyGUI::Window* window = mMainWidget->castType(); + window->setCoord(x, active ? y : viewSize.height + 1, width, height); + + adjustPanes(); + updatePreviewSize(); + } + + // Show L1 and R1 buttons next to tabs + MyGUI::Widget* image; + getWidget(image, "BtnL1Image"); + image->setVisible(active); + + getWidget(image, "BtnR1Image"); + image->setVisible(active); + + mItemView->setActiveControllerWindow(active); + WindowBase::setActiveControllerWindow(active); + } } diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index e245fe46ca..ce02a83c1b 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -77,8 +77,12 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Inventory"; } + ControllerButtons* getControllerButtons() override; + protected: void onTitleDoubleClicked() override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; private: Misc::NotNullPtr mDragAndDrop; @@ -126,9 +130,20 @@ namespace MWGui void onBackgroundSelected(); + enum class ControllerAction + { + None, + Use, + Transfer, + Sell, + Drop, + }; + ControllerAction mPendingControllerAction; + void sellItem(MyGUI::Widget* sender, std::size_t count); void dragItem(MyGUI::Widget* sender, std::size_t count); void transferItem(MyGUI::Widget* sender, std::size_t count); + void dropItem(MyGUI::Widget* sender, std::size_t count); void onWindowResize(MyGUI::Window* _sender); void onFilterChanged(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/itemchargeview.cpp b/apps/openmw/mwgui/itemchargeview.cpp index 02c3cc182c..59731717e1 100644 --- a/apps/openmw/mwgui/itemchargeview.cpp +++ b/apps/openmw/mwgui/itemchargeview.cpp @@ -1,16 +1,17 @@ #include "itemchargeview.hpp" +#include #include #include #include #include -#include -#include #include +#include #include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "../mwmechanics/spellutil.hpp" @@ -19,6 +20,8 @@ #include "itemmodel.hpp" #include "itemwidget.hpp" +#include "textcolours.hpp" +#include "windowbase.hpp" namespace MWGui { @@ -156,11 +159,20 @@ namespace MWGui mScrollView->setCanvasSize( MyGUI::IntSize(mScrollView->getWidth(), std::max(mScrollView->getHeight(), currentY))); mScrollView->setVisibleVScroll(true); + + if (Settings::gui().mControllerMenus) + updateControllerFocus(-1, mControllerFocus); } void ItemChargeView::resetScrollbars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + + if (Settings::gui().mControllerMenus) + { + updateControllerFocus(mControllerFocus, 0); + mControllerFocus = 0; + } } void ItemChargeView::setSize(const MyGUI::IntSize& value) @@ -224,4 +236,52 @@ namespace MWGui mScrollView->setViewOffset( MyGUI::IntPoint(0, static_cast(mScrollView->getViewOffset().top + rel * 0.3f))); } + + void ItemChargeView::onControllerButton(const unsigned char button) + { + if (mLines.empty()) + return; + + int prevFocus = mControllerFocus; + + if (button == SDL_CONTROLLER_BUTTON_A) + { + // Select the focused item, if any. + if (mControllerFocus >= 0 && mControllerFocus < static_cast(mLines.size())) + onIconClicked(mLines[mControllerFocus].mIcon); + } + else if (button == SDL_CONTROLLER_BUTTON_DPAD_UP) + mControllerFocus = wrap(mControllerFocus - 1, mLines.size()); + else if (button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + mControllerFocus = wrap(mControllerFocus + 1, mLines.size()); + + if (prevFocus != mControllerFocus) + updateControllerFocus(prevFocus, mControllerFocus); + } + + void ItemChargeView::updateControllerFocus(int prevFocus, int newFocus) + { + if (mLines.empty()) + return; + + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + + if (prevFocus >= 0 && prevFocus < static_cast(mLines.size())) + { + mLines[prevFocus].mText->setTextColour(textColours.normal); + mLines[prevFocus].mIcon->setControllerFocus(false); + } + + if (newFocus >= 0 && newFocus < static_cast(mLines.size())) + { + mLines[newFocus].mText->setTextColour(textColours.link); + mLines[newFocus].mIcon->setControllerFocus(true); + + // Scroll the list to keep the active item in view + if (newFocus <= 3) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(0, -55 * (newFocus - 3))); + } + } } diff --git a/apps/openmw/mwgui/itemchargeview.hpp b/apps/openmw/mwgui/itemchargeview.hpp index f7617d37eb..73bf9c3de2 100644 --- a/apps/openmw/mwgui/itemchargeview.hpp +++ b/apps/openmw/mwgui/itemchargeview.hpp @@ -52,6 +52,8 @@ namespace MWGui MyGUI::delegates::MultiDelegate eventItemClicked; + void onControllerButton(const unsigned char button); + private: struct Line { @@ -72,6 +74,9 @@ namespace MWGui std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; DisplayMode mDisplayMode; + + int mControllerFocus; + void updateControllerFocus(int prevFocus, int newFocus); }; } diff --git a/apps/openmw/mwgui/itemselection.cpp b/apps/openmw/mwgui/itemselection.cpp index 4fe40ce693..30d8dbfb60 100644 --- a/apps/openmw/mwgui/itemselection.cpp +++ b/apps/openmw/mwgui/itemselection.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "inventoryitemmodel.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" @@ -26,6 +28,10 @@ namespace MWGui cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemSelectionDialog::onCancelButtonClicked); center(); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mR3 = "#{sInfo}"; } bool ItemSelectionDialog::exit() @@ -40,6 +46,8 @@ namespace MWGui mSortModel = sortModel.get(); mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); + if (Settings::gui().mControllerMenus) + mItemView->setActiveControllerWindow(true); } void ItemSelectionDialog::setCategory(int category) @@ -65,4 +73,13 @@ namespace MWGui exit(); } + bool ItemSelectionDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(nullptr); + else + mItemView->onControllerButton(arg.button); + + return true; + } } diff --git a/apps/openmw/mwgui/itemselection.hpp b/apps/openmw/mwgui/itemselection.hpp index fe87d7e38a..83af6d4840 100644 --- a/apps/openmw/mwgui/itemselection.hpp +++ b/apps/openmw/mwgui/itemselection.hpp @@ -41,6 +41,7 @@ namespace MWGui void onSelectedItem(int index); void onCancelButtonClicked(MyGUI::Widget* sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index ff05a8b2d6..243837bb65 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -7,6 +7,12 @@ #include #include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" + #include "itemmodel.hpp" #include "itemwidget.hpp" @@ -15,6 +21,7 @@ namespace MWGui ItemView::ItemView() : mScrollView(nullptr) + , mControllerActiveWindow(false) { } @@ -46,13 +53,16 @@ namespace MWGui MyGUI::Widget* dragArea = mScrollView->getChildAt(0); int maxHeight = mScrollView->getHeight(); - int rows = maxHeight / 42; - rows = std::max(rows, 1); - bool showScrollbar = int(std::ceil(dragArea->getChildCount() / float(rows))) > mScrollView->getWidth() / 42; + mRows = std::max(maxHeight / 42, 1); + mItemCount = dragArea->getChildCount(); + bool showScrollbar = static_cast(std::ceil(mItemCount / float(mRows))) > mScrollView->getWidth() / 42; if (showScrollbar) + { maxHeight -= 18; + mRows = std::max(maxHeight / 42, 1); + } - for (unsigned int i = 0; i < dragArea->getChildCount(); ++i) + for (int i = 0; i < mItemCount; ++i) { MyGUI::Widget* w = dragArea->getChildAt(i); @@ -60,7 +70,7 @@ namespace MWGui y += 42; - if (y > maxHeight - 42 && i < dragArea->getChildCount() - 1) + if (y > maxHeight - 42 && i < mItemCount - 1) { x += 42; y = 0; @@ -70,6 +80,12 @@ namespace MWGui MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); + if (Settings::gui().mControllerMenus) + { + mControllerFocus = std::clamp(mControllerFocus, 0, mItemCount - 1); + updateControllerFocus(-1, mControllerFocus); + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mScrollView->setVisibleVScroll(false); @@ -122,6 +138,11 @@ namespace MWGui void ItemView::resetScrollBars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + if (Settings::gui().mControllerMenus) + { + updateControllerFocus(mControllerFocus, 0); + mControllerFocus = 0; + } } void ItemView::onSelectedItem(MyGUI::Widget* sender) @@ -165,4 +186,108 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } + void ItemView::setActiveControllerWindow(bool active) + { + mControllerActiveWindow = active; + + MWBase::Environment::get().getWindowManager()->setControllerTooltip( + active && Settings::gui().mControllerTooltips); + + if (active) + updateControllerFocus(-1, mControllerFocus); + else + updateControllerFocus(mControllerFocus, -1); + } + + void ItemView::onControllerButton(const unsigned char button) + { + if (!mItemCount) + return; + + int prevFocus = mControllerFocus; + + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: + // Select the focused item, if any. + if (mControllerFocus >= 0 && mControllerFocus < mItemCount) + { + MyGUI::Widget* dragArea = mScrollView->getChildAt(0); + onSelectedItem(dragArea->getChildAt(mControllerFocus)); + } + break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + // Toggle info tooltip + MWBase::Environment::get().getWindowManager()->setControllerTooltip( + !MWBase::Environment::get().getWindowManager()->getControllerTooltip()); + updateControllerFocus(-1, mControllerFocus); + break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + if (mControllerFocus % mRows == 0) + mControllerFocus = std::min(mControllerFocus + mRows - 1, mItemCount - 1); + else + mControllerFocus--; + break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + if (mControllerFocus % mRows == mRows - 1 || mControllerFocus == mItemCount - 1) + mControllerFocus -= mControllerFocus % mRows; + else + mControllerFocus++; + break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + if (mControllerFocus >= mRows) + mControllerFocus -= mRows; + break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + if (mControllerFocus + mRows < mItemCount) + mControllerFocus += mRows; + else if (mControllerFocus / mRows != (mItemCount - 1) / mRows) + mControllerFocus = mItemCount - 1; + break; + default: + return; + } + + if (prevFocus != mControllerFocus) + updateControllerFocus(prevFocus, mControllerFocus); + else + updateControllerFocus(-1, mControllerFocus); + } + + void ItemView::updateControllerFocus(int prevFocus, int newFocus) + { + MWBase::Environment::get().getWindowManager()->setCursorVisible( + !MWBase::Environment::get().getWindowManager()->getControllerTooltip()); + + if (!mItemCount) + return; + + MyGUI::Widget* dragArea = mScrollView->getChildAt(0); + + if (prevFocus >= 0 && prevFocus < mItemCount) + { + ItemWidget* prev = static_cast(dragArea->getChildAt(prevFocus)); + if (prev) + prev->setControllerFocus(false); + } + + if (mControllerActiveWindow && newFocus >= 0 && newFocus < mItemCount) + { + ItemWidget* focused = static_cast(dragArea->getChildAt(newFocus)); + if (focused) + { + focused->setControllerFocus(true); + + // Scroll the list to keep the active item in view + int column = newFocus / mRows; + if (column <= 3) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(-42 * (column - 3), 0)); + + if (MWBase::Environment::get().getWindowManager()->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(focused); + } + } + } } diff --git a/apps/openmw/mwgui/itemview.hpp b/apps/openmw/mwgui/itemview.hpp index aeed0a9113..6243618abc 100644 --- a/apps/openmw/mwgui/itemview.hpp +++ b/apps/openmw/mwgui/itemview.hpp @@ -33,6 +33,11 @@ namespace MWGui void resetScrollBars(); + void setActiveControllerWindow(bool active); + int getControllerFocus() { return mControllerFocus; } + int getItemCount() { return mItemCount; } + void onControllerButton(const unsigned char button); + private: void initialiseOverride() override; @@ -47,6 +52,12 @@ namespace MWGui std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; + + int mItemCount = 0; + int mRows; + int mControllerFocus = 0; + bool mControllerActiveWindow; + void updateControllerFocus(int prevFocus, int newFocus); }; } diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp index 05fff2d40f..5e47577b27 100644 --- a/apps/openmw/mwgui/itemwidget.cpp +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -58,6 +58,7 @@ namespace MWGui : mItem(nullptr) , mItemShadow(nullptr) , mFrame(nullptr) + , mControllerBorder(nullptr) , mText(nullptr) { } @@ -82,10 +83,22 @@ namespace MWGui assignWidget(mText, "Text"); if (mText) mText->setNeedMouseFocus(false); + if (Settings::gui().mControllerMenus) + { + assignWidget(mControllerBorder, "ControllerBorder"); + if (mControllerBorder) + mControllerBorder->setNeedMouseFocus(false); + } Base::initialiseOverride(); } + void ItemWidget::setControllerFocus(bool focus) + { + if (mControllerBorder) + mControllerBorder->setVisible(focus); + } + void ItemWidget::setCount(int count) { if (!mText) diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp index 63837ae92f..1191a23342 100644 --- a/apps/openmw/mwgui/itemwidget.hpp +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -40,12 +40,15 @@ namespace MWGui void setIcon(const MWWorld::Ptr& ptr); void setFrame(const std::string& frame, const MyGUI::IntCoord& coord); + void setControllerFocus(bool focus); + protected: void initialiseOverride() override; MyGUI::ImageBox* mItem; MyGUI::ImageBox* mItemShadow; MyGUI::ImageBox* mFrame; + MyGUI::ImageBox* mControllerBorder; MyGUI::TextBox* mText; std::string mCurrentIcon; diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index 86b45b4863..c698fd84d6 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -156,6 +156,13 @@ namespace MWGui return MWGui::BookTypesetter::Utf8Span(begin, begin + text.length()); } + int getCyrillicIndexPageCount() + { + // For small font size split alphabet to two columns (2x15 characers), for big font size split it to three + // colums (3x10 characters). + return Settings::gui().mFontSize < 18 ? 2 : 3; + } + typedef TypesetBook::Ptr book; JournalBooks::JournalBooks(JournalViewModel::Ptr model, ToUTF8::FromType encoding) @@ -169,7 +176,7 @@ namespace MWGui { BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* header = typesetter->createStyle({}, journalHeaderColour); BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); typesetter->write(header, to_utf8_span("You have no journal entries!")); @@ -184,7 +191,7 @@ namespace MWGui { BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* header = typesetter->createStyle({}, journalHeaderColour); BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); mModel->visitJournalEntries({}, AddJournalEntry(typesetter, body, header, true)); @@ -196,7 +203,7 @@ namespace MWGui { BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* header = typesetter->createStyle({}, journalHeaderColour); BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); mModel->visitTopicName(topicId, AddTopicName(typesetter, header)); @@ -212,7 +219,7 @@ namespace MWGui { BookTypesetter::Ptr typesetter = createTypesetter(); - BookTypesetter::Style* header = typesetter->createStyle({}, MyGUI::Colour(0.60f, 0.00f, 0.00f)); + BookTypesetter::Style* header = typesetter->createStyle({}, journalHeaderColour); BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::Black); AddQuestName addName(typesetter, header); @@ -277,13 +284,8 @@ namespace MWGui // for small font size split alphabet to two columns (2x15 characers), for big font size split it to three // colums (3x10 characters). - int sectionBreak = 10; - mIndexPagesCount = 3; - if (Settings::gui().mFontSize < 18) - { - sectionBreak = 15; - mIndexPagesCount = 2; - } + mIndexPagesCount = getCyrillicIndexPageCount(); + int sectionBreak = 30 / mIndexPagesCount; unsigned char ch[3] = { 0xd0, 0x90, 0x00 }; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 diff --git a/apps/openmw/mwgui/journalbooks.hpp b/apps/openmw/mwgui/journalbooks.hpp index 792edcc070..3d55135d9c 100644 --- a/apps/openmw/mwgui/journalbooks.hpp +++ b/apps/openmw/mwgui/journalbooks.hpp @@ -9,6 +9,9 @@ namespace MWGui { MWGui::BookTypesetter::Utf8Span to_utf8_span(std::string_view text); + int getCyrillicIndexPageCount(); + + const MyGUI::Colour journalHeaderColour = MyGUI::Colour(0.60f, 0.00f, 0.00f); struct JournalBooks { diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index 574c425d3e..adc05cf5b8 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -218,6 +218,16 @@ namespace } } + // Latin = 26 (13 + 13) + mIndexRowCount = 13; + bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); + if (isRussian) // Cyrillic is either (10 + 10 + 10) or (15 + 15) + mIndexRowCount = MWGui::getCyrillicIndexPageCount(); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mX = "#{OMWEngine:JournalQuests}"; + mControllerButtons.mY = "#{sTopics}"; + mQuestMode = false; mAllQuests = false; mOptionsMode = false; @@ -248,6 +258,9 @@ namespace } updateShowingPages(); + if (Settings::gui().mControllerMenus) + setControllerFocusedQuest(0); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(getWidget(CloseBTN)); } @@ -275,6 +288,8 @@ namespace updateShowingPages(); updateCloseJournalButton(); + + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void setOptionsMode() @@ -307,6 +322,8 @@ namespace notifyQuests(getWidget(QuestsList)); else notifyTopics(getWidget(TopicsList)); + + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void pushBook(Book& book, unsigned int page) @@ -338,6 +355,7 @@ namespace { setVisible(CloseBTN, mStates.size() < 2); setVisible(JournalBTN, mStates.size() >= 2); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void updateShowingPages() @@ -380,6 +398,8 @@ namespace setText(PageOneNum, page + 1); setText(PageTwoNum, page + 2); + + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void notifyKeyPress(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) @@ -407,6 +427,7 @@ namespace mTopicsMode = false; MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void notifyTopicSelected(const std::string& topicIdString, int id) @@ -439,6 +460,7 @@ namespace mOptionsMode = false; MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void notifyOptions(MyGUI::Widget* _sender) @@ -459,6 +481,9 @@ namespace getPage(LeftTopicIndex)->showPage(mTopicIndexBook, 0); getPage(RightTopicIndex)->showPage(mTopicIndexBook, 1); } + + if (Settings::gui().mControllerMenus) + setIndexControllerFocus(true); } void notifyJournal(MyGUI::Widget* _sender) @@ -467,6 +492,22 @@ namespace popBook(); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); + } + + void addControllerButtons(Gui::MWList* _list, size_t _selectedIndex) + { + mButtons.clear(); + for (size_t i = 0; i < _list->getItemCount(); i++) + { + MyGUI::Button* listItem = _list->getItemWidget(_list->getItemNameAt(i)); + if (listItem) + { + listItem->setTextColour( + mButtons.size() == _selectedIndex ? MWGui::journalHeaderColour : MyGUI::Colour::Black); + mButtons.push_back(listItem); + } + } } void notifyIndexLinkClicked(MWGui::TypesetBook::InteractiveId index) @@ -487,7 +528,14 @@ namespace list->adjustSize(); + if (Settings::gui().mControllerMenus) + { + setControllerFocusedQuest(0); + addControllerButtons(list, mSelectedQuest); + } + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void notifyTopics(MyGUI::Widget* _sender) @@ -503,6 +551,7 @@ namespace setVisible(ShowActiveBTN, false); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } struct AddNamesToList @@ -554,6 +603,12 @@ namespace list->sort(); list->adjustSize(); + if (Settings::gui().mControllerMenus) + { + addControllerButtons(list, mSelectedQuest); + setControllerFocusedQuest(MWGui::wrap(mSelectedQuest, mButtons.size())); + } + if (mAllQuests) { SetNamesInactive setInactive(list); @@ -561,6 +616,7 @@ namespace } MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void notifyShowAll(MyGUI::Widget* _sender) @@ -639,6 +695,240 @@ namespace } } } + + MWGui::ControllerButtons* getControllerButtons() override + { + mControllerButtons.mB = mOptionsMode || mStates.size() > 1 ? "#{sBack}" : "#{Interface:Close}"; + mControllerButtons.mL1 = mOptionsMode ? "" : "#{sPrev}"; + mControllerButtons.mR1 = mOptionsMode ? "" : "#{sNext}"; + mControllerButtons.mR3 = mOptionsMode && mQuestMode ? "#{OMWEngine:JournalShowAll}" : ""; + return &mControllerButtons; + } + + void setIndexControllerFocus(bool focused) + { + int col = mSelectedIndex / mIndexRowCount; + int row = mSelectedIndex % mIndexRowCount; + mTopicIndexBook->setColour(col, row, 0, focused ? MWGui::journalHeaderColour : MyGUI::Colour::Black); + } + + void moveSelectedIndex(int offset) + { + setIndexControllerFocus(false); + + int numChars = mEncoding == ToUTF8::WINDOWS_1251 ? 30 : 26; + int col = mSelectedIndex / mIndexRowCount; + + if (offset == -1) // Up + { + if (mSelectedIndex % mIndexRowCount == 0) + mSelectedIndex = (col * mIndexRowCount) + mIndexRowCount - 1; + else + mSelectedIndex--; + } + else if (offset == 1) // Down + { + if (mSelectedIndex % mIndexRowCount == mIndexRowCount - 1) + mSelectedIndex = col * mIndexRowCount; + else + mSelectedIndex++; + } + else + { + // mSelectedIndex is unsigned, so we have to be careful with our math. + if (offset < 0) + offset += numChars; + + mSelectedIndex = (mSelectedIndex + offset) % numChars; + } + + setIndexControllerFocus(true); + setText(PageOneNum, 1); // Redraw the list + } + + bool optionsModeButtonHandler(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) // A: Mouse click or Select + { + if (mQuestMode) + { + // Choose a quest + Gui::MWList* list = getWidget(QuestsList); + if (mSelectedQuest < list->getItemCount()) + notifyQuestClicked(list->getItemNameAt(mSelectedQuest), 0); + } + else if (mTopicsMode) + { + // Choose a topic + Gui::MWList* list = getWidget(TopicsList); + if (mSelectedQuest < list->getItemCount()) + notifyTopicSelected(list->getItemNameAt(mSelectedQuest), 0); + } + else + { + // Choose an index. Cyrillic capital A is a 0xd090 in UTF-8. + // Words can not be started with characters 26 or 28. + int russianOffset = 0xd090; + if (mSelectedIndex >= 26) + russianOffset++; + if (mSelectedIndex >= 27) + russianOffset++; // 27, not 28, because of skipping char 26 + bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); + notifyIndexLinkClicked(isRussian ? mSelectedIndex + russianOffset : mSelectedIndex + 'A'); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) // B: Back + { + // Hide the options overlay + notifyCancel(getWidget(CancelBTN)); + mQuestMode = false; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) // X: Quests + { + if (mQuestMode) + { + // Hide the quest overlay if visible + notifyCancel(getWidget(CancelBTN)); + mQuestMode = false; + } + else + { + // Show the quest overlay if viewing the topics list + notifyQuests(getWidget(QuestsBTN)); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) // Y: Topics + { + if (!mQuestMode) + { + // Hide the topics overlay if visible + notifyCancel(getWidget(CancelBTN)); + } + else + { + // Show the topics overlay if viewing the quest list + notifyTopics(getWidget(TopicsBTN)); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK && mQuestMode) // R3: Show All/Some + { + if (mAllQuests) + notifyShowActive(getWidget(ShowActiveBTN)); + else + notifyShowAll(getWidget(ShowAllBTN)); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mQuestMode || mTopicsMode) + { + if (mButtons.size() <= 1) + return true; + + // Scroll through the list of quests or topics + setControllerFocusedQuest(MWGui::wrap(mSelectedQuest - 1, mButtons.size())); + } + else + moveSelectedIndex(-1); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mQuestMode || mTopicsMode) + { + if (mButtons.size() <= 1) + return true; + + // Scroll through the list of quests or topics + setControllerFocusedQuest(MWGui::wrap(mSelectedQuest + 1, mButtons.size())); + } + else + moveSelectedIndex(1); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT && !mQuestMode && !mTopicsMode) + moveSelectedIndex(-mIndexRowCount); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT && !mQuestMode && !mTopicsMode) + moveSelectedIndex(mIndexRowCount); + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER && (mQuestMode || mTopicsMode)) + { + // Scroll up 5 items in the list of quests or topics + setControllerFocusedQuest(std::max(static_cast(mSelectedQuest) - 5, 0)); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER && (mQuestMode || mTopicsMode)) + { + // Scroll down 5 items in the list of quests or topics + setControllerFocusedQuest(std::min(mSelectedQuest + 5, mButtons.size() - 1)); + } + + return true; + } + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override + { + // If the topics or quest list is open, it should handle the buttons. + if (mOptionsMode) + return optionsModeButtonHandler(arg); + + if (arg.button == SDL_CONTROLLER_BUTTON_A) + return false; + else if (arg.button == SDL_CONTROLLER_BUTTON_B) // B: Back + { + if (mStates.size() > 1) + { + // Pop the current book. If in quest mode, reopen the quest list. + notifyJournal(getWidget(JournalBTN)); + if (mQuestMode) + { + notifyOptions(getWidget(OptionsBTN)); + notifyQuests(getWidget(QuestsBTN)); + } + } + else + { + // Close the journal window + notifyClose(getWidget(CloseBTN)); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) // X: Quests + { + // Show the quest overlay + notifyOptions(getWidget(OptionsBTN)); + if (!mQuestMode) + notifyQuests(getWidget(QuestsBTN)); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) // Y: Topics + { + // Show the topics overlay + notifyOptions(getWidget(OptionsBTN)); + if (mQuestMode) + notifyTopics(getWidget(TopicsBTN)); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT || arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + notifyPrevPage(getWidget(PrevPageBTN)); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT + || arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + notifyNextPage(getWidget(NextPageBTN)); + + return true; + } + + void setControllerFocusedQuest(size_t index) + { + size_t listSize = mButtons.size(); + if (mSelectedQuest < listSize) + mButtons[mSelectedQuest]->setTextColour(MyGUI::Colour::Black); + + mSelectedQuest = index; + if (mSelectedQuest < listSize) + { + mButtons[mSelectedQuest]->setTextColour(MWGui::journalHeaderColour); + + // Scroll the list to keep the active item in view + Gui::MWList* list = getWidget(mQuestMode ? QuestsList : TopicsList); + int offset = 0; + for (int i = 4; i < static_cast(mSelectedQuest); i++) + offset += mButtons[i]->getHeight(); + list->setViewOffset(-offset); + } + } }; } diff --git a/apps/openmw/mwgui/journalwindow.hpp b/apps/openmw/mwgui/journalwindow.hpp index f0f394156c..6122bf70c7 100644 --- a/apps/openmw/mwgui/journalwindow.hpp +++ b/apps/openmw/mwgui/journalwindow.hpp @@ -31,6 +31,16 @@ namespace MWGui void setVisible(bool newValue) override = 0; std::string_view getWindowIdForLua() const override { return "Journal"; } + + size_t mIndexRowCount; + + std::vector mButtons; + size_t mSelectedQuest = 0; + size_t mSelectedIndex = 0; + void moveSelectedIndex(int offset); + void setIndexControllerFocus(bool focused); + void setControllerFocusedQuest(size_t index); + bool optionsModeButtonHandler(const SDL_ControllerButtonEvent& arg); }; } diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 87f2db55a5..708fc7b02f 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -48,13 +49,12 @@ namespace MWGui { const auto& store = MWBase::Environment::get().getESMStore()->get(); - const size_t perCol - = static_cast(std::ceil(store.getSize() / static_cast(std::size(sColumnOffsets)))); + mPerCol = static_cast(std::ceil(store.getSize() / static_cast(std::size(sColumnOffsets)))); size_t i = 0; for (const ESM::Attribute& attribute : store) { - const int offset = sColumnOffsets[i / perCol]; - const int row = static_cast(i % perCol); + const int offset = sColumnOffsets[i / mPerCol]; + const int row = static_cast(i % mPerCol); Widgets widgets; widgets.mMultiplier = mAssignWidget->createWidget( "SandTextVCenter", { offset, 20 * row, 100, 20 }, MyGUI::Align::Default); @@ -72,12 +72,13 @@ namespace MWGui widgets.mButton->setCaption(attribute.mName); widgets.mValue = hbox->createWidget("SandText", {}, MyGUI::Align::Default); mAttributeWidgets.emplace(attribute.mId, widgets); + mAttributeButtons.emplace_back(widgets.mButton); ++i; } mAssignWidget->setVisibleVScroll(false); mAssignWidget->setCanvasSize(MyGUI::IntSize( - mAssignWidget->getWidth(), std::max(mAssignWidget->getHeight(), static_cast(20 * perCol)))); + mAssignWidget->getWidth(), std::max(mAssignWidget->getHeight(), static_cast(20 * mPerCol)))); mAssignWidget->setVisibleVScroll(true); mAssignWidget->setViewOffset(MyGUI::IntPoint()); } @@ -90,6 +91,15 @@ namespace MWGui mCoins.push_back(image); } + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mX = "#{sDone}"; + mOkButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + } + center(); } @@ -217,6 +227,13 @@ namespace MWGui center(); + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + for (size_t i = 0; i < mAttributeButtons.size(); i++) + setControllerFocus(mAttributeButtons, i, i == 0); + } + // Play LevelUp Music MWBase::Environment::get().getSoundManager()->streamMusic(MWSound::triumphMusic, MWSound::MusicType::Normal); } @@ -363,4 +380,44 @@ namespace MWGui return ret; } + + bool LevelupDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mAttributeButtons.size()) + onAttributeClicked(mAttributeButtons[mControllerFocus]); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Gold Up")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkButtonClicked(mOkButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + setControllerFocus(mAttributeButtons, mControllerFocus, false); + if (mControllerFocus % mPerCol == 0) + mControllerFocus += mPerCol - 1; + else + mControllerFocus--; + setControllerFocus(mAttributeButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + setControllerFocus(mAttributeButtons, mControllerFocus, false); + if (mControllerFocus % mPerCol == mPerCol - 1) + mControllerFocus -= mPerCol - 1; + else + mControllerFocus++; + setControllerFocus(mAttributeButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + setControllerFocus(mAttributeButtons, mControllerFocus, false); + mControllerFocus = (mControllerFocus + mPerCol) % mAttributeButtons.size(); + setControllerFocus(mAttributeButtons, mControllerFocus, true); + } + + return true; + } } diff --git a/apps/openmw/mwgui/levelupdialog.hpp b/apps/openmw/mwgui/levelupdialog.hpp index 486390679b..70bd2839f2 100644 --- a/apps/openmw/mwgui/levelupdialog.hpp +++ b/apps/openmw/mwgui/levelupdialog.hpp @@ -37,6 +37,7 @@ namespace MWGui std::vector mSpentAttributes; + size_t mPerCol; unsigned int mCoinCount; void onOkButtonClicked(MyGUI::Widget* sender); @@ -49,6 +50,10 @@ namespace MWGui std::string_view getLevelupClassImage( const int combatIncreases, const int magicIncreases, const int stealthIncreases); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + std::vector mAttributeButtons; + size_t mControllerFocus; }; } diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index 1b3619bd9f..d9d5b5c33b 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -1,6 +1,7 @@ #include "mainmenu.hpp" #include +#include #include #include @@ -105,6 +106,7 @@ namespace MWGui constexpr VFS::Path::NormalizedView menuBackgroundVideo("video/menu_background.bik"); mHasAnimatedMenu = mVFS->exists(menuBackgroundVideo); + mDisableGamepadCursor = Settings::gui().mControllerMenus; updateMenu(); } @@ -163,9 +165,7 @@ namespace MWGui const std::string& name = *sender->getUserData(); winMgr->playSound(ESM::RefId::stringRefId("Menu Click")); if (name == "return") - { winMgr->removeGuiMode(GM_MainMenu); - } else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") @@ -208,6 +208,32 @@ namespace MWGui } } + bool MainMenu::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Space, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B || arg.button == SDL_CONTROLLER_BUTTON_START) + { + if (mButtons["return"]->getVisible()) + onButtonClicked(mButtons["return"]); + else + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Escape, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::LeftShift); + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); + MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::LeftShift); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); + } + return true; + } + void MainMenu::showBackground(bool show) { if (mVideo && !show) diff --git a/apps/openmw/mwgui/mainmenu.hpp b/apps/openmw/mwgui/mainmenu.hpp index 06a8c945c1..453a16b5e4 100644 --- a/apps/openmw/mwgui/mainmenu.hpp +++ b/apps/openmw/mwgui/mainmenu.hpp @@ -49,6 +49,7 @@ namespace MWGui MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription); void onResChange(int w, int h) override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; void setVisible(bool visible) override; diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 51a765442a..5543caf09f 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -830,6 +831,14 @@ namespace MWGui mGlobalMap->setVisible(global); mLocalMap->setVisible(!global); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mB = "#{sBack}"; + mControllerButtons.mX = global ? "#{sLocal}" : "#{sWorld}"; + mControllerButtons.mY = "#{sCenter}"; + mControllerButtons.mDpad = Settings::map().mAllowZooming ? "" : "#{sMove}"; + } } void MapWindow::onNoteEditOk() @@ -1018,7 +1027,20 @@ namespace MWGui void MapWindow::setVisible(bool visible) { WindowBase::setVisible(visible); - mButton->setVisible(visible && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None); + MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); + mButton->setVisible(visible && mode != MWGui::GM_None); + + if (Settings::gui().mControllerMenus && mode == MWGui::GM_None && pinned() && visible) + { + // Restore the window to pinned size. + MyGUI::Window* window = mMainWidget->castType(); + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + const float x = Settings::windows().mMapX * viewSize.width; + const float y = Settings::windows().mMapY * viewSize.height; + const float w = Settings::windows().mMapW * viewSize.width; + const float h = Settings::windows().mMapH * viewSize.height; + window->setCoord(x, y, w, h); + } } void MapWindow::renderGlobalMap() @@ -1206,6 +1228,8 @@ namespace MWGui mLocalMap->setVisible(!global); mButton->setCaptionWithReplacing(global ? "#{sLocal}" : "#{sWorld}"); + mControllerButtons.mX = global ? "#{sLocal}" : "#{sWorld}"; + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); } void MapWindow::onPinToggled() @@ -1217,7 +1241,9 @@ namespace MWGui void MapWindow::onTitleDoubleClicked() { - if (MyGUI::InputManager::getInstance().isShiftPressed()) + if (Settings::gui().mControllerMenus) + return; + else if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); @@ -1367,6 +1393,73 @@ namespace MWGui mGlobalMapRender->asyncWritePng(); } + bool MapWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onWorldButtonClicked(mButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) + { + centerView(); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + shiftMap(0, 100); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + shiftMap(0, -100); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + shiftMap(100, 0); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + shiftMap(-100, 0); + + return true; + } + + void MapWindow::shiftMap(int dx, int dy) + { + if (dx == 0 && dy == 0) + return; + + if (!Settings::map().mGlobal) + { + mNeedDoorMarkersUpdate = true; + mLocalMap->setViewOffset( + MyGUI::IntPoint(mLocalMap->getViewOffset().left + dx, mLocalMap->getViewOffset().top + dy)); + } + else + { + mGlobalMap->setViewOffset( + MyGUI::IntPoint(mGlobalMap->getViewOffset().left + dx, mGlobalMap->getViewOffset().top + dy)); + } + } + + void MapWindow::setActiveControllerWindow(bool active) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + if (winMgr->getMode() == MWGui::GM_Inventory) + { + // Fill the screen, or limit to a certain size on large screens. Size chosen to + // show the entire local map without scrolling. + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + MyGUI::IntSize canvasSize = mLocalMap->getCanvasSize(); + MyGUI::IntSize borderSize = mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize(); + + int width = std::min(viewSize.width, canvasSize.width + borderSize.width); + int height = std::min(winMgr->getControllerMenuHeight(), canvasSize.height + borderSize.height); + int x = (viewSize.width - width) / 2; + int y = (viewSize.height - height) / 2; + + MyGUI::Window* window = mMainWidget->castType(); + window->setCoord(x, active ? y : viewSize.height + 1, width, height); + } + + WindowBase::setActiveControllerWindow(active); + } + // ------------------------------------------------------------------- EditNoteDialog::EditNoteDialog() @@ -1380,6 +1473,12 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onOkButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onDeleteButtonClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{Interface:OK}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } void EditNoteDialog::showDeleteButton(bool show) @@ -1407,6 +1506,13 @@ namespace MWGui WindowModal::onOpen(); center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = getDeleteButtonShown() ? 1 : 0; + mOkButton->setStateSelected(true); + mCancelButton->setStateSelected(false); + } } void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget* sender) @@ -1424,6 +1530,78 @@ namespace MWGui eventDeleteClicked(); } + ControllerButtons* EditNoteDialog::getControllerButtons() + { + mControllerButtons.mX = getDeleteButtonShown() ? "#{sDelete}" : ""; + return &mControllerButtons; + } + + bool EditNoteDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (getDeleteButtonShown()) + { + if (mControllerFocus == 0) + onDeleteButtonClicked(mDeleteButton); + else if (mControllerFocus == 1) + onOkButtonClicked(mOkButton); + else + onCancelButtonClicked(mCancelButton); + } + else + { + if (mControllerFocus == 0) + onOkButtonClicked(mOkButton); + else + onCancelButtonClicked(mCancelButton); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + if (getDeleteButtonShown()) + onDeleteButtonClicked(mDeleteButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + if (getDeleteButtonShown()) + { + mControllerFocus = wrap(mControllerFocus - 1, 3); + mDeleteButton->setStateSelected(mControllerFocus == 0); + mOkButton->setStateSelected(mControllerFocus == 1); + mCancelButton->setStateSelected(mControllerFocus == 2); + } + else + { + mControllerFocus = 0; + mOkButton->setStateSelected(mControllerFocus == 0); + mCancelButton->setStateSelected(mControllerFocus == 1); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + if (getDeleteButtonShown()) + { + mControllerFocus = wrap(mControllerFocus + 1, 3); + mDeleteButton->setStateSelected(mControllerFocus == 0); + mOkButton->setStateSelected(mControllerFocus == 1); + mCancelButton->setStateSelected(mControllerFocus == 2); + } + else + { + mControllerFocus = 1; + mOkButton->setStateSelected(mControllerFocus == 0); + mCancelButton->setStateSelected(mControllerFocus == 1); + } + } + + return true; + } + bool LocalMapBase::MarkerUserData::isPositionExplored() const { if (!mLocalMapRender) diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index ed070c5407..9a474e8170 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -212,6 +212,8 @@ namespace MWGui EventHandle_Void eventDeleteClicked; EventHandle_Void eventOkClicked; + ControllerButtons* getControllerButtons() override; + private: void onCancelButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); @@ -221,6 +223,9 @@ namespace MWGui MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; MyGUI::Button* mDeleteButton; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + int mControllerFocus; }; class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop @@ -265,6 +270,10 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Map"; } + protected: + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; + private: void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); @@ -283,6 +292,7 @@ namespace MWGui void setGlobalMapMarkerTooltip(MyGUI::Widget* widget, int x, int y); float getMarkerSize(size_t agregatedWeight) const; void resizeGlobalMap(); + void shiftMap(int dx, int dy); void worldPosToGlobalMapImageSpace(float x, float z, float& imageX, float& imageY) const; MyGUI::IntCoord createMarkerCoords(float x, float y, float agregatedWeight) const; MyGUI::Widget* createMarker(const std::string& name, float x, float y, float agregatedWeight); diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index 54f9ae4187..87cda0b4db 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -28,6 +28,13 @@ namespace MWGui getWidget(mGoldLabel, "PlayerGold"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onOkButtonClick); + + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sRepair}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } void MerchantRepair::setPtr(const MWWorld::Ptr& actor) @@ -38,6 +45,7 @@ namespace MWGui while (mList->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mList->getChildAt(0)); + mButtons.clear(); const int lineHeight = Settings::gui().mFontSize + 2; int currentY = 0; @@ -101,6 +109,15 @@ namespace MWGui button->eventMouseWheel += MyGUI::newDelegate(this, &MerchantRepair::onMouseWheel); button->setUserString("ToolTipType", "ItemPtr"); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); + if (price <= playerGold) + mButtons.emplace_back(std::make_pair(button, mButtons.size())); + } + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mButtons.size() > 0) + mButtons[0].first->setStateSelected(true); } // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the @@ -157,4 +174,49 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); } + bool MerchantRepair::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mButtons.size()) + onRepairButtonClick(mButtons[mControllerFocus].first); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onOkButtonClick(mOkButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mButtons.size() <= 1) + return true; + + mButtons[mControllerFocus].first->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + mButtons[mControllerFocus].first->setStateSelected(true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mButtons.size() <= 1) + return true; + + mButtons[mControllerFocus].first->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + mButtons[mControllerFocus].first->setStateSelected(true); + } + + // Scroll the list to keep the active item in view + if (mControllerFocus < mButtons.size()) + { + size_t line = mButtons[mControllerFocus].second; + if (line <= 5) + mList->setViewOffset(MyGUI::IntPoint(0, 0)); + else + { + const int lineHeight = Settings::gui().mFontSize + 2; + mList->setViewOffset(MyGUI::IntPoint(0, -lineHeight * (line - 5))); + } + } + + return true; + } } diff --git a/apps/openmw/mwgui/merchantrepair.hpp b/apps/openmw/mwgui/merchantrepair.hpp index ffe5b86bdb..28fb2a7c3c 100644 --- a/apps/openmw/mwgui/merchantrepair.hpp +++ b/apps/openmw/mwgui/merchantrepair.hpp @@ -22,13 +22,18 @@ namespace MWGui MyGUI::ScrollView* mList; MyGUI::Button* mOkButton; MyGUI::TextBox* mGoldLabel; + /// List of enabled/repairable items and their index in the full list. + std::vector> mButtons; MWWorld::Ptr mActor; + size_t mControllerFocus; + protected: void onMouseWheel(MyGUI::Widget* _sender, int _rel); void onRepairButtonClick(MyGUI::Widget* sender); void onOkButtonClick(MyGUI::Widget* sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 1d6e1511c4..d705d66bb6 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -8,6 +8,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -217,7 +218,7 @@ namespace MWGui } InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, - const std::vector& buttons, bool immediate, int defaultFocus) + const std::vector& buttons, bool immediate, size_t defaultFocus) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") @@ -225,6 +226,7 @@ namespace MWGui , mButtonPressed(-1) , mDefaultFocus(defaultFocus) , mImmediate(immediate) + , mControllerFocus(0) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget @@ -280,6 +282,22 @@ namespace MWGui } } + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{Interface:OK}"; + + // If we have more than one button, we need to set the focus to the first one. + if (mButtons.size() > 1) + { + mControllerFocus = 0; + if (mDefaultFocus < mButtons.size()) + mControllerFocus = mDefaultFocus; + for (size_t i = 0; i < mButtons.size(); ++i) + mButtons[i]->setStateSelected(i == mControllerFocus); + } + } + MyGUI::IntSize mainWidgetSize; if (buttonsWidth < textSize.width) { @@ -380,7 +398,7 @@ namespace MWGui MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { - if (mDefaultFocus >= 0 && mDefaultFocus < static_cast(mButtons.size())) + if (mDefaultFocus < mButtons.size()) return mButtons[mDefaultFocus]; auto& languageManager = MyGUI::LanguageManager::getInstance(); std::vector keywords{ languageManager.replaceTags("#{sOk}"), @@ -431,4 +449,45 @@ namespace MWGui return mButtonPressed; } + bool InteractiveMessageBox::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (!mButtons.empty()) + { + if (mControllerFocus >= mButtons.size()) + mControllerFocus = mButtons.size() - 1; + buttonActivated(mButtons[mControllerFocus]); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + if (mButtons.size() == 1) + buttonActivated(mButtons[0]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP || arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + if (mButtons.size() <= 1) + return true; + if (mButtons.size() == 2 && mControllerFocus == 0) + return true; + + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + if (mButtons.size() <= 1) + return true; + if (mButtons.size() == 2 && mControllerFocus == 1) + return true; + + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + + return true; + } } diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index feb717e0ad..98eb69cc69 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -93,7 +93,7 @@ namespace MWGui { public: InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, - const std::vector& buttons, bool immediate, int defaultFocus); + const std::vector& buttons, bool immediate, size_t defaultFocus); void mousePressed(MyGUI::Widget* _widget); int readPressedButton(); @@ -103,6 +103,8 @@ namespace MWGui bool mMarkedToDelete; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + private: void buttonActivated(MyGUI::Widget* _widget); @@ -112,8 +114,9 @@ namespace MWGui std::vector mButtons; int mButtonPressed; - int mDefaultFocus; + size_t mDefaultFocus; bool mImmediate; + size_t mControllerFocus; }; } diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 3c62400e0d..782949b99a 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -39,7 +40,7 @@ namespace MWGui , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) - + , mControllerFocus(0) { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); @@ -58,6 +59,12 @@ namespace MWGui unassign(&mKey[i]); } + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:OK}"; + } } void QuickKeysMenu::clear() @@ -108,6 +115,13 @@ namespace MWGui { validate(index); } + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + for (size_t i = 0; i < mKey.size(); i++) + mKey[i].button->setControllerFocus(i == mControllerFocus); + } } void QuickKeysMenu::onClose() @@ -454,11 +468,45 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } + bool QuickKeysMenu::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + onQuickKeyButtonClicked(mKey[mControllerFocus].button); + if (arg.button == SDL_CONTROLLER_BUTTON_B) + onOkButtonClicked(mOkButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP || arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + mControllerFocus = (mControllerFocus + 5) % 10; + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + if (mControllerFocus == 0) + mControllerFocus = 4; + else if (mControllerFocus == 5) + mControllerFocus = 9; + else + mControllerFocus--; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + if (mControllerFocus == 4) + mControllerFocus = 0; + else if (mControllerFocus == 9) + mControllerFocus = 5; + else + mControllerFocus++; + } + + for (size_t i = 0; i < mKey.size(); i++) + mKey[i].button->setControllerFocus(i == mControllerFocus); + + return true; + } + // --------------------------------------------------------------------------------------------------------- QuickKeysMenuAssign::QuickKeysMenuAssign(QuickKeysMenu* parent) : WindowModal("openmw_quickkeys_menu_assign.layout") , mParent(parent) + , mControllerFocus(0) { getWidget(mLabel, "Label"); getWidget(mItemButton, "ItemButton"); @@ -489,9 +537,45 @@ namespace MWGui mCancelButton->setCoord((maxWidth - mCancelButton->getTextSize().width - 24) / 2 + 8, mCancelButton->getTop(), mCancelButton->getTextSize().width + 24, mCancelButton->getHeight()); + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mItemButton->setStateSelected(true); + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } + center(); } + bool QuickKeysMenuAssign::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus == 0) + mParent->onItemButtonClicked(mItemButton); + else if (mControllerFocus == 1) + mParent->onMagicButtonClicked(mMagicButton); + else if (mControllerFocus == 2) + mParent->onUnassignButtonClicked(mUnassignButton); + else if (mControllerFocus == 3) + mParent->onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + mParent->onCancelButtonClicked(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + mControllerFocus = wrap(mControllerFocus - 1, 4); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + mControllerFocus = wrap(mControllerFocus + 1, 4); + + mItemButton->setStateSelected(mControllerFocus == 0); + mMagicButton->setStateSelected(mControllerFocus == 1); + mUnassignButton->setStateSelected(mControllerFocus == 2); + mCancelButton->setStateSelected(mControllerFocus == 3); + + return true; + } + void QuickKeysMenu::write(ESM::ESMWriter& writer) { writer.startRecord(ESM::REC_KEYS); @@ -601,6 +685,12 @@ namespace MWGui mMagicList->setHighlightSelected(false); mMagicList->eventSpellClicked += MyGUI::newDelegate(this, &MagicSelectionDialog::onModelIndexSelected); + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } + center(); } @@ -632,4 +722,13 @@ namespace MWGui mParent->onAssignMagic(spell.mId); } + bool MagicSelectionDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(mCancelButton); + else + mMagicList->onControllerButton(arg.button); + + return true; + } } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index a43cce50b4..11648cdb40 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -73,6 +73,9 @@ namespace MWGui inline void validate(int index); void unassign(keyData* key); void assignItem(MWWorld::Ptr item); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; }; class QuickKeysMenuAssign : public WindowModal @@ -88,6 +91,9 @@ namespace MWGui MyGUI::Button* mCancelButton; QuickKeysMenu* mParent; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + int mControllerFocus; }; class MagicSelectionDialog : public WindowModal @@ -106,6 +112,9 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* sender); void onModelIndexSelected(SpellModel::ModelIndex index); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + int mControllerFocus; }; } diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index c7de8f4125..193c409ab7 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -23,16 +23,6 @@ namespace { - int wrap(int index, int max) - { - if (index < 0) - return max - 1; - else if (index >= max) - return 0; - else - return index; - } - bool sortRaces(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; @@ -108,15 +98,23 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu7", "Specials")); getWidget(mSpellPowerList, "SpellPowerList"); - MyGUI::Button* backButton; - getWidget(backButton, "BackButton"); - backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onBackClicked); + getWidget(mBackButton, "BackButton"); + mBackButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onBackClicked); - MyGUI::Button* okButton; - getWidget(okButton, "OKButton"); - okButton->setCaption( + getWidget(mOkButton, "OKButton"); + mOkButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); - okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mLStick = "#{sMouse}"; + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + mControllerButtons.mY = "#{sSex}"; + mControllerButtons.mL1 = "#{sHair}"; + mControllerButtons.mR1 = "#{sFace}"; + } updateRaces(); updateSkills(); @@ -129,8 +127,17 @@ namespace MWGui getWidget(okButton, "OKButton"); if (shown) + { okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); + mControllerButtons.mX = "#{sNext}"; + } + else if (Settings::gui().mControllerMenus) + { + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + mControllerButtons.mX = "#{sDone}"; + } else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); @@ -462,6 +469,55 @@ namespace MWGui } } + bool RaceDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mBackButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkClicked(mOkButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) + { + onSelectNextGender(nullptr); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + onSelectNextHair(nullptr); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + onSelectNextFace(nullptr); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mRaceList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mRaceList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + + return true; + } + + bool RaceDialog::onControllerThumbstickEvent(const SDL_ControllerAxisEvent& arg) + { + if (arg.axis == SDL_CONTROLLER_AXIS_RIGHTX) + { + onPreviewScroll(nullptr, arg.value < 0 ? 1 : -1); + return true; + } + + return false; + } + const ESM::NPC& RaceDialog::getResult() const { return mPreview->getPrototype(); diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index a6ac0e2813..0b00de40c0 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -101,6 +101,8 @@ namespace MWGui MyGUI::ImageBox* mPreviewImage; MyGUI::ListBox* mRaceList; MyGUI::ScrollBar* mHeadRotate; + MyGUI::Button* mBackButton; + MyGUI::Button* mOkButton; MyGUI::Widget* mSkillList; std::vector mSkillItems; @@ -118,6 +120,9 @@ namespace MWGui std::unique_ptr mPreviewTexture; bool mPreviewDirty; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + bool onControllerThumbstickEvent(const SDL_ControllerAxisEvent& arg) override; }; } #endif diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index 7d57988d97..b6354c1d70 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -39,6 +39,10 @@ namespace MWGui mBox->setDisplayMode(ItemChargeView::DisplayMode_EnchantmentCharge); mGemIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onSelectItem); + + mControllerButtons.mA = "#{OMWEngine:RechargeSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mY = "#{sSoulGem}"; } void Recharge::onOpen() @@ -136,4 +140,18 @@ namespace MWGui updateView(); } + bool Recharge::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if ((arg.button == SDL_CONTROLLER_BUTTON_A && !mGemBox->getVisible()) || arg.button == SDL_CONTROLLER_BUTTON_Y) + { + onSelectItem(mGemIcon); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancel(mCancelButton); + else + mBox->onControllerButton(arg.button); + + return true; + } } diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index f8a037d2db..c10f96e71e 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -51,6 +51,8 @@ namespace MWGui void onItemClicked(MyGUI::Widget* sender, const MWWorld::Ptr& item); void onCancel(MyGUI::Widget* sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index c1602b8407..cb3f6be8bb 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -39,6 +39,10 @@ namespace MWGui mRepairBox->setDisplayMode(ItemChargeView::DisplayMode_Health); mToolIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onSelectItem); + + mControllerButtons.mA = "#{sRepair}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mY = "#{OMWEngine:RepairTool}"; } void Repair::onOpen() @@ -150,4 +154,18 @@ namespace MWGui updateRepairView(); } + bool Repair::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if ((arg.button == SDL_CONTROLLER_BUTTON_A && !mToolBox->getVisible()) || arg.button == SDL_CONTROLLER_BUTTON_Y) + { + onSelectItem(mToolIcon); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancel(mCancelButton); + else + mRepairBox->onControllerButton(arg.button); + + return true; + } } diff --git a/apps/openmw/mwgui/repair.hpp b/apps/openmw/mwgui/repair.hpp index 093a10e3fa..986b28b613 100644 --- a/apps/openmw/mwgui/repair.hpp +++ b/apps/openmw/mwgui/repair.hpp @@ -50,6 +50,8 @@ namespace MWGui void onRepairItem(MyGUI::Widget* sender, const MWWorld::Ptr& ptr); void onCancel(MyGUI::Widget* sender); + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; }; } diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index ddce2c5f50..66b48aa2ee 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -36,6 +36,7 @@ namespace MWGui ReviewDialog::ReviewDialog() : WindowModal("openmw_chargen_review.layout") , mUpdateSkillArea(false) + , mControllerFocus(5) { // Centre dialog center(); @@ -46,21 +47,25 @@ namespace MWGui getWidget(button, "NameButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked); + mButtons.push_back(button); getWidget(mRaceWidget, "RaceText"); getWidget(button, "RaceButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked); + mButtons.push_back(button); getWidget(mClassWidget, "ClassText"); getWidget(button, "ClassButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked); + mButtons.push_back(button); getWidget(mBirthSignWidget, "SignText"); getWidget(button, "SignButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked); + mButtons.push_back(button); // Setup dynamic stats getWidget(mHealth, "Health"); @@ -108,10 +113,22 @@ namespace MWGui MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBackClicked); + mButtons.push_back(backButton); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onOkClicked); + mButtons.push_back(okButton); + + if (Settings::gui().mControllerMenus) + { + setControllerFocus(mButtons, mControllerFocus, true); + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + mControllerButtons.mX = "#{sDone}"; + okButton->setCaption( + MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sDone", {}))); + } } void ReviewDialog::onOpen() @@ -522,4 +539,54 @@ namespace MWGui MyGUI::IntPoint(0, static_cast(mSkillView->getViewOffset().top + _rel * 0.3))); } + bool ReviewDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + switch (mControllerFocus) + { + case 0: + onNameClicked(mButtons[0]); + break; + case 1: + onRaceClicked(mButtons[1]); + break; + case 2: + onClassClicked(mButtons[2]); + break; + case 3: + onBirthSignClicked(mButtons[3]); + break; + case 4: + onBackClicked(mButtons[4]); + break; + case 5: + onOkClicked(mButtons[5]); + break; + } + return true; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onBackClicked(mButtons[4]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkClicked(mButtons[5]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP || arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + setControllerFocus(mButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mButtons.size()); + setControllerFocus(mButtons, mControllerFocus, true); + } + + return true; + } } diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 7226ad628d..cd3fc594a7 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -72,6 +72,7 @@ namespace MWGui void onBirthSignClicked(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: void addSkills(const std::vector& skills, const std::string& titleId, @@ -100,6 +101,10 @@ namespace MWGui std::vector mSkillWidgets; //< Skills and other information bool mUpdateSkillArea; + + // 0 = Name, 1 = Race, 2 = Class, 3 = BirthSign, 4 = Back, 5 = OK + std::vector mButtons; + int mControllerFocus; }; } #endif diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index eec3b7bfe6..28c1b17cda 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include "../mwbase/world.hpp" #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/timestamp.hpp" #include "../mwstate/character.hpp" @@ -64,6 +66,9 @@ namespace MWGui // To avoid accidental deletions mDeleteButton->setNeedKeyFocus(false); + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; } void SaveGameDialog::onSlotActivated(MyGUI::ListBox* sender, size_t pos) @@ -145,6 +150,28 @@ namespace MWGui WindowModal::onOpen(); mSaveNameEdit->setCaption({}); + if (Settings::gui().mControllerMenus && mSaving) + { + // For controller mode, set a default save file name. The format is + // "Day 24 - Last Steed 7 p.m." + const MWWorld::DateTimeManager& timeManager = *MWBase::Environment::get().getWorld()->getTimeManager(); + std::string_view month = timeManager.getMonthName(); + int hour = static_cast(timeManager.getTimeStamp().getHour()); + bool pm = hour >= 12; + if (hour >= 13) + hour -= 12; + if (hour == 0) + hour = 12; + + ESM::EpochTimeStamp currentDate = timeManager.getEpochTimeStamp(); + std::string daysPassed + = Misc::StringUtils::format("#{Calendar:day} %i", timeManager.getTimeStamp().getDay()); + std::string_view formattedHour(pm ? "#{Calendar:pm}" : "#{Calendar:am}"); + std::string autoFilename = Misc::StringUtils::format( + "%s - %i %s %i %s", daysPassed, currentDate.mDay, month, hour, formattedHour); + + mSaveNameEdit->setCaptionWithReplacing(autoFilename); + } if (mSaving) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveNameEdit); else @@ -159,6 +186,13 @@ namespace MWGui mSaveList->removeAllItems(); onSlotSelected(mSaveList, MyGUI::ITEM_NONE); + if (Settings::gui().mControllerMenus) + { + mOkButtonFocus = true; + mOkButton->setStateSelected(true); + mCancelButton->setStateSelected(false); + } + MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); if (mgr->characterBegin() == mgr->characterEnd()) return; @@ -491,4 +525,55 @@ namespace MWGui mScreenshotTexture = std::make_unique(texture); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); } + + ControllerButtons* SaveGameDialog::getControllerButtons() + { + mControllerButtons.mY = mSaving ? "" : "#{OMWEngine:LoadingSelectCharacter}"; + return &mControllerButtons; + } + + bool SaveGameDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mOkButtonFocus) + onOkButtonClicked(mOkButton); + else + onCancelButtonClicked(mCancelButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_Y) + { + size_t index = mCharacterSelection->getIndexSelected(); + index = wrap(index + 1, mCharacterSelection->getItemCount()); + mCharacterSelection->setIndexSelected(index); + onCharacterSelected(mCharacterSelection, index); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mSaveList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setKeyFocusWidget(mSaveList); + winMgr->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + } + else if ((arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT && !mOkButtonFocus) + || (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT && mOkButtonFocus)) + { + mOkButtonFocus = !mOkButtonFocus; + mOkButton->setStateSelected(mOkButtonFocus); + mCancelButton->setStateSelected(!mOkButtonFocus); + } + + return true; + } } diff --git a/apps/openmw/mwgui/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index af831f066e..2b5b163044 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -24,6 +24,8 @@ namespace MWGui void setLoadOrSave(bool load); + ControllerButtons* getControllerButtons() override; + private: void confirmDeleteSave(); @@ -67,6 +69,9 @@ namespace MWGui const MWState::Character* mCurrentCharacter; const MWState::Slot* mCurrentSlot; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + bool mOkButtonFocus = true; }; } diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index 0b1658fd84..5b5f4a4ec4 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -7,6 +7,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -38,6 +39,10 @@ namespace MWGui mCloseButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); mTakeButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); + mControllerScrollWidget = mTextView; + mControllerButtons.mB = "#{Interface:Close}"; + mControllerButtons.mDpad = "#{sScrolldown}"; + center(); } @@ -115,4 +120,32 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } + + void ScrollWindow::onClose() + { + if (Settings::gui().mControllerMenus) + MWBase::Environment::get().getInputManager()->setGamepadGuiCursorEnabled(true); + BookWindowBase::onClose(); + } + + ControllerButtons* ScrollWindow::getControllerButtons() + { + mControllerButtons.mA = mTakeButton->getVisible() ? "#{sTake}" : ""; + return &mControllerButtons; + } + + bool ScrollWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mTakeButton->getVisible()) + onTakeButtonClicked(mTakeButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCloseButtonClicked(mCloseButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP || arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + return false; // Fall through to keyboard + + return true; + } } diff --git a/apps/openmw/mwgui/scrollwindow.hpp b/apps/openmw/mwgui/scrollwindow.hpp index 7daea98894..398843f824 100644 --- a/apps/openmw/mwgui/scrollwindow.hpp +++ b/apps/openmw/mwgui/scrollwindow.hpp @@ -20,15 +20,19 @@ namespace MWGui void setPtr(const MWWorld::Ptr& scroll) override; void setInventoryAllowed(bool allowed); + void onClose() override; void onResChange(int, int) override { center(); } std::string_view getWindowIdForLua() const override { return "Scroll"; } + ControllerButtons* getControllerButtons() override; + protected: void onCloseButtonClicked(MyGUI::Widget* _sender); void onTakeButtonClicked(MyGUI::Widget* _sender); void setTakeButtonShow(bool show); void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: Gui::ImageButton* mCloseButton; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 77032623d2..15e083d94e 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -462,6 +462,10 @@ namespace MWGui i++; } + + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:OK}"; + mControllerButtons.mLStick = "#{sMouse}"; } void SettingsWindow::onTabChanged(MyGUI::TabControl* /*_sender*/, size_t /*index*/) @@ -471,7 +475,7 @@ namespace MWGui void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { - setVisible(false); + MWBase::Environment::get().getWindowManager()->toggleSettingsWindow(); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) @@ -1139,4 +1143,32 @@ namespace MWGui mResolutionList->setScrollPosition(0); mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); } + + bool SettingsWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onOkButtonClicked(mOkButton); + return true; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + size_t index = mSettingsTab->getIndexSelected(); + index = wrap(index - 1, mSettingsTab->getItemCount()); + mSettingsTab->setIndexSelected(index); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + return true; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + size_t index = mSettingsTab->getIndexSelected(); + index = wrap(index + 1, mSettingsTab->getItemCount()); + mSettingsTab->setIndexSelected(index); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + return true; + } + + return false; + } + } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 22a15eab97..0cc1c13fb4 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -29,6 +29,8 @@ namespace MWGui void onResChange(int, int) override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + protected: MyGUI::TabControl* mSettingsTab; MyGUI::Button* mOkButton; diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index 2a67af5498..4a9f118d75 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -9,6 +9,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -26,12 +27,21 @@ namespace MWGui SpellBuyingWindow::SpellBuyingWindow() : WindowBase("openmw_spell_buying_window.layout") , mCurrentY(0) + , mControllerFocus(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mSpellsView, "SpellsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onCancelButtonClicked); + + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sBuy}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mR3 = "#{sInfo}"; + } } bool SpellBuyingWindow::sortSpells(const ESM::Spell* left, const ESM::Spell* right) @@ -71,6 +81,8 @@ namespace MWGui toAdd->setUserString("SpellCost", std::to_string(spell.mData.mCost)); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onSpellButtonClick); mSpellsWidgetMap.insert(std::make_pair(toAdd, spell.mId)); + if (price <= playerGold) + mSpellButtons.emplace_back(std::make_pair(toAdd, mSpellsWidgetMap.size())); } void SpellBuyingWindow::clearSpells() @@ -80,6 +92,7 @@ namespace MWGui while (mSpellsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mSpellsView->getChildAt(0)); mSpellsWidgetMap.clear(); + mSpellButtons.clear(); } void SpellBuyingWindow::setPtr(const MWWorld::Ptr& actor) @@ -130,6 +143,20 @@ namespace MWGui updateLabels(); + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mSpellButtons.size() > 0) + { + mSpellButtons[0].first->setStateSelected(true); + + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + winMgr->setControllerTooltip(Settings::gui().mControllerTooltips); + if (winMgr->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(mSpellButtons[0].first); + } + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mSpellsView->setVisibleVScroll(false); @@ -200,4 +227,62 @@ namespace MWGui mSpellsView->setViewOffset( MyGUI::IntPoint(0, static_cast(mSpellsView->getViewOffset().top + _rel * 0.3f))); } + + bool SpellBuyingWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mSpellButtons.size()) + onSpellButtonClick(mSpellButtons[mControllerFocus].first); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK) + { + // Toggle info tooltip + MWBase::Environment::get().getWindowManager()->setControllerTooltip( + !MWBase::Environment::get().getWindowManager()->getControllerTooltip()); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mSpellButtons.size() <= 1) + return true; + + mSpellButtons[mControllerFocus].first->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus - 1, mSpellButtons.size()); + mSpellButtons[mControllerFocus].first->setStateSelected(true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mSpellButtons.size() <= 1) + return true; + + mSpellButtons[mControllerFocus].first->setStateSelected(false); + mControllerFocus = wrap(mControllerFocus + 1, mSpellButtons.size()); + mSpellButtons[mControllerFocus].first->setStateSelected(true); + } + else + return true; + + if (mControllerFocus < mSpellButtons.size()) + { + // Scroll the list to keep the active item in view + size_t line = mSpellButtons[mControllerFocus].second; + if (line <= 5) + mSpellsView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + { + const int lineHeight = Settings::gui().mFontSize + 2; + mSpellsView->setViewOffset(MyGUI::IntPoint(0, -lineHeight * (line - 5))); + } + + // Warp the mouse to the selected spell to show the tooltip + if (MWBase::Environment::get().getWindowManager()->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(mSpellButtons[mControllerFocus].first); + } + + return true; + } } diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index 257b8a0df9..a53b57cc09 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.hpp +++ b/apps/openmw/mwgui/spellbuyingwindow.hpp @@ -39,6 +39,8 @@ namespace MWGui MyGUI::ScrollView* mSpellsView; std::map mSpellsWidgetMap; + /// List of enabled/purchasable spells and their index in the full list. + std::vector> mSpellButtons; void onCancelButtonClicked(MyGUI::Widget* _sender); void onSpellButtonClick(MyGUI::Widget* _sender); @@ -55,6 +57,8 @@ namespace MWGui private: static bool sortSpells(const ESM::Spell* left, const ESM::Spell* right); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; }; } diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 6bd9ef3ac8..5684e9ef2d 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -7,11 +7,13 @@ #include #include +#include #include #include #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -25,8 +27,8 @@ #include "../mwmechanics/spellutil.hpp" #include "class.hpp" +#include "textcolours.hpp" #include "tooltips.hpp" -#include "widgets.hpp" namespace { @@ -95,6 +97,13 @@ namespace MWGui += MyGUI::newDelegate(this, &EditEffectDialog::onMagnitudeMaxChanged); mDurationSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onDurationChanged); mAreaSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onAreaChanged); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mX = "#{Interface:OK}"; + } } void EditEffectDialog::setConstantEffect(bool constant) @@ -154,6 +163,15 @@ namespace MWGui mMagnitudeMaxValue->setCaption(to + " 1"); mAreaValue->setCaption("0"); + if (Settings::gui().mControllerMenus) + { + mRangeButton->setStateSelected(true); + mDeleteButton->setStateSelected(false); + mOkButton->setStateSelected(false); + mCancelButton->setStateSelected(false); + mControllerFocus = 0; + } + setVisible(true); } @@ -187,6 +205,15 @@ namespace MWGui onDurationChanged(mDurationSlider, effect.mDuration - 1); eventEffectModified(mEffect); + if (Settings::gui().mControllerMenus) + { + mRangeButton->setStateSelected(true); + mDeleteButton->setStateSelected(false); + mOkButton->setStateSelected(false); + mCancelButton->setStateSelected(false); + mControllerFocus = 0; + } + updateBoxes(); } @@ -231,6 +258,25 @@ namespace MWGui mAreaBox->setVisible(true); // curY += mAreaBox->getSize().height; } + + if (Settings::gui().mControllerMenus) + { + mButtons.clear(); + mButtons.emplace_back(mRangeButton); + if (mMagnitudeBox->getVisible()) + { + mButtons.emplace_back(mMagnitudeMinValue); + mButtons.emplace_back(mMagnitudeMaxValue); + } + if (mDurationBox->getVisible()) + mButtons.emplace_back(mDurationValue); + if (mAreaBox->getVisible()) + mButtons.emplace_back(mAreaValue); + if (mDeleteButton->getVisible()) + mButtons.emplace_back(mDeleteButton); + mButtons.emplace_back(mOkButton); + mButtons.emplace_back(mCancelButton); + } } void EditEffectDialog::onRangeButtonClicked(MyGUI::Widget* sender) @@ -340,6 +386,195 @@ namespace MWGui eventEffectModified(mEffect); } + bool EditEffectDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + int prevFocus = mControllerFocus; + mControllerFocus = std::clamp(mControllerFocus, 0, static_cast(mButtons.size()) - 1); + MyGUI::TextBox* button = mButtons[mControllerFocus]; + + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (button == mRangeButton) + onRangeButtonClicked(mRangeButton); + else if (button == mCancelButton) + onCancelButtonClicked(mCancelButton); + else if (button == mOkButton) + onOkButtonClicked(mOkButton); + else if (button == mDeleteButton) + onDeleteButtonClicked(mDeleteButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onOkButtonClicked(mOkButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mControllerFocus == 0) + mControllerFocus = static_cast(mButtons.size()) - 2; + else if (button == mCancelButton && mDeleteButton->getVisible()) + mControllerFocus -= 3; + else if (button == mCancelButton || (button == mOkButton && mDeleteButton->getVisible())) + mControllerFocus -= 2; + else + mControllerFocus = std::max(mControllerFocus - 1, 0); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (button == mDeleteButton || button == mOkButton || button == mCancelButton) + mControllerFocus = 0; + else + mControllerFocus++; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + if (button == mMagnitudeMinValue) + { + mMagnitudeMinSlider->setScrollPosition(0); + onMagnitudeMinChanged(nullptr, mMagnitudeMinSlider->getScrollPosition()); + } + else if (button == mMagnitudeMaxValue) + { + mMagnitudeMaxSlider->setScrollPosition(mMagnitudeMinSlider->getScrollPosition()); + onMagnitudeMaxChanged(nullptr, mMagnitudeMaxSlider->getScrollPosition()); + } + else if (button == mDurationValue) + { + mDurationSlider->setScrollPosition(0); + onDurationChanged(nullptr, mDurationSlider->getScrollPosition()); + } + else if (button == mAreaValue) + { + mAreaSlider->setScrollPosition(0); + onAreaChanged(nullptr, mAreaSlider->getScrollPosition()); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + if (button == mMagnitudeMinValue) + { + mMagnitudeMinSlider->setScrollPosition(mMagnitudeMaxSlider->getScrollPosition()); + onMagnitudeMinChanged(nullptr, mMagnitudeMinSlider->getScrollPosition()); + } + else if (button == mMagnitudeMaxValue) + { + mMagnitudeMaxSlider->setScrollPosition(mMagnitudeMaxSlider->getScrollRange() - 1); + onMagnitudeMaxChanged(nullptr, mMagnitudeMaxSlider->getScrollPosition()); + } + else if (button == mDurationValue) + { + mDurationSlider->setScrollPosition(mDurationSlider->getScrollRange() - 1); + onDurationChanged(nullptr, mDurationSlider->getScrollPosition()); + } + else if (button == mAreaValue) + { + mAreaSlider->setScrollPosition(mAreaSlider->getScrollRange() - 1); + onAreaChanged(nullptr, mAreaSlider->getScrollPosition()); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + { + if (button == mRangeButton) + onRangeButtonClicked(mRangeButton); + else if (button == mCancelButton) + mControllerFocus--; + else if (button == mOkButton && mDeleteButton->getVisible()) + mControllerFocus--; + else if (button == mMagnitudeMinValue) + { + mMagnitudeMinSlider->setScrollPosition(mMagnitudeMinSlider->getScrollPosition() - 1); + onMagnitudeMinChanged(nullptr, mMagnitudeMinSlider->getScrollPosition()); + } + else if (button == mMagnitudeMaxValue) + { + mMagnitudeMaxSlider->setScrollPosition( + std::max(mMagnitudeMaxSlider->getScrollPosition() - 1, mMagnitudeMinSlider->getScrollPosition())); + onMagnitudeMaxChanged(nullptr, mMagnitudeMaxSlider->getScrollPosition()); + } + else if (button == mDurationValue) + { + mDurationSlider->setScrollPosition(mDurationSlider->getScrollPosition() - 1); + onDurationChanged(nullptr, mDurationSlider->getScrollPosition()); + } + else if (button == mAreaValue) + { + mAreaSlider->setScrollPosition(mAreaSlider->getScrollPosition() - 1); + onAreaChanged(nullptr, mAreaSlider->getScrollPosition()); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + if (button == mRangeButton) + onRangeButtonClicked(mRangeButton); + else if (button == mDeleteButton) + mControllerFocus++; + else if (button == mOkButton) + mControllerFocus++; + else if (button == mMagnitudeMinValue) + { + mMagnitudeMinSlider->setScrollPosition( + std::min(mMagnitudeMinSlider->getScrollPosition() + 1, mMagnitudeMaxSlider->getScrollPosition())); + onMagnitudeMinChanged(nullptr, mMagnitudeMinSlider->getScrollPosition()); + } + else if (button == mMagnitudeMaxValue) + { + mMagnitudeMaxSlider->setScrollPosition(mMagnitudeMaxSlider->getScrollPosition() + 1); + onMagnitudeMaxChanged(nullptr, mMagnitudeMaxSlider->getScrollPosition()); + } + else if (button == mDurationValue) + { + mDurationSlider->setScrollPosition(mDurationSlider->getScrollPosition() + 1); + onDurationChanged(nullptr, mDurationSlider->getScrollPosition()); + } + else if (button == mAreaValue) + { + mAreaSlider->setScrollPosition(mAreaSlider->getScrollPosition() + 1); + onAreaChanged(nullptr, mAreaSlider->getScrollPosition()); + } + } + + if (prevFocus != mControllerFocus) + updateControllerFocus(prevFocus, mControllerFocus); + + return true; + } + + void EditEffectDialog::updateControllerFocus(int prevFocus, int newFocus) + { + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + + if (prevFocus >= 0 && prevFocus < static_cast(mButtons.size())) + { + MyGUI::TextBox* button = mButtons[prevFocus]; + if (button == mMagnitudeMinValue || button == mMagnitudeMaxValue || button == mDurationValue + || button == mAreaValue) + { + button->setTextColour(textColours.normal); + } + else + { + static_cast(button)->setStateSelected(false); + } + } + + if (newFocus >= 0 && newFocus < static_cast(mButtons.size())) + { + MyGUI::TextBox* button = mButtons[newFocus]; + if (button == mMagnitudeMinValue || button == mMagnitudeMaxValue || button == mDurationValue + || button == mAreaValue) + { + button->setTextColour(textColours.link); + } + else + { + static_cast(button)->setStateSelected(true); + } + } + } + // ------------------------------------------------------------------------------------------------ SpellCreationDialog::SpellCreationDialog() @@ -361,6 +596,14 @@ namespace MWGui mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SpellCreationDialog::onAccept); setWidgets(mAvailableEffectsList, mUsedEffectsView); + + if (Settings::gui().mControllerMenus) + { + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mX = "#{sBuy}"; + mControllerButtons.mR3 = "#{sInfo}"; + } } void SpellCreationDialog::setPtr(const MWWorld::Ptr& actor) @@ -495,6 +738,22 @@ namespace MWGui mSuccessChance->setCaption(MyGUI::utility::toString(intChance)); } + bool SpellCreationDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + return true; + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + onBuyButtonClicked(mBuyButton); + return true; + } + else + return EffectEditorBase::onControllerButtonEvent(arg); + } + // ------------------------------------------------------------------------------------------------ EffectEditorBase::EffectEditorBase(Type type) @@ -566,6 +825,7 @@ namespace MWGui mAvailableEffectsList->adjustSize(); mAvailableEffectsList->scrollToTop(); + mAvailableButtons.clear(); for (const short effectId : knownEffects) { const std::string& name = MWBase::Environment::get() @@ -573,13 +833,27 @@ namespace MWGui ->get() .find(ESM::MagicEffect::indexToGmstString(effectId)) ->mValue.getString(); - MyGUI::Widget* w = mAvailableEffectsList->getItemWidget(name); + MyGUI::Button* w = mAvailableEffectsList->getItemWidget(name); + mAvailableButtons.emplace_back(w); ToolTips::createMagicEffectToolTip(w, effectId); } mEffects.clear(); updateEffectsView(); + + if (Settings::gui().mControllerMenus) + { + mAvailableFocus = 0; + mEffectFocus = 0; + mRightColumn = false; + if (mAvailableButtons.size() > 0) + { + mAvailableButtons[0]->setStateSelected(true); + if (MWBase::Environment::get().getWindowManager()->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(mAvailableButtons[0]); + } + } } void EffectEditorBase::setWidgets(Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView) @@ -691,6 +965,7 @@ namespace MWGui MyGUI::IntSize size(0, 0); + mEffectButtons.clear(); int i = 0; for (const ESM::ENAMstruct& effectInfo : mEffects) { @@ -723,6 +998,8 @@ namespace MWGui size.width = std::max(size.width, effect->getRequestedWidth()); size.height += 24; ++i; + + mEffectButtons.emplace_back(std::pair(effect, button)); } // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the @@ -760,4 +1037,102 @@ namespace MWGui effect.mRange = ESM::RT_Self; mConstantEffect = constant; } + + bool EffectEditorBase::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (!mRightColumn && mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + { + onAvailableEffectClicked(mAvailableButtons[mAvailableFocus]); + winMgr->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (mRightColumn && mEffectFocus >= 0 && mEffectFocus < static_cast(mEffectButtons.size())) + { + onEditEffect(mEffectButtons[mEffectFocus].second); + winMgr->playSound(ESM::RefId::stringRefId("Menu Click")); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK) + { + // Toggle info tooltip + winMgr->setControllerTooltip(!mRightColumn && !winMgr->getControllerTooltip()); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mRightColumn && mEffectButtons.size() > 0) + { + if (mEffectFocus >= 0 && mEffectFocus < static_cast(mEffectButtons.size())) + mEffectButtons[mEffectFocus].first->setStateSelected(false); + mEffectFocus = wrap(mEffectFocus - 1, mEffectButtons.size()); + mEffectButtons[mEffectFocus].first->setStateSelected(true); + } + else if (!mRightColumn && mAvailableButtons.size() > 0) + { + if (mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + mAvailableButtons[mAvailableFocus]->setStateSelected(false); + mAvailableFocus = wrap(mAvailableFocus - 1, mAvailableButtons.size()); + mAvailableButtons[mAvailableFocus]->setStateSelected(true); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mRightColumn && mEffectButtons.size() > 0) + { + if (mEffectFocus >= 0 && mEffectFocus < static_cast(mEffectButtons.size())) + mEffectButtons[mEffectFocus].first->setStateSelected(false); + mEffectFocus = wrap(mEffectFocus + 1, mEffectButtons.size()); + mEffectButtons[mEffectFocus].first->setStateSelected(true); + } + else if (!mRightColumn && mAvailableButtons.size() > 0) + { + if (mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + mAvailableButtons[mAvailableFocus]->setStateSelected(false); + mAvailableFocus = wrap(mAvailableFocus + 1, mAvailableButtons.size()); + mAvailableButtons[mAvailableFocus]->setStateSelected(true); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT && mRightColumn) + { + mRightColumn = false; + if (mEffectFocus >= 0 && mEffectFocus < static_cast(mEffectButtons.size())) + mEffectButtons[mEffectFocus].first->setStateSelected(false); + if (mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + mAvailableButtons[mAvailableFocus]->setStateSelected(true); + + winMgr->setControllerTooltip(Settings::gui().mControllerTooltips); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT && !mRightColumn && mEffectButtons.size() > 0) + { + mRightColumn = true; + if (mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + mAvailableButtons[mAvailableFocus]->setStateSelected(false); + if (mEffectFocus >= 0 && mEffectFocus < static_cast(mEffectButtons.size())) + mEffectButtons[mEffectFocus].first->setStateSelected(true); + + winMgr->setControllerTooltip(false); + } + else + return true; + + // Scroll the list to keep the active item in view + if (mAvailableFocus <= 5) + mAvailableEffectsList->setViewOffset(0); + else + { + const int lineHeight = Settings::gui().mFontSize + 3; + mAvailableEffectsList->setViewOffset(-lineHeight * (mAvailableFocus - 5)); + } + + if (!mRightColumn && mAvailableFocus >= 0 && mAvailableFocus < static_cast(mAvailableButtons.size())) + { + // Warp the mouse to the selected spell to show the tooltip + if (winMgr->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(mAvailableButtons[mAvailableFocus]); + } + + return true; + } } diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index 0887dd8c94..f2c440d305 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -7,6 +7,7 @@ #include #include "referenceinterface.hpp" +#include "widgets.hpp" #include "windowbase.hpp" namespace Gui @@ -83,13 +84,18 @@ namespace MWGui void updateBoxes(); - protected: + private: ESM::ENAMstruct mEffect; ESM::ENAMstruct mOldEffect; const ESM::MagicEffect* mMagicEffect; bool mConstantEffect; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void updateControllerFocus(int prevFocus, int newFocus); + int mControllerFocus; + std::vector mButtons; }; class EffectEditorBase @@ -142,8 +148,16 @@ namespace MWGui virtual void notifyEffectsChanged() {} + virtual bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg); + private: Type mType; + + int mAvailableFocus; + int mEffectFocus; + bool mRightColumn; + std::vector mAvailableButtons; + std::vector> mEffectButtons; }; class SpellCreationDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase @@ -166,6 +180,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* sender); void onBuyButtonClicked(MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; void notifyEffectsChanged() override; diff --git a/apps/openmw/mwgui/spellview.cpp b/apps/openmw/mwgui/spellview.cpp index 678f6ffe1f..ff77284bdf 100644 --- a/apps/openmw/mwgui/spellview.cpp +++ b/apps/openmw/mwgui/spellview.cpp @@ -9,7 +9,12 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" +#include "../mwbase/windowmanager.hpp" + #include "tooltips.hpp" +#include "windowbase.hpp" namespace MWGui { @@ -28,6 +33,8 @@ namespace MWGui : mScrollView(nullptr) , mShowCostColumn(true) , mHighlightSelected(true) + , mControllerActiveWindow(false) + , mControllerFocus(0) { } @@ -88,6 +95,8 @@ namespace MWGui const int spellHeight = Settings::gui().mFontSize + 2; mLines.clear(); + mButtons.clear(); + mGroupIndices.clear(); while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); @@ -106,7 +115,9 @@ namespace MWGui curType = spell.mType; } - const std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; + std::string skin = spell.mActive ? "SandTextButton" : "SpellTextUnequipped"; + if (Settings::gui().mControllerMenus) + skin = spell.mActive ? "SpellTextEquippedController" : "SpellTextUnequippedController"; const std::string captionSuffix = MWGui::ToolTips::getCountString(spell.mCount); Gui::SharedStateButton* t = mScrollView->createWidget( @@ -115,6 +126,7 @@ namespace MWGui t->setCaption(spell.mName + captionSuffix); t->setTextAlign(MyGUI::Align::Left); adjustSpellWidget(spell, i, t); + mButtons.emplace_back(std::make_pair(t, i)); if (!spell.mCostColumn.empty() && mShowCostColumn) { @@ -132,7 +144,7 @@ namespace MWGui mLines.emplace_back(t, costChance, i); } else - mLines.emplace_back(t, (MyGUI::Widget*)nullptr, i); + mLines.emplace_back(t, static_cast(nullptr), i); t->setStateSelected(spell.mSelected); } @@ -221,6 +233,12 @@ namespace MWGui height += lineHeight; } + if (Settings::gui().mControllerMenus) + { + mControllerFocus = wrap(mControllerFocus, mButtons.size()); + updateControllerFocus(-1, mControllerFocus); + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mScrollView->setVisibleVScroll(false); @@ -235,7 +253,7 @@ namespace MWGui MyGUI::ImageBox* separator = mScrollView->createWidget( "MW_HLine", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 18), MyGUI::Align::Left | MyGUI::Align::Top); separator->setNeedMouseFocus(false); - mLines.emplace_back(separator, (MyGUI::Widget*)nullptr, NoSpellIndex); + mLines.emplace_back(separator, static_cast(nullptr), NoSpellIndex); } MyGUI::TextBox* groupWidget = mScrollView->createWidget("SandBrightText", @@ -255,7 +273,9 @@ namespace MWGui mLines.emplace_back(groupWidget, groupWidget2, NoSpellIndex); } else - mLines.emplace_back(groupWidget, (MyGUI::Widget*)nullptr, NoSpellIndex); + mLines.emplace_back(groupWidget, static_cast(nullptr), NoSpellIndex); + + mGroupIndices.push_back(mButtons.size()); } void SpellView::setSize(const MyGUI::IntSize& _value) @@ -316,4 +336,124 @@ namespace MWGui { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } + + void SpellView::setActiveControllerWindow(bool active) + { + mControllerActiveWindow = active; + if (active) + update(); + } + + void SpellView::onControllerButton(const unsigned char button) + { + if (mButtons.empty()) + return; + + int prevFocus = mControllerFocus; + + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: + // Select the focused item, if any. + if (mControllerFocus >= 0 && mControllerFocus < static_cast(mButtons.size())) + { + onSpellSelected(mButtons[mControllerFocus].first); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + // Toggle info tooltip + MWBase::Environment::get().getWindowManager()->setControllerTooltip( + !MWBase::Environment::get().getWindowManager()->getControllerTooltip()); + break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + mControllerFocus--; + break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + mControllerFocus++; + break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + mControllerFocus = std::max(0, mControllerFocus - 10); + break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + mControllerFocus = std::min(mControllerFocus + 10, static_cast(mButtons.size()) - 1); + break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + { + // Jump to first item in previous group + int prevGroupIndex = 0; + for (int groupIndex : mGroupIndices) + { + if (groupIndex >= mControllerFocus) + break; + else + prevGroupIndex = groupIndex; + } + mControllerFocus = prevGroupIndex; + } + break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + { + // Jump to first item in next group + int newFocus = mControllerFocus; + for (int groupIndex : mGroupIndices) + { + if (groupIndex > mControllerFocus) + { + newFocus = groupIndex; + break; + } + } + // If on last group, jump to bottom of whole list + if (newFocus == mControllerFocus) + newFocus = mButtons.size() - 1; + mControllerFocus = newFocus; + } + break; + default: + return; + } + + mControllerFocus = wrap(mControllerFocus, mButtons.size()); + + if (prevFocus != mControllerFocus) + updateControllerFocus(prevFocus, mControllerFocus); + else + updateControllerFocus(-1, mControllerFocus); + } + + void SpellView::updateControllerFocus(int prevFocus, int newFocus) + { + if (mButtons.empty()) + return; + + if (prevFocus >= 0 && prevFocus < static_cast(mButtons.size())) + { + Gui::SharedStateButton* prev = mButtons[prevFocus].first; + if (prev) + prev->onMouseLostFocus(nullptr); + } + + if (mControllerActiveWindow && newFocus >= 0 && newFocus < static_cast(mButtons.size())) + { + Gui::SharedStateButton* focused = mButtons[newFocus].first; + if (focused) + { + focused->onMouseSetFocus(nullptr); + + // Scroll the list to keep the active item in view + int line = mButtons[newFocus].second; + if (line <= 5) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + { + const int lineHeight = focused->getHeight(); + mScrollView->setViewOffset(MyGUI::IntPoint(0, -lineHeight * (line - 5))); + } + + if (MWBase::Environment::get().getWindowManager()->getControllerTooltip()) + MWBase::Environment::get().getInputManager()->warpMouseToWidget(focused); + } + } + } } diff --git a/apps/openmw/mwgui/spellview.hpp b/apps/openmw/mwgui/spellview.hpp index caff43a33e..7895b86ec8 100644 --- a/apps/openmw/mwgui/spellview.hpp +++ b/apps/openmw/mwgui/spellview.hpp @@ -13,6 +13,11 @@ namespace MyGUI class ScrollView; } +namespace Gui +{ + class SharedStateButton; +} + namespace MWGui { @@ -54,6 +59,9 @@ namespace MWGui void resetScrollbars(); + void setActiveControllerWindow(bool active); + void onControllerButton(const unsigned char button); + private: MyGUI::ScrollView* mScrollView; @@ -89,6 +97,15 @@ namespace MWGui void addGroup(const std::string& label1, const std::string& label2); void adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget); + /// Keep a list of buttons for controller navigation and their index in the full list. + std::vector> mButtons; + /// Keep a list of group offsets for controller navigation + std::vector mGroupIndices; + + bool mControllerActiveWindow; + int mControllerFocus; + void updateControllerFocus(int prevFocus, int newFocus); + void onSpellSelected(MyGUI::Widget* _sender); void onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index 566b7f4ccd..78c7fabbff 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -27,6 +29,7 @@ #include "confirmationdialog.hpp" #include "spellicons.hpp" #include "spellview.hpp" +#include "statswindow.hpp" namespace MWGui { @@ -55,6 +58,14 @@ namespace MWGui // Adjust the spell filtering widget size because of MyGUI limitations. int filterWidth = mSpellView->getSize().width - deleteButton->getSize().width - 3; mFilterEdit->setSize(filterWidth, mFilterEdit->getSize().height); + + if (Settings::gui().mControllerMenus) + { + setPinButtonVisible(false); + mControllerButtons.mA = "#{sSelect}"; + mControllerButtons.mB = "#{sBack}"; + mControllerButtons.mR3 = "#{sInfo}"; + } } void SpellWindow::onPinToggled() @@ -66,7 +77,9 @@ namespace MWGui void SpellWindow::onTitleDoubleClicked() { - if (MyGUI::InputManager::getInstance().isShiftPressed()) + if (Settings::gui().mControllerMenus) + return; + else if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); @@ -288,4 +301,39 @@ namespace MWGui onSpellSelected(selectedSpell.mId); } } + + bool SpellWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + else + mSpellView->onControllerButton(arg.button); + + return true; + } + + void SpellWindow::setActiveControllerWindow(bool active) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + if (winMgr->getMode() == MWGui::GM_Inventory) + { + // Fill the screen, or limit to a certain size on large screens. Size chosen to + // match the size of the stats window. + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + int width = std::min(viewSize.width, StatsWindow::getIdealWidth()); + int height = std::min(winMgr->getControllerMenuHeight(), StatsWindow::getIdealHeight()); + int x = (viewSize.width - width) / 2; + int y = (viewSize.height - height) / 2; + + MyGUI::Window* window = mMainWidget->castType(); + window->setCoord(x, active ? y : viewSize.height + 1, width, height); + + MWBase::Environment::get().getWindowManager()->setControllerTooltip( + active && Settings::gui().mControllerTooltips); + } + + mSpellView->setActiveControllerWindow(active); + + WindowBase::setActiveControllerWindow(active); + } } diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index e35c5cdc4c..c27ec276a3 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -41,6 +41,8 @@ namespace MWGui void onPinToggled() override; void onTitleDoubleClicked() override; void onOpen() override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; SpellView* mSpellView; std::unique_ptr mSpellIcons; diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 69f0b4b449..9ae598052a 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,14 @@ namespace MWGui MyGUI::Window* t = mMainWidget->castType(); t->eventWindowChangeCoord += MyGUI::newDelegate(this, &StatsWindow::onWindowResize); + if (Settings::gui().mControllerMenus) + { + setPinButtonVisible(false); + mControllerButtons.mLStick = "#{sMouse}"; + mControllerButtons.mRStick = "#{sScrolldown}"; + mControllerButtons.mB = "#{sBack}"; + } + onWindowResize(t); } @@ -714,7 +723,9 @@ namespace MWGui void StatsWindow::onTitleDoubleClicked() { - if (MyGUI::InputManager::getInstance().isShiftPressed()) + if (Settings::gui().mControllerMenus) + return; + else if (MyGUI::InputManager::getInstance().isShiftPressed()) { MWBase::Environment::get().getWindowManager()->toggleMaximized(this); MyGUI::Window* t = mMainWidget->castType(); @@ -723,4 +734,35 @@ namespace MWGui else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); } + + bool StatsWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_B) + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + + return true; + } + + void StatsWindow::setActiveControllerWindow(bool active) + { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + if (winMgr->getMode() == MWGui::GM_Inventory) + { + // Fill the screen, or limit to a certain size on large screens. Size chosen to + // show all stats. + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + int width = std::min(viewSize.width, getIdealWidth()); + int height = std::min(winMgr->getControllerMenuHeight(), getIdealHeight()); + int x = (viewSize.width - width) / 2; + int y = (viewSize.height - height) / 2; + + MyGUI::Window* window = mMainWidget->castType(); + window->setCoord(x, active ? y : viewSize.height + 1, width, height); + + if (active) + onWindowResize(window); + } + + WindowBase::setActiveControllerWindow(active); + } } diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index a3fc3157c5..c1564498c2 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -13,6 +13,10 @@ namespace MWGui public: typedef std::map FactionList; + /// It would be nice to measure these, but for now they're hardcoded. + static int getIdealHeight() { return 750; } + static int getIdealWidth() { return 600; } + StatsWindow(DragAndDrop* drag); /// automatically updates all the data in the stats window, but only if it has changed. @@ -47,6 +51,10 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Stats"; } + protected: + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; + private: void addSkills(const std::vector& skills, const std::string& titleId, const std::string& titleDefault, MyGUI::IntCoord& coord1, MyGUI::IntCoord& coord2); diff --git a/apps/openmw/mwgui/textinput.cpp b/apps/openmw/mwgui/textinput.cpp index 5f47b96f03..d059ac9603 100644 --- a/apps/openmw/mwgui/textinput.cpp +++ b/apps/openmw/mwgui/textinput.cpp @@ -7,6 +7,8 @@ #include #include +#include + namespace MWGui { @@ -25,6 +27,8 @@ namespace MWGui // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); + + mControllerButtons.mA = "#{Interface:OK}"; } void TextInputDialog::setNextButtonShow(bool shown) @@ -83,4 +87,15 @@ namespace MWGui mTextEdit->setCaption(text); } + bool TextInputDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + onOkClicked(nullptr); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + return true; + } + + return false; + } } diff --git a/apps/openmw/mwgui/textinput.hpp b/apps/openmw/mwgui/textinput.hpp index c11d40f1a9..ad7896ff27 100644 --- a/apps/openmw/mwgui/textinput.hpp +++ b/apps/openmw/mwgui/textinput.hpp @@ -27,6 +27,7 @@ namespace MWGui protected: void onOkClicked(MyGUI::Widget* _sender); void onTextAccepted(MyGUI::EditBox* _sender); + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; private: MyGUI::EditBox* mTextEdit; diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 7f8de572ed..b705fda64b 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -94,7 +94,7 @@ namespace MWGui if (guiMode) { - if (!winMgr->getCursorVisible()) + if (!winMgr->getCursorVisible() && !winMgr->getControllerTooltip()) return; const MyGUI::IntPoint& mousePos = MyGUI::InputManager::getInstance().getMousePosition(); diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index bf5d4d4279..0422162eae 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -169,6 +171,29 @@ namespace MWGui std::numeric_limits::min() + 1); // disallow INT_MIN since abs(INT_MIN) is undefined setCoord(400, 0, 400, 300); + + if (Settings::gui().mControllerMenus) + { + // Show L1 and R1 buttons next to tabs + MyGUI::ImageBox* image; + getWidget(image, "BtnL1Image"); + image->setVisible(true); + image->setUserString("Hidden", "false"); + image->setImageTexture(MWBase::Environment::get().getInputManager()->getControllerButtonIcon( + SDL_CONTROLLER_BUTTON_LEFTSHOULDER)); + + getWidget(image, "BtnR1Image"); + image->setVisible(true); + image->setUserString("Hidden", "false"); + image->setImageTexture(MWBase::Environment::get().getInputManager()->getControllerButtonIcon( + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)); + + mControllerButtons.mA = "#{sBuy}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + mControllerButtons.mX = "#{sOffer}"; + mControllerButtons.mR3 = "#{sInfo}"; + mControllerButtons.mL2 = "#{sInventory}"; + } } void TradeWindow::setPtr(const MWWorld::Ptr& actor) @@ -203,6 +228,10 @@ namespace MWGui onFilterChanged(mFilterAll); mFilterEdit->setCaption({}); + // Cycle to the buy window if it's not active. + if (Settings::gui().mControllerMenus && !mActiveControllerWindow) + MWBase::Environment::get().getWindowManager()->cycleActiveControllerWindow(true); + for (const auto& source : itemSources) source.getClass().getContainerStore(source).setContListener(this); } @@ -353,6 +382,13 @@ namespace MWGui } } + void TradeWindow::onOfferSubmitted(MyGUI::Widget* _sender, size_t offerAmount) + { + mCurrentBalance = offerAmount * (mCurrentBalance < 0 ? -1 : 1); + updateLabels(); + onOfferButtonClicked(mOfferButton); + } + void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { TradeItemModel* playerItemModel @@ -658,6 +694,91 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } + bool TradeWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + int index = mItemView->getControllerFocus(); + if (index >= 0 && index < mItemView->getItemCount()) + onItemSelected(index); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_X) + { + if (mCurrentBalance == 0) + return true; + // Show a count dialog to allow for bartering. + CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); + if (mCurrentBalance < 0) + { + // Buying from the merchant + dialog->openCountDialog("#{sTotalcost}:", "#{sOffer}", -mCurrentMerchantOffer); + dialog->setCount(-mCurrentBalance); + } + else + { + // Selling to the merchant + dialog->openCountDialog("#{sTotalsold}:", "#{sOffer}", getMerchantGold()); + dialog->setCount(mCurrentBalance); + } + dialog->eventOkClicked.clear(); + dialog->eventOkClicked += MyGUI::newDelegate(this, &TradeWindow::onOfferSubmitted); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + if (mFilterAll->getStateSelected()) + onFilterChanged(mFilterMisc); + else if (mFilterWeapon->getStateSelected()) + onFilterChanged(mFilterAll); + else if (mFilterApparel->getStateSelected()) + onFilterChanged(mFilterWeapon); + else if (mFilterMagic->getStateSelected()) + onFilterChanged(mFilterApparel); + else if (mFilterMisc->getStateSelected()) + onFilterChanged(mFilterMagic); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + if (mFilterAll->getStateSelected()) + onFilterChanged(mFilterWeapon); + else if (mFilterWeapon->getStateSelected()) + onFilterChanged(mFilterApparel); + else if (mFilterApparel->getStateSelected()) + onFilterChanged(mFilterMagic); + else if (mFilterMagic->getStateSelected()) + onFilterChanged(mFilterMisc); + else if (mFilterMisc->getStateSelected()) + onFilterChanged(mFilterAll); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSTICK || arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN || arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT + || arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + { + mItemView->onControllerButton(arg.button); + } + + return true; + } + + void TradeWindow::setActiveControllerWindow(bool active) + { + // Show L1 and R1 buttons next to tabs + MyGUI::Widget* image; + getWidget(image, "BtnL1Image"); + image->setVisible(active); + + getWidget(image, "BtnR1Image"); + image->setVisible(active); + + mItemView->setActiveControllerWindow(active); + WindowBase::setActiveControllerWindow(active); + } + void TradeWindow::updateItemView() { mItemView->update(); diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index 5a3889d2d8..2e7628b13f 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -49,6 +49,9 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "Trade"; } + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + void setActiveControllerWindow(bool active) override; + private: friend class InventoryWindow; @@ -113,6 +116,7 @@ namespace MWGui void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onBalanceValueChanged(int value); void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); + void onOfferSubmitted(MyGUI::Widget* _sender, size_t offerAmount); void addRepeatController(MyGUI::Widget* widget); diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 3fc8412d4c..0bb8dbf9ba 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -36,6 +36,13 @@ namespace MWGui mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &TrainingWindow::onTrainingProgressChanged); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &TrainingWindow::onTrainingFinished); + + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sBuy}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } void TrainingWindow::onOpen() @@ -104,6 +111,7 @@ namespace MWGui const int lineHeight = Settings::gui().mFontSize + 2; + mTrainingButtons.clear(); for (size_t i = 0; i < skills.size(); ++i) { const ESM::Skill* skill = skills[i].first; @@ -127,6 +135,16 @@ namespace MWGui button->setSize(button->getTextSize().width + 12, button->getSize().height); ToolTips::createSkillToolTip(button, skill->mId); + + if (price <= playerGold) + mTrainingButtons.emplace_back(button); + } + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mTrainingButtons.size() > 0) + mTrainingButtons[0]->setStateSelected(true); } center(); @@ -228,4 +246,36 @@ namespace MWGui return !mTimeAdvancer.isRunning(); } + bool TrainingWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mTrainingButtons.size()) + onTrainingSelected(mTrainingButtons[mControllerFocus]); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mTrainingButtons.size() <= 1) + return true; + + setControllerFocus(mTrainingButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mTrainingButtons.size()); + setControllerFocus(mTrainingButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mTrainingButtons.size() <= 1) + return true; + + setControllerFocus(mTrainingButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mTrainingButtons.size()); + setControllerFocus(mTrainingButtons, mControllerFocus, true); + } + + return true; + } } diff --git a/apps/openmw/mwgui/trainingwindow.hpp b/apps/openmw/mwgui/trainingwindow.hpp index ee13f24b23..f44c5524f5 100644 --- a/apps/openmw/mwgui/trainingwindow.hpp +++ b/apps/openmw/mwgui/trainingwindow.hpp @@ -49,9 +49,13 @@ namespace MWGui MyGUI::Widget* mTrainingOptions; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; + std::vector mTrainingButtons; WaitDialogProgressBar mProgressBar; TimeAdvancer mTimeAdvancer; + + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; }; } diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index e001cf9b43..9836887c2b 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -37,6 +37,13 @@ namespace MWGui getWidget(mDestinationsView, "DestinationsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onCancelButtonClicked); + + if (Settings::gui().mControllerMenus) + { + mDisableGamepadCursor = true; + mControllerButtons.mA = "#{sTravel}"; + mControllerButtons.mB = "#{Interface:Cancel}"; + } } void TravelWindow::addDestination(const ESM::RefId& name, const ESM::Position& pos, bool interior) @@ -94,6 +101,8 @@ namespace MWGui toAdd->setUserString("Destination", nameString); toAdd->setUserData(pos); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onTravelButtonClick); + if (price <= playerGold) + mDestinationButtons.emplace_back(toAdd); } void TravelWindow::clearDestinations() @@ -102,6 +111,7 @@ namespace MWGui mCurrentY = 0; while (mDestinationsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mDestinationsView->getChildAt(0)); + mDestinationButtons.clear(); } void TravelWindow::setPtr(const MWWorld::Ptr& actor) @@ -146,6 +156,14 @@ namespace MWGui } updateLabels(); + + if (Settings::gui().mControllerMenus) + { + mControllerFocus = 0; + if (mDestinationButtons.size() > 0) + mDestinationButtons[0]->setStateSelected(true); + } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // scrollbar is hidden mDestinationsView->setVisibleVScroll(false); @@ -240,4 +258,49 @@ namespace MWGui mDestinationsView->setViewOffset( MyGUI::IntPoint(0, static_cast(mDestinationsView->getViewOffset().top + _rel * 0.3f))); } + + bool TravelWindow::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + if (mControllerFocus < mDestinationButtons.size()) + { + onTravelButtonClick(mDestinationButtons[mControllerFocus]); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + { + onCancelButtonClicked(mCancelButton); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_UP) + { + if (mDestinationButtons.size() <= 1) + return true; + + setControllerFocus(mDestinationButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus - 1, mDestinationButtons.size()); + setControllerFocus(mDestinationButtons, mControllerFocus, true); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) + { + if (mDestinationButtons.size() <= 1) + return true; + + setControllerFocus(mDestinationButtons, mControllerFocus, false); + mControllerFocus = wrap(mControllerFocus + 1, mDestinationButtons.size()); + setControllerFocus(mDestinationButtons, mControllerFocus, true); + } + + // Scroll the list to keep the active item in view + if (mControllerFocus <= 5) + mDestinationsView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + { + const int lineHeight = Settings::gui().mFontSize + 2; + mDestinationsView->setViewOffset(MyGUI::IntPoint(0, -lineHeight * (mControllerFocus - 5))); + } + + return true; + } } diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 630e27518a..c0002b805b 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -25,6 +25,8 @@ namespace MWGui MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; + std::vector mDestinationButtons; + MyGUI::ScrollView* mDestinationsView; void onCancelButtonClicked(MyGUI::Widget* _sender); @@ -37,6 +39,10 @@ namespace MWGui void updateLabels(); void onReferenceUnavailable() override; + + private: + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; + size_t mControllerFocus; }; } diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 9609def96d..95219cb521 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -79,6 +79,9 @@ namespace MWGui mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &WaitDialog::onWaitingProgressChanged); mTimeAdvancer.eventInterrupted += MyGUI::newDelegate(this, &WaitDialog::onWaitingInterrupted); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &WaitDialog::onWaitingFinished); + + mControllerButtons.mB = "#{Interface:Cancel}"; + mDisableGamepadCursor = Settings::gui().mControllerMenus; } void WaitDialog::setPtr(const MWWorld::Ptr& ptr) @@ -325,6 +328,45 @@ namespace MWGui } } + ControllerButtons* WaitDialog::getControllerButtons() + { + mControllerButtons.mA = mSleeping ? "#{sRest}" : "#{sWait}"; + mControllerButtons.mX = mSleeping && mUntilHealedButton->getVisible() ? "#{sUntilHealed}" : ""; + return &mControllerButtons; + } + + bool WaitDialog::onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) + { + if (arg.button == SDL_CONTROLLER_BUTTON_A) + { + onWaitButtonClicked(mWaitButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_B) + onCancelButtonClicked(mCancelButton); + else if (arg.button == SDL_CONTROLLER_BUTTON_X && mUntilHealedButton->getVisible()) + { + onUntilHealedButtonClicked(mUntilHealedButton); + MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowDown, 0, false); + else if (arg.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::ArrowUp, 0, false); + else if (arg.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + { + mHourSlider->setScrollPosition(0); + onHourSliderChangedPosition(mHourSlider, mHourSlider->getScrollPosition()); + } + else if (arg.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + { + mHourSlider->setScrollPosition(mHourSlider->getScrollRange() - 1); + onHourSliderChangedPosition(mHourSlider, mHourSlider->getScrollPosition()); + } + + return true; + } + void WaitDialog::stopWaiting() { MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f); diff --git a/apps/openmw/mwgui/waitdialog.hpp b/apps/openmw/mwgui/waitdialog.hpp index 3d66584f54..fb25ddc1fa 100644 --- a/apps/openmw/mwgui/waitdialog.hpp +++ b/apps/openmw/mwgui/waitdialog.hpp @@ -36,6 +36,7 @@ namespace MWGui void clear() override; void onFrame(float dt) override; + bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) override; bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; } void wakeUp(); @@ -45,6 +46,8 @@ namespace MWGui std::string_view getWindowIdForLua() const override { return "WaitDialog"; } + ControllerButtons* getControllerButtons() override; + protected: MyGUI::TextBox* mDateTimeText; MyGUI::TextBox* mRestText; diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index 6cc5bdfdf5..8995f04b92 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -14,6 +14,8 @@ #include #include +#include "textcolours.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -66,6 +68,12 @@ namespace MWGui::Widgets } } + void MWSkill::setStateSelected(bool selected) + { + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mSkillNameWidget->setTextColour(selected ? textColours.link : textColours.normal); + } + void MWSkill::onClicked(MyGUI::Widget* _sender) { eventClicked(this); @@ -150,6 +158,12 @@ namespace MWGui::Widgets } } + void MWAttribute::setStateSelected(bool selected) + { + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mAttributeNameWidget->setTextColour(selected ? textColours.link : textColours.normal); + } + void MWAttribute::initialiseOverride() { Base::initialiseOverride(); @@ -231,6 +245,12 @@ namespace MWGui::Widgets } } + void MWSpell::setStateSelected(bool selected) + { + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mSpellNameWidget->setTextColour(selected ? textColours.link : textColours.normal); + } + void MWSpell::initialiseOverride() { Base::initialiseOverride(); @@ -461,6 +481,12 @@ namespace MWGui::Widgets MWSpellEffect::~MWSpellEffect() {} + void MWSpellEffect::setStateSelected(bool selected) + { + const TextColours& textColours{ MWBase::Environment::get().getWindowManager()->getTextColours() }; + mTextWidget->setTextColour(selected ? textColours.link : textColours.normal); + } + void MWSpellEffect::initialiseOverride() { Base::initialiseOverride(); diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index d562e4e07f..c51846d9df 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -110,6 +110,8 @@ namespace MWGui */ EventHandle_SkillVoid eventClicked; + void setStateSelected(bool selected); + protected: virtual ~MWSkill(); @@ -149,6 +151,8 @@ namespace MWGui */ EventHandle_AttributeVoid eventClicked; + void setStateSelected(bool selected); + protected: ~MWAttribute() override = default; @@ -191,6 +195,8 @@ namespace MWGui const ESM::RefId& getSpellId() const { return mId; } + void setStateSelected(bool selected); + protected: virtual ~MWSpell(); @@ -256,6 +262,8 @@ namespace MWGui int getRequestedWidth() const { return mRequestedWidth; } + void setStateSelected(bool selected); + protected: virtual ~MWSpellEffect(); diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index f5d90590f8..d6a5cb9086 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -7,6 +7,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include #include #include "draganddrop.hpp" @@ -14,6 +15,22 @@ using namespace MWGui; +int MWGui::wrap(int index, int max) +{ + if (index < 0) + return max - 1; + else if (index >= max) + return 0; + else + return index; +} + +void MWGui::setControllerFocus(const std::vector& buttons, int index, bool focused) +{ + if (index >= 0 && index < static_cast(buttons.size())) + buttons[index]->setStateSelected(focused); +} + WindowBase::WindowBase(std::string_view parLayout) : Layout(parLayout) { @@ -122,6 +139,7 @@ void WindowModal::onOpen() void WindowModal::onClose() { MWBase::Environment::get().getWindowManager()->removeCurrentModal(this); + MWBase::Environment::get().getWindowManager()->updateControllerButtonsOverlay(); MyGUI::InputManager::getInstance().removeWidgetModal(mMainWidget); } diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 466060c6ad..5a84db4a5c 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -1,6 +1,8 @@ #ifndef MWGUI_WINDOW_BASE_H #define MWGUI_WINDOW_BASE_H +#include + #include "layout.hpp" namespace MWWorld @@ -12,6 +14,28 @@ namespace MWGui { class DragAndDrop; + int wrap(int index, int max); + void setControllerFocus(const std::vector& buttons, int index, bool selected); + + struct ControllerButtons + { + std::string mA; + std::string mB; + std::string mDpad; + std::string mL1; + std::string mL2; + std::string mL3; + std::string mLStick; + std::string mMenu; + std::string mR1; + std::string mR2; + std::string mR3; + std::string mRStick; + std::string mView; + std::string mX; + std::string mY; + }; + class WindowBase : public Layout { public: @@ -54,9 +78,21 @@ namespace MWGui static void clampWindowCoordinates(MyGUI::Window* window); + virtual ControllerButtons* getControllerButtons() { return &mControllerButtons; } + MyGUI::Widget* getControllerScrollWidget() { return mControllerScrollWidget; } + bool isGamepadCursorAllowed() { return !mDisableGamepadCursor; } + virtual bool onControllerButtonEvent(const SDL_ControllerButtonEvent& arg) { return true; } + virtual bool onControllerThumbstickEvent(const SDL_ControllerAxisEvent& arg) { return false; } + virtual void setActiveControllerWindow(bool active) { mActiveControllerWindow = active; } + protected: virtual void onTitleDoubleClicked(); + ControllerButtons mControllerButtons; + bool mActiveControllerWindow = false; + bool mDisableGamepadCursor = false; + MyGUI::Widget* mControllerScrollWidget = nullptr; + private: void onDoubleClick(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index bfacbd7e68..bcc67e42c1 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -83,6 +83,7 @@ #include "confirmationdialog.hpp" #include "console.hpp" #include "container.hpp" +#include "controllerbuttonsoverlay.hpp" #include "controllers.hpp" #include "countdialog.hpp" #include "cursor.hpp" @@ -91,6 +92,7 @@ #include "enchantingdialog.hpp" #include "exposedwindow.hpp" #include "hud.hpp" +#include "inventorytabsoverlay.hpp" #include "inventorywindow.hpp" #include "itemchargeview.hpp" #include "itemtransfer.hpp" @@ -182,6 +184,8 @@ namespace MWGui , mPostProcessorHud(nullptr) , mJailScreen(nullptr) , mContainerWindow(nullptr) + , mControllerButtonsOverlay(nullptr) + , mInventoryTabsOverlay(nullptr) , mTranslationDataStorage(translationDataStorage) , mInputBlocker(nullptr) , mHudEnabled(true) @@ -508,6 +512,15 @@ namespace MWGui mWindows.push_back(std::move(postProcessorHud)); trackWindow(mPostProcessorHud, makePostprocessorWindowSettingValues()); + auto controllerButtonsOverlay = std::make_unique(); + mControllerButtonsOverlay = controllerButtonsOverlay.get(); + mWindows.push_back(std::move(controllerButtonsOverlay)); + + auto inventoryTabsOverlay = std::make_unique(); + mInventoryTabsOverlay = inventoryTabsOverlay.get(); + mWindows.push_back(std::move(inventoryTabsOverlay)); + mActiveControllerWindows[GM_Inventory] = 1; // Start on Inventory page + mInputBlocker = MyGUI::Gui::getInstance().createWidget( {}, 0, 0, w, h, MyGUI::Align::Stretch, "InputBlocker"); @@ -665,6 +678,14 @@ namespace MWGui && !(mForceHidden & GW_Inventory) && (mAllowed & GW_Inventory)); mSpellWindow->setVisible( mSpellWindow->pinned() && !isConsoleMode() && !(mForceHidden & GW_Magic) && (mAllowed & GW_Magic)); + + if (Settings::gui().mControllerMenus) + { + if (mControllerButtonsOverlay) + mControllerButtonsOverlay->setVisible(false); + if (mInventoryTabsOverlay) + mInventoryTabsOverlay->setVisible(false); + } return; } else if (getMode() != GM_Inventory) @@ -694,6 +715,8 @@ namespace MWGui mStatsWindow->setVisible(eff & GW_Stats); } + updateControllerButtonsOverlay(); + switch (mode) { // FIXME: refactor chargen windows to use modes properly (or not use them at all) @@ -723,6 +746,7 @@ namespace MWGui return; dialog->setVisible(false); mGarbageDialogs.push_back(std::move(dialog)); + updateControllerButtonsOverlay(); } void WindowManager::exitCurrentGuiMode() @@ -862,6 +886,103 @@ namespace MWGui mHud->setPlayerPos(x, y, u, v); } + WindowBase* WindowManager::getActiveControllerWindow() + { + if (!mCurrentModals.empty()) + return mCurrentModals.back(); + + if (isSettingsWindowVisible()) + return mSettingsWindow; + + if (!mGuiModes.empty()) + { + GuiMode mode = mGuiModes.back(); + GuiModeState& state = mGuiModeStates[mode]; + if (state.mWindows.size() == 0) + return nullptr; + + int activeIndex + = std::clamp(mActiveControllerWindows[mode], 0, static_cast(state.mWindows.size()) - 1); + + // If the active window is no longer visible, find the next visible window. + if (!state.mWindows[activeIndex]->isVisible()) + cycleActiveControllerWindow(true); + + return state.mWindows[activeIndex]; + } + + return nullptr; + } + + void WindowManager::cycleActiveControllerWindow(bool next) + { + if (!Settings::gui().mControllerMenus || mGuiModes.empty()) + return; + + GuiMode mode = mGuiModes.back(); + int winCount = mGuiModeStates[mode].mWindows.size(); + + int activeIndex = 0; + if (winCount > 1) + { + // Find next/previous visible window + activeIndex = mActiveControllerWindows[mode]; + int delta = next ? 1 : -1; + + for (int i = 0; i < winCount; i++) + { + activeIndex = wrap(activeIndex + delta, winCount); + if (mGuiModeStates[mode].mWindows[activeIndex]->isVisible()) + break; + } + } + + if (mActiveControllerWindows[mode] != activeIndex) + setActiveControllerWindow(mode, activeIndex); + } + + void WindowManager::reapplyActiveControllerWindow() + { + if (!Settings::gui().mControllerMenus || mGuiModes.empty()) + return; + + const GuiMode mode = mGuiModes.back(); + int winCount = mGuiModeStates[mode].mWindows.size(); + + for (int i = 0; i < winCount; i++) + { + // Set active window last so inactive windows don't stomp on changes it makes, e.g. to tooltips. + if (i != mActiveControllerWindows[mode]) + mGuiModeStates[mode].mWindows[i]->setActiveControllerWindow(false); + } + if (winCount > 0) + mGuiModeStates[mode].mWindows[mActiveControllerWindows[mode]]->setActiveControllerWindow(true); + } + + void WindowManager::setActiveControllerWindow(GuiMode mode, int activeIndex) + { + if (!Settings::gui().mControllerMenus) + return; + + int winCount = mGuiModeStates[mode].mWindows.size(); + if (winCount == 0) + return; + + activeIndex = std::clamp(activeIndex, 0, winCount - 1); + mActiveControllerWindows[mode] = activeIndex; + + reapplyActiveControllerWindow(); + + MWBase::Environment::get().getInputManager()->setGamepadGuiCursorEnabled( + mGuiModeStates[mode].mWindows[activeIndex]->isGamepadCursorAllowed()); + + updateControllerButtonsOverlay(); + setCursorActive(false); + + if (winCount > 1) + playSound(ESM::RefId::stringRefId("Menu Size")); + } + void WindowManager::update(float frameDuration) { handleScheduledMessageBoxes(); @@ -925,6 +1046,12 @@ namespace MWGui if (isSettingsWindowVisible()) mSettingsWindow->onFrame(frameDuration); + if (mControllerButtonsOverlay && mControllerButtonsOverlay->isVisible()) + mControllerButtonsOverlay->onFrame(frameDuration); + + if (mInventoryTabsOverlay && mInventoryTabsOverlay->isVisible()) + mInventoryTabsOverlay->onFrame(frameDuration); + if (!gameRunning) return; @@ -1218,6 +1345,9 @@ namespace MWGui for (const auto& window : mWindows) window->onResChange(x, y); + // Re-apply any controller-specific window changes. + reapplyActiveControllerWindow(); + // TODO: check if any windows are now off-screen and move them back if so } @@ -1296,6 +1426,15 @@ namespace MWGui updateVisible(); MWBase::Environment::get().getLuaManager()->uiModeChanged(arg); + + if (Settings::gui().mControllerMenus) + { + if (mode == GM_Container) + mActiveControllerWindows[mode] = 0; // Ensure controller focus is on container + // Activate first visible window. This needs to be called after updateVisible. + mActiveControllerWindows[mode] = std::max(mActiveControllerWindows[mode] - 1, -1); + cycleActiveControllerWindow(true); + } } void WindowManager::setCullMask(uint32_t mask) @@ -1350,6 +1489,14 @@ namespace MWGui // To make sure that console window get focus again if (mConsole && mConsole->isVisible()) mConsole->onOpen(); + + if (Settings::gui().mControllerMenus) + { + if (mGuiModes.empty()) + setControllerTooltip(false); + else + reapplyActiveControllerWindow(); + } } void WindowManager::removeGuiMode(GuiMode mode) @@ -1475,6 +1622,10 @@ namespace MWGui mConsole->executeFile(path); } + std::vector WindowManager::getGuiModeWindows(GuiMode mode) + { + return mGuiModeStates[mode].mWindows; + } MWGui::InventoryWindow* WindowManager::getInventoryWindow() { return mInventoryWindow; @@ -1487,6 +1638,10 @@ namespace MWGui { return mConfirmationDialog; } + MWGui::HUD* WindowManager::getHud() + { + return mHud; + } MWGui::TradeWindow* WindowManager::getTradeWindow() { return mTradeWindow; @@ -1770,6 +1925,13 @@ namespace MWGui void WindowManager::onWindowChangeCoord(MyGUI::Window* window) { + // If using controller menus, don't persist changes to size of the stats or magic + // windows. + if (Settings::gui().mControllerMenus + && (window == mStatsWindow->mMainWidget->castType() + || window == mSpellWindow->mMainWidget->castType())) + return; + const auto it = mTrackedWindows.find(window); if (it == mTrackedWindows.end()) return; @@ -1963,6 +2125,7 @@ namespace MWGui if (!window->exit()) return; window->setVisible(false); + updateControllerButtonsOverlay(); } } @@ -1976,6 +2139,8 @@ namespace MWGui mKeyboardNavigation->setModalWindow(input->mMainWidget); mKeyboardNavigation->setDefaultFocus(input->mMainWidget, input->getDefaultKeyFocus()); + + updateControllerButtonsOverlay(); } void WindowManager::removeCurrentModal(WindowModal* input) @@ -2013,6 +2178,16 @@ namespace MWGui void WindowManager::updatePinnedWindows() { + if (Settings::gui().mControllerMenus) + { + // In controller mode, don't hide any menus and only allow pinning the map. + mInventoryWindow->setPinned(false); + mMap->setPinned(Settings::windows().mMapPin); + mSpellWindow->setPinned(false); + mStatsWindow->setPinned(false); + return; + } + mInventoryWindow->setPinned(Settings::windows().mInventoryPin); if (Settings::windows().mInventoryHidden) mShown = (GuiWindow)(mShown ^ GW_Inventory); @@ -2451,4 +2626,46 @@ namespace MWGui } return res; } + + int WindowManager::getControllerMenuHeight() + { + int height = MyGUI::RenderManager::getInstance().getViewSize().height; + if (mControllerButtonsOverlay != nullptr && mControllerButtonsOverlay->isVisible()) + height -= mControllerButtonsOverlay->getHeight(); + if (mInventoryTabsOverlay != nullptr && mInventoryTabsOverlay->isVisible()) + height -= mInventoryTabsOverlay->getHeight(); + return height; + } + + void WindowManager::setControllerTooltip(bool enabled) + { + if (!Settings::gui().mControllerMenus) + return; + + mControllerTooltip = enabled; + } + + void WindowManager::updateControllerButtonsOverlay() + { + if (!Settings::gui().mControllerMenus || !mControllerButtonsOverlay) + return; + + WindowBase* topWin = getActiveControllerWindow(); + if (!topWin || !topWin->isVisible()) + { + mControllerButtonsOverlay->setVisible(false); + mInventoryTabsOverlay->setVisible(false); + return; + } + + // setButtons will handle setting visibility based on if any buttons are defined. + mControllerButtonsOverlay->setButtons(topWin->getControllerButtons()); + if (getMode() == GM_Inventory) + { + mInventoryTabsOverlay->setVisible(true); + mInventoryTabsOverlay->setTab(mActiveControllerWindows[GM_Inventory]); + } + else + mInventoryTabsOverlay->setVisible(false); + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index c231718db6..81294d3cd4 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -118,6 +118,8 @@ namespace MWGui class JailScreen; class KeyboardNavigation; class ItemTransfer; + class ControllerButtonsOverlay; + class InventoryTabsOverlay; class WindowManager : public MWBase::WindowManager { @@ -183,7 +185,9 @@ namespace MWGui MWGui::CountDialog* getCountDialog() override; MWGui::ConfirmationDialog* getConfirmationDialog() override; MWGui::TradeWindow* getTradeWindow() override; + MWGui::HUD* getHud() override; MWGui::PostProcessorHud* getPostProcessorHud() override; + std::vector getGuiModeWindows(GuiMode mode) override; /// Make the player use an item, while updating GUI state accordingly void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions = false) override; @@ -387,6 +391,14 @@ namespace MWGui void asyncPrepareSaveMap() override; + WindowBase* getActiveControllerWindow() override; + int getControllerMenuHeight() override; + void cycleActiveControllerWindow(bool next) override; + void setActiveControllerWindow(GuiMode mode, int activeIndex) override; + bool getControllerTooltip() const override { return mControllerTooltip; } + void setControllerTooltip(bool enabled) override; + void updateControllerButtonsOverlay() override; + // Used in Lua bindings const std::vector& getGuiModeStack() const override { return mGuiModes; } void setDisabledByLua(std::string_view windowId, bool disabled) override; @@ -456,6 +468,8 @@ namespace MWGui PostProcessorHud* mPostProcessorHud; JailScreen* mJailScreen; ContainerWindow* mContainerWindow; + ControllerButtonsOverlay* mControllerButtonsOverlay; + InventoryTabsOverlay* mInventoryTabsOverlay; std::vector> mWindows; @@ -495,6 +509,11 @@ namespace MWGui std::map mGuiModeStates; // The currently active stack of GUI modes (top mode is the one we are in). std::vector mGuiModes; + // The active window for controller mode for each GUI mode. + std::map mActiveControllerWindows; + bool mControllerTooltip; + + void reapplyActiveControllerWindow(); std::unique_ptr mCursorManager; diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 0bba8bfa32..f32bf741db 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -16,6 +16,7 @@ #include "../mwbase/luamanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwgui/windowbase.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" @@ -31,6 +32,7 @@ namespace MWInput , mGamepadGuiCursorEnabled(true) , mGuiCursorEnabled(true) , mJoystickLastUsed(false) + , mGamepadMousePressed(false) { if (!controllerBindingsFile.empty()) { @@ -141,6 +143,7 @@ namespace MWInput if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonPress(SDL_BUTTON_LEFT); + mGamepadMousePressed = true; if (MyGUI::InputManager::getInstance().getMouseFocusWidget()) { MyGUI::Button* b @@ -185,12 +188,13 @@ namespace MWInput mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { - if (mGamepadGuiCursorEnabled) + if (mGamepadGuiCursorEnabled && (!Settings::gui().mControllerMenus || mGamepadMousePressed)) { // Temporary mouse binding until keyboard controls are available: if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonRelease(SDL_BUTTON_LEFT); + mGamepadMousePressed = false; if (mBindingsManager->isDetectingBindingState()) // If the player just triggered binding, don't let // button release bind. return; @@ -217,7 +221,8 @@ namespace MWInput mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { - gamepadToGuiControl(arg); + if (gamepadToGuiControl(arg)) + return; } else if (mBindingsManager->actionIsActive(A_TogglePOV) && (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)) @@ -241,6 +246,33 @@ namespace MWInput bool ControllerManager::gamepadToGuiControl(const SDL_ControllerButtonEvent& arg) { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + + if (Settings::gui().mControllerMenus) + { + // Update cursor state. + bool treatAsMouse = winMgr->getCursorVisible(); + winMgr->setCursorActive(false); + + MWGui::WindowBase* topWin = winMgr->getActiveControllerWindow(); + if (topWin) + { + // When the inventory tooltip is visible, we don't actually want the A button to + // act like a mouse button; it should act normally. + if (treatAsMouse && arg.button == SDL_CONTROLLER_BUTTON_A && winMgr->getControllerTooltip()) + treatAsMouse = false; + + mGamepadGuiCursorEnabled = topWin->isGamepadCursorAllowed(); + + // Fall through to mouse click + if (mGamepadGuiCursorEnabled && treatAsMouse && arg.button == SDL_CONTROLLER_BUTTON_A) + return false; + + if (topWin->onControllerButtonEvent(arg)) + return true; + } + } + // Presumption of GUI mode will be removed in the future. // MyGUI KeyCodes *may* change. MyGUI::KeyCode key = MyGUI::KeyCode::None; @@ -266,9 +298,9 @@ namespace MWInput break; case SDL_CONTROLLER_BUTTON_B: if (MyGUI::InputManager::getInstance().isModalAny()) - MWBase::Environment::get().getWindowManager()->exitCurrentModal(); + winMgr->exitCurrentModal(); else - MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + winMgr->exitCurrentGuiMode(); return true; case SDL_CONTROLLER_BUTTON_X: key = MyGUI::KeyCode::Semicolon; @@ -278,7 +310,7 @@ namespace MWInput break; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::LeftShift); - MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); + winMgr->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::LeftShift); return true; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: @@ -286,7 +318,7 @@ namespace MWInput return true; case SDL_CONTROLLER_BUTTON_LEFTSTICK: mGamepadGuiCursorEnabled = !mGamepadGuiCursorEnabled; - MWBase::Environment::get().getWindowManager()->setCursorActive(mGamepadGuiCursorEnabled); + winMgr->setCursorActive(mGamepadGuiCursorEnabled); return true; default: return false; @@ -296,21 +328,87 @@ namespace MWInput if (SDL_IsTextInputActive()) return false; - MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); + winMgr->injectKeyPress(key, 0, false); return true; } bool ControllerManager::gamepadToGuiControl(const SDL_ControllerAxisEvent& arg) { + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + + if (Settings::gui().mControllerMenus) + { + // Left and right triggers toggle through open GUI windows. + if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) + { + if (arg.value == 32767) // Treat like a button. + winMgr->cycleActiveControllerWindow(true); + return true; + } + else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) + { + if (arg.value == 32767) // Treat like a button. + winMgr->cycleActiveControllerWindow(false); + return true; + } + + MWGui::WindowBase* topWin = winMgr->getActiveControllerWindow(); + if (topWin) + { + // Update cursor state + mGamepadGuiCursorEnabled = topWin->isGamepadCursorAllowed(); + if (!mGamepadGuiCursorEnabled) + winMgr->setCursorActive(false); + + // Deadzone check + if (std::abs(arg.value) < 2000) + return !mGamepadGuiCursorEnabled; + + if (mGamepadGuiCursorEnabled + && (arg.axis == SDL_CONTROLLER_AXIS_LEFTX || arg.axis == SDL_CONTROLLER_AXIS_LEFTY)) + { + // Treat the left stick like a cursor, which is the default behavior. + if (winMgr->getControllerTooltip()) + { + winMgr->setControllerTooltip(false); + winMgr->setCursorVisible(true); + } + else if (mGamepadGuiCursorEnabled) + { + winMgr->setCursorVisible(true); + } + return false; + } + + // Some windows have a specific widget to scroll with the right stick. Move the mouse there. + if (arg.axis == SDL_CONTROLLER_AXIS_RIGHTY && topWin->getControllerScrollWidget() != nullptr) + { + mMouseManager->warpMouseToWidget(topWin->getControllerScrollWidget()); + winMgr->setCursorVisible(false); + } + + if (topWin->onControllerThumbstickEvent(arg)) + { + // Window handled the event. + return true; + } + else if (arg.axis == SDL_CONTROLLER_AXIS_RIGHTX || arg.axis == SDL_CONTROLLER_AXIS_RIGHTY) + { + // Only right-stick scroll if mouse is visible or there's a widget to scroll. + return !winMgr->getCursorVisible() && topWin->getControllerScrollWidget() == nullptr; + } + } + } + switch (arg.axis) { case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: if (arg.value == 32767) // Treat like a button. - MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Minus, 0, false); + winMgr->injectKeyPress(MyGUI::KeyCode::Minus, 0, false); break; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: if (arg.value == 32767) // Treat like a button. - MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Equals, 0, false); + winMgr->injectKeyPress(MyGUI::KeyCode::Equals, 0, false); break; case SDL_CONTROLLER_AXIS_LEFTX: case SDL_CONTROLLER_AXIS_LEFTY: @@ -380,6 +478,106 @@ namespace MWInput return std::array({ gyro[0], gyro[1], gyro[2] }); } + int ControllerManager::getControllerType() + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (cntrl) + return SDL_GameControllerGetType(cntrl); + return 0; + } + + std::string ControllerManager::getControllerButtonIcon(int button) + { + int controllerType = ControllerManager::getControllerType(); + + bool isXbox = controllerType == SDL_CONTROLLER_TYPE_XBOX360 || controllerType == SDL_CONTROLLER_TYPE_XBOXONE; + bool isPsx = controllerType == SDL_CONTROLLER_TYPE_PS3 || controllerType == SDL_CONTROLLER_TYPE_PS4 + || controllerType == SDL_CONTROLLER_TYPE_PS5; + bool isSwitch = controllerType == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO; + + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: + if (isPsx) + return "textures/omw_psx_button_x.dds"; + return "textures/omw_steam_button_a.dds"; + case SDL_CONTROLLER_BUTTON_B: + if (isPsx) + return "textures/omw_psx_button_circle.dds"; + return "textures/omw_steam_button_b.dds"; + case SDL_CONTROLLER_BUTTON_BACK: + return "textures/omw_steam_button_view.dds"; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + case SDL_CONTROLLER_BUTTON_DPAD_UP: + if (isPsx) + return "textures/omw_psx_button_dpad.dds"; + return "textures/omw_steam_button_dpad.dds"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + if (isXbox) + return "textures/omw_xbox_button_lb.dds"; + else if (isSwitch) + return "textures/omw_switch_button_l.dds"; + return "textures/omw_steam_button_l1.dds"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return "textures/omw_steam_button_l3.dds"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + if (isXbox) + return "textures/omw_xbox_button_rb.dds"; + else if (isSwitch) + return "textures/omw_switch_button_r.dds"; + return "textures/omw_steam_button_r1.dds"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return "textures/omw_steam_button_r3.dds"; + case SDL_CONTROLLER_BUTTON_START: + return "textures/omw_steam_button_menu.dds"; + case SDL_CONTROLLER_BUTTON_X: + if (isPsx) + return "textures/omw_psx_button_square.dds"; + return "textures/omw_steam_button_x.dds"; + case SDL_CONTROLLER_BUTTON_Y: + if (isPsx) + return "textures/omw_psx_button_triangle.dds"; + return "textures/omw_steam_button_y.dds"; + case SDL_CONTROLLER_BUTTON_GUIDE: + default: + return {}; + } + } + + std::string ControllerManager::getControllerAxisIcon(int axis) + { + int controllerType = ControllerManager::getControllerType(); + + bool isXbox = controllerType == SDL_CONTROLLER_TYPE_XBOX360 || controllerType == SDL_CONTROLLER_TYPE_XBOXONE; + bool isSwitch = controllerType == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO; + + switch (axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + case SDL_CONTROLLER_AXIS_LEFTY: + return "textures/omw_steam_button_lstick.dds"; + case SDL_CONTROLLER_AXIS_RIGHTX: + case SDL_CONTROLLER_AXIS_RIGHTY: + return "textures/omw_steam_button_rstick.dds"; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + if (isXbox) + return "textures/omw_xbox_button_lt.dds"; + else if (isSwitch) + return "textures/omw_switch_button_zl.dds"; + return "textures/omw_steam_button_l2.dds"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + if (isXbox) + return "textures/omw_xbox_button_rt.dds"; + else if (isSwitch) + return "textures/omw_switch_button_zr.dds"; + return "textures/omw_steam_button_r2.dds"; + default: + return {}; + } + } + void ControllerManager::touchpadMoved(int deviceId, const SDLUtil::TouchEvent& arg) { MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::TouchMoved, arg }); diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index 596a9ef465..535ee85fd5 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -48,6 +48,9 @@ namespace MWInput bool isGyroAvailable() const; std::array getGyroValues() const; + std::string getControllerButtonIcon(int button); + std::string getControllerAxisIcon(int axis); + private: // Return true if GUI consumes input. bool gamepadToGuiControl(const SDL_ControllerButtonEvent& arg); @@ -55,6 +58,8 @@ namespace MWInput void enableGyroSensor(); + int getControllerType(); + BindingsManager* mBindingsManager; MouseManager* mMouseManager; @@ -62,6 +67,7 @@ namespace MWInput bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; + bool mGamepadMousePressed; }; } #endif diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index d81d720b21..250b25aeaa 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -189,6 +189,13 @@ namespace MWInput return mMouseManager->getMouseMoveY(); } + void InputManager::warpMouseToWidget(MyGUI::Widget* widget) + { + mMouseManager->warpMouseToWidget(widget); + mMouseManager->injectMouseMove(1, 0, 0); + MWBase::Environment::get().getWindowManager()->setCursorActive(true); + } + const std::initializer_list& InputManager::getActionKeySorting() { return mBindingsManager->getActionKeySorting(); @@ -242,6 +249,16 @@ namespace MWInput return mControllerManager->joystickLastUsed(); } + std::string InputManager::getControllerButtonIcon(int button) + { + return mControllerManager->getControllerButtonIcon(button); + } + + std::string InputManager::getControllerAxisIcon(int axis) + { + return mControllerManager->getControllerAxisIcon(axis); + } + void InputManager::executeAction(int action) { mActionManager->executeAction(action); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 46e4774b6b..3e964ae2a8 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -81,6 +81,7 @@ namespace MWInput float getControllerAxisValue(SDL_GameControllerAxis axis) const override; int getMouseMoveX() const override; int getMouseMoveY() const override; + void warpMouseToWidget(MyGUI::Widget* widget) override; int getNumActions() override { return A_Last; } const std::initializer_list& getActionKeySorting() override; @@ -91,6 +92,8 @@ namespace MWInput void setJoystickLastUsed(bool enabled) override; bool joystickLastUsed() override; + std::string getControllerButtonIcon(int button) override; + std::string getControllerAxisIcon(int axis) override; int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index eed95cf1c9..b17b92e118 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -57,7 +57,8 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor - float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); + float uiScale = winMgr->getScalingFactor(); mGuiCursorX = static_cast(arg.x) / uiScale; mGuiCursorY = static_cast(arg.y) / uiScale; @@ -70,7 +71,14 @@ namespace MWInput MyGUI::InputManager::getInstance().injectMouseMove( static_cast(mGuiCursorX), static_cast(mGuiCursorY), mMouseWheel); - MWBase::Environment::get().getWindowManager()->setCursorActive(true); + winMgr->setCursorActive(true); + if (Settings::gui().mControllerMenus && !winMgr->getCursorVisible() + && (std::abs(arg.xrel) > 1 || std::abs(arg.yrel) > 1)) + { + // Unhide the cursor if it was hidden to show a controller tooltip. + winMgr->setControllerTooltip(false); + winMgr->setCursorVisible(true); + } } if (mMouseLookEnabled && !input->controlsDisabled()) @@ -263,4 +271,17 @@ namespace MWInput mInputWrapper->warpMouse( static_cast(mGuiCursorX * guiUiScale), static_cast(mGuiCursorY * guiUiScale)); } + + void MouseManager::warpMouseToWidget(MyGUI::Widget* widget) + { + float widgetX = widget->getAbsoluteCoord().left + widget->getWidth() / 2; + float widgetY = widget->getAbsoluteCoord().top + widget->getHeight() / 4; + if (std::abs(mGuiCursorX - widgetX) > 1 || std::abs(mGuiCursorY - widgetY) > 1) + { + mGuiCursorX = widgetX; + mGuiCursorY = widgetY; + warpMouse(); + } + } + } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 5de8a8f3bc..0a9c4eccd7 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -32,6 +32,7 @@ namespace MWInput bool injectMouseButtonRelease(Uint8 button); void injectMouseMove(float xMove, float yMove, float mouseWheelMove); void warpMouse(); + void warpMouseToWidget(MyGUI::Widget* widget); void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index 9a781106a6..6f9b82d8a9 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -240,6 +241,7 @@ namespace MWLua api["isMouseButtonPressed"] = [](int button) -> bool { return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button); }; api["_isGamepadCursorActive"] = [input]() -> bool { return input->isGamepadGuiCursorEnabled(); }; + api["_isControllerMenusEnabled"] = []() -> bool { return Settings::gui().mControllerMenus; }; api["_setGamepadCursorActive"] = [input](bool v) { input->setGamepadGuiCursorEnabled(v); MWBase::Environment::get().getWindowManager()->setCursorActive(v); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 3508fdcd44..0a73acfc42 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -645,6 +645,9 @@ namespace MWLua } inventoryT["isResolved"] = [](const InventoryT& inventory) -> bool { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); + // Avoid initializing custom data + if (!ptr.getRefData().getCustomData()) + return false; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); return store.isResolved(); }; diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index 425e44451b..bd098d99d5 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -16,6 +16,7 @@ #include "../context.hpp" +#include "servicesoffered.hpp" namespace MWLua { @@ -25,15 +26,6 @@ namespace MWLua record["servicesOffered"] = sol::readonly_property([context](const T& rec) -> sol::table { sol::state_view lua = context.sol(); sol::table providedServices(lua, sol::create); - constexpr std::array, 19> serviceNames = { { { ESM::NPC::Spells, - "Spells" }, - { ESM::NPC::Spellmaking, "Spellmaking" }, { ESM::NPC::Enchanting, "Enchanting" }, - { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, { ESM::NPC::AllItems, "Barter" }, - { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, { ESM::NPC::Clothing, "Clothing" }, - { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, { ESM::NPC::Picks, "Picks" }, - { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, { ESM::NPC::Apparatus, "Apparatus" }, - { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, { ESM::NPC::Potions, "Potions" }, - { ESM::NPC::MagicItems, "MagicItems" } } }; int services = rec.mAiData.mServices; if constexpr (std::is_same_v) @@ -42,10 +34,11 @@ namespace MWLua services = MWBase::Environment::get().getESMStore()->get().find(rec.mClass)->mData.mServices; } - for (const auto& [flag, name] : serviceNames) + for (const auto& [flag, name] : MWLua::ServiceNames) { providedServices[name] = (services & flag) != 0; } + providedServices["Travel"] = !rec.getTransport().empty(); return LuaUtil::makeReadOnly(providedServices); }); diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index 380a2d1e9b..e847357bd9 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -2,6 +2,7 @@ #include "actor.hpp" #include "modelproperty.hpp" +#include "servicesoffered.hpp" #include #include @@ -44,6 +45,126 @@ namespace return faction->mRanks.size(); } + ESM::NPC tableToNPC(const sol::table& rec) + { + ESM::NPC npc; + + // Start from template if provided + if (rec["template"] != sol::nil) + npc = LuaUtil::cast(rec["template"]); + else + npc.blank(); + + npc.mId = {}; + + // Basic fields + if (rec["name"] != sol::nil) + npc.mName = rec["name"]; + if (rec["model"] != sol::nil) + npc.mModel = Misc::ResourceHelpers::meshPathForESM3(rec["model"].get()); + if (rec["mwscript"] != sol::nil) + npc.mScript = ESM::RefId::deserializeText(rec["mwscript"].get()); + if (rec["race"] != sol::nil) + npc.mRace = ESM::RefId::deserializeText(rec["race"].get()); + if (rec["class"] != sol::nil) + npc.mClass = ESM::RefId::deserializeText(rec["class"].get()); + if (rec["head"] != sol::nil) + npc.mHead = ESM::RefId::deserializeText(rec["head"].get()); + if (rec["hair"] != sol::nil) + npc.mHair = ESM::RefId::deserializeText(rec["hair"].get()); + if (rec["primaryFaction"] != sol::nil) + { + auto factionStr = rec["primaryFaction"].get(); + ESM::RefId factionId = ESM::RefId::deserializeText(factionStr); + + const auto& factionStore = MWBase::Environment::get().getESMStore()->get(); + if (!factionStore.search(factionId)) + throw std::runtime_error("Invalid faction '" + std::string(factionStr) + "' in primaryFaction"); + + npc.mFaction = factionId; + } + if (rec["isMale"] != sol::nil) + { + bool male = rec["isMale"]; + if (male) + npc.mFlags &= ~ESM::NPC::Female; + else + npc.mFlags |= ESM::NPC::Female; + } + + if (rec["isEssential"] != sol::nil) + { + bool essential = rec["isEssential"]; + if (essential) + npc.mFlags |= ESM::NPC::Essential; + else + npc.mFlags &= ~ESM::NPC::Essential; + } + + if (rec["isAutocalc"] != sol::nil) + { + bool autoCalc = rec["isAutocalc"]; + if (autoCalc) + npc.mFlags |= ESM::NPC::Autocalc; + else + npc.mFlags &= ~ESM::NPC::Autocalc; + } + + if (rec["isRespawning"] != sol::nil) + { + bool respawn = rec["isRespawning"]; + if (respawn) + npc.mFlags |= ESM::NPC::Respawn; + else + npc.mFlags &= ~ESM::NPC::Respawn; + } + + if (rec["baseDisposition"] != sol::nil) + npc.mNpdt.mDisposition = rec["baseDisposition"].get(); + + if (rec["baseGold"] != sol::nil) + npc.mNpdt.mGold = rec["baseGold"].get(); + + if (rec["bloodType"] != sol::nil) + npc.mBloodType = rec["bloodType"].get(); + + if (rec["primaryFactionRank"] != sol::nil) + { + if (!npc.mFaction.empty()) + { + const ESM::Faction* faction + = MWBase::Environment::get().getESMStore()->get().find(npc.mFaction); + + int luaValue = rec["primaryFactionRank"]; + int rank = LuaUtil::fromLuaIndex(luaValue); + + int maxRank = static_cast(getValidRanksCount(faction)); + + if (rank < 0 || rank >= maxRank) + throw std::runtime_error("primaryFactionRank: Requested rank " + std::to_string(rank) + + " is out of bounds for faction " + npc.mFaction.toDebugString()); + + npc.mNpdt.mRank = rank; + } + } + + if (rec["servicesOffered"] != sol::nil) + { + const sol::table services = rec["servicesOffered"]; + int flags = 0; + + for (const auto& [mask, key] : MWLua::ServiceNames) + { + sol::object value = services[key]; + if (value != sol::nil && value.as()) + flags |= mask; + } + + npc.mAiData.mServices = flags; + } + + return npc; + } ESM::RefId parseFactionId(std::string_view faction) { @@ -95,9 +216,18 @@ namespace MWLua = sol::readonly_property([](const ESM::NPC& rec) -> int { return (int)rec.mNpdt.mDisposition; }); record["head"] = sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHead.serializeText(); }); + record["primaryFaction"] = sol::readonly_property( + [](const ESM::NPC& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mFaction); }); + record["primaryFactionRank"] = sol::readonly_property([](const ESM::NPC& rec, sol::this_state s) -> int { + if (rec.mFaction.empty()) + return 0; + return LuaUtil::toLuaIndex(rec.mNpdt.mRank); + }); addModelProperty(record); record["isEssential"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Essential; }); + record["isAutocalc"] + = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Autocalc; }); record["isMale"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.isMale(); }); record["isRespawning"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); @@ -152,6 +282,7 @@ namespace MWLua stats.setBaseDisposition(stats.getBaseDisposition() + value); }; + npc["createRecordDraft"] = tableToNPC; npc["getFactionRank"] = [](const Object& actor, std::string_view faction) -> size_t { const MWWorld::Ptr ptr = actor.ptr(); ESM::RefId factionId = parseFactionId(faction); diff --git a/apps/openmw/mwlua/types/servicesoffered.hpp b/apps/openmw/mwlua/types/servicesoffered.hpp new file mode 100644 index 0000000000..94695c44fb --- /dev/null +++ b/apps/openmw/mwlua/types/servicesoffered.hpp @@ -0,0 +1,21 @@ +#ifndef MWLUA_SERVICESOFFERED_HPP +#define MWLUA_SERVICESOFFERED_HPP + +#include +#include +#include + +namespace MWLua +{ + + inline constexpr std::array, 19> ServiceNames + = { { { ESM::NPC::Spells, "Spells" }, { ESM::NPC::Spellmaking, "Spellmaking" }, + { ESM::NPC::Enchanting, "Enchanting" }, { ESM::NPC::Training, "Training" }, { ESM::NPC::Repair, "Repair" }, + { ESM::NPC::AllItems, "Barter" }, { ESM::NPC::Weapon, "Weapon" }, { ESM::NPC::Armor, "Armor" }, + { ESM::NPC::Clothing, "Clothing" }, { ESM::NPC::Books, "Books" }, { ESM::NPC::Ingredients, "Ingredients" }, + { ESM::NPC::Picks, "Picks" }, { ESM::NPC::Probes, "Probes" }, { ESM::NPC::Lights, "Lights" }, + { ESM::NPC::Apparatus, "Apparatus" }, { ESM::NPC::RepairItem, "RepairItem" }, { ESM::NPC::Misc, "Misc" }, + { ESM::NPC::Potions, "Potions" }, { ESM::NPC::MagicItems, "MagicItems" } } }; +} + +#endif diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index d98c769e65..5539d8d77c 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -188,6 +189,14 @@ namespace MWLua checkGameInitialized(lua); return MWBase::Environment::get().getESMStore()->insert(potion); }, + [lua = context.mLua](const ESM::NPC& npc) -> const ESM::NPC* { + checkGameInitialized(lua); + if (npc.mId.empty()) + return MWBase::Environment::get().getESMStore()->insert(npc); + ESM::NPC copy = npc; + copy.mId = {}; + return MWBase::Environment::get().getESMStore()->insert(copy); + }, [lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* { checkGameInitialized(lua); return MWBase::Environment::get().getESMStore()->insert(weapon); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 7262805f81..369731a019 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -726,8 +726,6 @@ namespace MWWorld switch (type) { case ESM::REC_ALCH: - case ESM::REC_MISC: - case ESM::REC_ACTI: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: @@ -735,14 +733,16 @@ namespace MWWorld case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: - case ESM::REC_LEVI: - case ESM::REC_LEVC: - case ESM::REC_LIGH: mStoreImp->mRecNameToStore[type]->read(reader); return true; case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: + case ESM::REC_MISC: + case ESM::REC_ACTI: + case ESM::REC_LEVI: + case ESM::REC_LEVC: + case ESM::REC_LIGH: mStoreImp->mRecNameToStore[type]->read(reader, true); return true; diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index e4e67c2f3d..12120b998d 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -269,9 +269,12 @@ namespace MWWorld list.push_back((*it)->mId); } } + template T* TypedDynamicStore::insert(const T& item, bool overrideOnly) { + if constexpr (std::is_same_v) + overrideOnly = overrideOnly && !item.mId.template is(); if (overrideOnly) { auto it = mStatic.find(item.mId); diff --git a/components/settings/categories/gui.hpp b/components/settings/categories/gui.hpp index ffef047094..06f32640fd 100644 --- a/components/settings/categories/gui.hpp +++ b/components/settings/categories/gui.hpp @@ -23,6 +23,8 @@ namespace Settings SettingValue mMenuTransparency{ mIndex, "GUI", "menu transparency", makeClampSanitizerFloat(0, 1) }; SettingValue mTooltipDelay{ mIndex, "GUI", "tooltip delay", makeMaxSanitizerFloat(0) }; SettingValue mStretchMenuBackground{ mIndex, "GUI", "stretch menu background" }; + SettingValue mControllerMenus{ mIndex, "GUI", "controller menus" }; + SettingValue mControllerTooltips{ mIndex, "GUI", "controller tooltips" }; SettingValue mSubtitles{ mIndex, "GUI", "subtitles" }; SettingValue mHitFader{ mIndex, "GUI", "hit fader" }; SettingValue mWerewolfOverlay{ mIndex, "GUI", "werewolf overlay" }; diff --git a/components/widgets/list.cpp b/components/widgets/list.cpp index 416590ed48..739d50bff2 100644 --- a/components/widgets/list.cpp +++ b/components/widgets/list.cpp @@ -176,4 +176,9 @@ namespace Gui { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } + + void MWList::setViewOffset(int offset) + { + mScrollView->setViewOffset(MyGUI::IntPoint(0, offset)); + } } diff --git a/components/widgets/list.hpp b/components/widgets/list.hpp index f67a7da97b..94829d93df 100644 --- a/components/widgets/list.hpp +++ b/components/widgets/list.hpp @@ -48,6 +48,7 @@ namespace Gui ///< get widget for an item name, useful to set up tooltip void scrollToTop(); + void setViewOffset(int offset); void setPropertyOverride(std::string_view _key, std::string_view _value) override; diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp index 33dd70c763..1cf7364bf0 100644 --- a/components/widgets/sharedstatebutton.hpp +++ b/components/widgets/sharedstatebutton.hpp @@ -21,13 +21,14 @@ namespace Gui public: SharedStateButton(); + void onMouseSetFocus(MyGUI::Widget* _old) override; + void onMouseLostFocus(MyGUI::Widget* _new) override; + protected: void updateButtonState(); void onMouseButtonPressed(int _left, int _top, MyGUI::MouseButton _id) override; void onMouseButtonReleased(int _left, int _top, MyGUI::MouseButton _id) override; - void onMouseSetFocus(MyGUI::Widget* _old) override; - void onMouseLostFocus(MyGUI::Widget* _new) override; void baseUpdateEnable() override; void shutdownOverride() override; diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index 7dc38f8c39..f5afb3a017 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -62,6 +62,36 @@ GUI Settings Bethesda assets are 4:3 ratio; others may differ. If false, assets are centered with black bars filling remainder. +.. omw-setting:: + :title: controller menus + :type: boolean + :range: true, false + :default: false + :location: :bdg-success:`Launcher > Settings > Interface` + + Menus are now fully navigable with a controller using the DPad, triggers, and + face buttons. Most button mappings follow the Xbox version, with modern + additions like R3 to toggle item info. All menus can be opened and closed + with the controller, though some elements—such as editing settings or viewing + certain tooltips—still use the controller’s mouse cursor. + A button hint bar appears at the bottom of each menu, with icons that match + the connected controller type (Xbox, PlayStation, Switch, or Steam). In the + inventory menu, L2/R2 switch between sub-menus (Map, Inventory, Magic, Stats), + while L1/R1 adjust filters in inventory and barter screens. R3 toggles + tooltips, similar to Oblivion Remastered. Mouse input remains fully supported. + If false, the controller works as a GUI mouse. + +.. omw-setting:: + :title: controller tooltips + :type: boolean + :range: true, false + :default: false + :location: :bdg-success:`Launcher > Settings > Interface` + + When true, you do not need to press R3 to show tooltips when using + controller menus. + If false, controller menu tooltips are hidden until R3 is pressed. + .. omw-setting:: :title: subtitles :type: boolean diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 10faff39ad..da7d97454e 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -9,6 +9,35 @@ set(BUILTIN_DATA_FILES textures/omw_menu_scroll_right.dds textures/omw_menu_scroll_center_h.dds textures/omw_menu_scroll_center_v.dds + textures/omw_menu_icon_active.dds + textures/omw_psx_button_circle.dds + textures/omw_psx_button_dpad.dds + textures/omw_psx_button_square.dds + textures/omw_psx_button_triangle.dds + textures/omw_psx_button_x.dds + textures/omw_steam_button_a.dds + textures/omw_steam_button_b.dds + textures/omw_steam_button_dpad.dds + textures/omw_steam_button_l1.dds + textures/omw_steam_button_l2.dds + textures/omw_steam_button_l3.dds + textures/omw_steam_button_lstick.dds + textures/omw_steam_button_menu.dds + textures/omw_steam_button_r1.dds + textures/omw_steam_button_r2.dds + textures/omw_steam_button_r3.dds + textures/omw_steam_button_rstick.dds + textures/omw_steam_button_view.dds + textures/omw_steam_button_x.dds + textures/omw_steam_button_y.dds + textures/omw_switch_button_l.dds + textures/omw_switch_button_r.dds + textures/omw_switch_button_zl.dds + textures/omw_switch_button_zr.dds + textures/omw_xbox_button_lb.dds + textures/omw_xbox_button_lt.dds + textures/omw_xbox_button_rb.dds + textures/omw_xbox_button_rt.dds textures/omw/water_nm.png fonts/DejaVuFontLicense.txt @@ -163,6 +192,7 @@ set(BUILTIN_DATA_FILES mygui/openmw_console.layout mygui/openmw_console.skin.xml mygui/openmw_container_window.layout + mygui/openmw_controllerbuttons.layout mygui/openmw_count_window.layout mygui/openmw_dialogue_window.layout mygui/openmw_dialogue_window.skin.xml @@ -173,6 +203,7 @@ set(BUILTIN_DATA_FILES mygui/openmw_infobox.layout mygui/openmw_interactive_messagebox.layout mygui/openmw_interactive_messagebox_notransp.layout + mygui/openmw_inventory_tabs.layout mygui/openmw_inventory_window.layout mygui/openmw_journal.layout mygui/openmw_journal.skin.xml diff --git a/files/data/l10n/OMWEngine/de.yaml b/files/data/l10n/OMWEngine/de.yaml index a23dbebdae..782ae50824 100644 --- a/files/data/l10n/OMWEngine/de.yaml +++ b/files/data/l10n/OMWEngine/de.yaml @@ -213,3 +213,14 @@ WindowModeWindowed: "Fenster" WindowModeWindowedFullscreen: "Fenster in Vollbildgröße" # More fitting translations of "wobbly" are welcome WobblyShores: "Wabbelige Uferlinien" + + +# Controller button names + +EnchantType: "Zaubertyp" +InventorySelect: "Geben" +JournalQuests: "Quests" +JournalShowAll: "Alle Anzeigen" +LoadingSelectCharacter: "Charakter auswählen" +RechargeSelect: "Aufladen" +RepairTool: "Werkzeug" diff --git a/files/data/l10n/OMWEngine/en.yaml b/files/data/l10n/OMWEngine/en.yaml index 8a3217a03c..4ec744ce80 100644 --- a/files/data/l10n/OMWEngine/en.yaml +++ b/files/data/l10n/OMWEngine/en.yaml @@ -212,3 +212,14 @@ WindowModeHint: "Hint: Windowed Fullscreen mode\nalways uses the native display WindowModeWindowed: "Windowed" WindowModeWindowedFullscreen: "Windowed Fullscreen" WobblyShores: "Wobbly Shores" + + +# Controller button names + +EnchantType: "Cast Type" +InventorySelect: "Put" +JournalQuests: "Quests" +JournalShowAll: "Show All" +LoadingSelectCharacter: "Change Character" +RechargeSelect: "Recharge" +RepairTool: "Tool" diff --git a/files/data/l10n/OMWEngine/fr.yaml b/files/data/l10n/OMWEngine/fr.yaml index fc584d8293..808d15b4d8 100644 --- a/files/data/l10n/OMWEngine/fr.yaml +++ b/files/data/l10n/OMWEngine/fr.yaml @@ -212,3 +212,14 @@ WindowModeHint: "Info : Le mode \"Fenêtré plein écran\" utilise toujours la r WindowModeWindowed: "Fenêtré" WindowModeWindowedFullscreen: "Fenêtré plein écran" WobblyShores: "Rivages vacillants" + + +# Controller button names + +EnchantType: "Type de lancement" +InventorySelect: "Placer" +JournalQuests: "Quêtes" +JournalShowAll: "Tout Afficher" +LoadingSelectCharacter: "Sélection du personnage" +RechargeSelect: "Recharge" +RepairTool: "Outil" diff --git a/files/data/l10n/OMWEngine/ru.yaml b/files/data/l10n/OMWEngine/ru.yaml index 753d1fa219..b780ba1e05 100644 --- a/files/data/l10n/OMWEngine/ru.yaml +++ b/files/data/l10n/OMWEngine/ru.yaml @@ -212,3 +212,14 @@ WindowModeHint: "Подсказка: режим \"Оконный без поле WindowModeWindowed: "Оконный" WindowModeWindowedFullscreen: "Оконный без полей" WobblyShores: "Колеблющиеся берега" + + +# Controller button names + +EnchantType: "Тип заклинания" +InventorySelect: "Поместить" +JournalQuests: "Квесты" +JournalShowAll: "Показать все" +LoadingSelectCharacter: "Выбрать персонажа" +RechargeSelect: "Перезарядить" +RepairTool: "Инструмент" diff --git a/files/data/l10n/OMWEngine/sv.yaml b/files/data/l10n/OMWEngine/sv.yaml index b3bb6788ed..c5ad166849 100644 --- a/files/data/l10n/OMWEngine/sv.yaml +++ b/files/data/l10n/OMWEngine/sv.yaml @@ -213,3 +213,14 @@ WindowModeHint: "Notera: Helskärm i fönsterläge\nanvänder alltid skärmens n WindowModeWindowed: "Fönster" WindowModeWindowedFullscreen: "Helskärm i fönsterläge" WobblyShores: "Vaggande stränder" + + +# Controller button names + +EnchantType: "Typ av förtrollning" +InventorySelect: "Placera" +JournalQuests: "Uppdrag" +JournalShowAll: "Visa Alla" +LoadingSelectCharacter: "Välj spelfigur" +RechargeSelect: "Ladda" +RepairTool: "Verktyg" diff --git a/files/data/mygui/openmw_controllerbuttons.layout b/files/data/mygui/openmw_controllerbuttons.layout new file mode 100644 index 0000000000..30f92b9b4b --- /dev/null +++ b/files/data/mygui/openmw_controllerbuttons.layout @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/data/mygui/openmw_inventory_tabs.layout b/files/data/mygui/openmw_inventory_tabs.layout new file mode 100644 index 0000000000..d13361d5db --- /dev/null +++ b/files/data/mygui/openmw_inventory_tabs.layout @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/data/mygui/openmw_inventory_window.layout b/files/data/mygui/openmw_inventory_window.layout index a555c94031..2f2af06021 100644 --- a/files/data/mygui/openmw_inventory_window.layout +++ b/files/data/mygui/openmw_inventory_window.layout @@ -30,6 +30,13 @@ + + + + + + + @@ -50,6 +57,13 @@ + + + + + + + diff --git a/files/data/mygui/openmw_layers.xml b/files/data/mygui/openmw_layers.xml index 459db3fcb9..f2eed488e1 100644 --- a/files/data/mygui/openmw_layers.xml +++ b/files/data/mygui/openmw_layers.xml @@ -14,6 +14,7 @@ + diff --git a/files/data/mygui/openmw_resources.xml b/files/data/mygui/openmw_resources.xml index 4c32512004..47fa71e984 100644 --- a/files/data/mygui/openmw_resources.xml +++ b/files/data/mygui/openmw_resources.xml @@ -144,6 +144,10 @@ + + + + @@ -168,6 +172,10 @@ + + + + @@ -184,7 +192,10 @@ - + + + + diff --git a/files/data/mygui/openmw_text.skin.xml b/files/data/mygui/openmw_text.skin.xml index 5f96d0a57c..1724458522 100644 --- a/files/data/mygui/openmw_text.skin.xml +++ b/files/data/mygui/openmw_text.skin.xml @@ -120,6 +120,38 @@ color_misc=0,205,205 # ???? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/data/mygui/openmw_trade_window.layout b/files/data/mygui/openmw_trade_window.layout index 49ccf25ba2..e5cf8a0ba0 100644 --- a/files/data/mygui/openmw_trade_window.layout +++ b/files/data/mygui/openmw_trade_window.layout @@ -7,6 +7,13 @@ + + + + + + + @@ -27,6 +34,13 @@ + + + + + + + diff --git a/files/data/scripts/omw/input/gamepadcontrols.lua b/files/data/scripts/omw/input/gamepadcontrols.lua index 9b89d505b5..594e89e6ad 100644 --- a/files/data/scripts/omw/input/gamepadcontrols.lua +++ b/files/data/scripts/omw/input/gamepadcontrols.lua @@ -11,7 +11,7 @@ return { interface = { --- Interface version -- @field [parent=#GamepadControls] #number version - version = 0, + version = 1, --- Checks if the gamepad cursor is active. If it is active, the left stick can move the cursor, and A will be interpreted as a mouse click. -- @function [parent=#GamepadControls] isGamepadCursorActive @@ -20,6 +20,13 @@ return { return input._isGamepadCursorActive() end, + --- Checks if the controller menu option is enabled. If true, UI is replaced with a more controller appropriate interface. + -- @function [parent=#GamepadControls] isControllerMenusEnabled + -- @return #boolean + isControllerMenusEnabled = function() + return input._isControllerMenusEnabled() + end, + --- Sets if the gamepad cursor is active. If it is active, the left stick can move the cursor, and A will be interpreted as a mouse click. -- @function [parent=#GamepadControls] setGamepadCursorActive -- @param #boolean value diff --git a/files/data/textures/omw_menu_icon_active.dds b/files/data/textures/omw_menu_icon_active.dds new file mode 100644 index 0000000000..be743ace7f Binary files /dev/null and b/files/data/textures/omw_menu_icon_active.dds differ diff --git a/files/data/textures/omw_psx_button_circle.dds b/files/data/textures/omw_psx_button_circle.dds new file mode 100644 index 0000000000..53d9856a28 Binary files /dev/null and b/files/data/textures/omw_psx_button_circle.dds differ diff --git a/files/data/textures/omw_psx_button_dpad.dds b/files/data/textures/omw_psx_button_dpad.dds new file mode 100644 index 0000000000..b5f74b5153 Binary files /dev/null and b/files/data/textures/omw_psx_button_dpad.dds differ diff --git a/files/data/textures/omw_psx_button_square.dds b/files/data/textures/omw_psx_button_square.dds new file mode 100644 index 0000000000..df6e49c4e7 Binary files /dev/null and b/files/data/textures/omw_psx_button_square.dds differ diff --git a/files/data/textures/omw_psx_button_triangle.dds b/files/data/textures/omw_psx_button_triangle.dds new file mode 100644 index 0000000000..db2d20efda Binary files /dev/null and b/files/data/textures/omw_psx_button_triangle.dds differ diff --git a/files/data/textures/omw_psx_button_x.dds b/files/data/textures/omw_psx_button_x.dds new file mode 100644 index 0000000000..dc032dfedc Binary files /dev/null and b/files/data/textures/omw_psx_button_x.dds differ diff --git a/files/data/textures/omw_steam_button_a.dds b/files/data/textures/omw_steam_button_a.dds new file mode 100644 index 0000000000..447aa60815 Binary files /dev/null and b/files/data/textures/omw_steam_button_a.dds differ diff --git a/files/data/textures/omw_steam_button_b.dds b/files/data/textures/omw_steam_button_b.dds new file mode 100644 index 0000000000..0be3063643 Binary files /dev/null and b/files/data/textures/omw_steam_button_b.dds differ diff --git a/files/data/textures/omw_steam_button_dpad.dds b/files/data/textures/omw_steam_button_dpad.dds new file mode 100644 index 0000000000..d730db44a8 Binary files /dev/null and b/files/data/textures/omw_steam_button_dpad.dds differ diff --git a/files/data/textures/omw_steam_button_l1.dds b/files/data/textures/omw_steam_button_l1.dds new file mode 100644 index 0000000000..83790af2be Binary files /dev/null and b/files/data/textures/omw_steam_button_l1.dds differ diff --git a/files/data/textures/omw_steam_button_l2.dds b/files/data/textures/omw_steam_button_l2.dds new file mode 100644 index 0000000000..156b1ca96c Binary files /dev/null and b/files/data/textures/omw_steam_button_l2.dds differ diff --git a/files/data/textures/omw_steam_button_l3.dds b/files/data/textures/omw_steam_button_l3.dds new file mode 100644 index 0000000000..3b6b174e48 Binary files /dev/null and b/files/data/textures/omw_steam_button_l3.dds differ diff --git a/files/data/textures/omw_steam_button_lstick.dds b/files/data/textures/omw_steam_button_lstick.dds new file mode 100644 index 0000000000..f5df28d6fd Binary files /dev/null and b/files/data/textures/omw_steam_button_lstick.dds differ diff --git a/files/data/textures/omw_steam_button_menu.dds b/files/data/textures/omw_steam_button_menu.dds new file mode 100644 index 0000000000..ef0a0a501c Binary files /dev/null and b/files/data/textures/omw_steam_button_menu.dds differ diff --git a/files/data/textures/omw_steam_button_r1.dds b/files/data/textures/omw_steam_button_r1.dds new file mode 100644 index 0000000000..8474bb9ff8 Binary files /dev/null and b/files/data/textures/omw_steam_button_r1.dds differ diff --git a/files/data/textures/omw_steam_button_r2.dds b/files/data/textures/omw_steam_button_r2.dds new file mode 100644 index 0000000000..052e97ebe1 Binary files /dev/null and b/files/data/textures/omw_steam_button_r2.dds differ diff --git a/files/data/textures/omw_steam_button_r3.dds b/files/data/textures/omw_steam_button_r3.dds new file mode 100644 index 0000000000..73b4bea1f7 Binary files /dev/null and b/files/data/textures/omw_steam_button_r3.dds differ diff --git a/files/data/textures/omw_steam_button_rstick.dds b/files/data/textures/omw_steam_button_rstick.dds new file mode 100644 index 0000000000..f7dfb186f4 Binary files /dev/null and b/files/data/textures/omw_steam_button_rstick.dds differ diff --git a/files/data/textures/omw_steam_button_view.dds b/files/data/textures/omw_steam_button_view.dds new file mode 100644 index 0000000000..8fb56f847f Binary files /dev/null and b/files/data/textures/omw_steam_button_view.dds differ diff --git a/files/data/textures/omw_steam_button_x.dds b/files/data/textures/omw_steam_button_x.dds new file mode 100644 index 0000000000..9619ed243c Binary files /dev/null and b/files/data/textures/omw_steam_button_x.dds differ diff --git a/files/data/textures/omw_steam_button_y.dds b/files/data/textures/omw_steam_button_y.dds new file mode 100644 index 0000000000..4a27710b2a Binary files /dev/null and b/files/data/textures/omw_steam_button_y.dds differ diff --git a/files/data/textures/omw_switch_button_l.dds b/files/data/textures/omw_switch_button_l.dds new file mode 100644 index 0000000000..ee6ed5cd3f Binary files /dev/null and b/files/data/textures/omw_switch_button_l.dds differ diff --git a/files/data/textures/omw_switch_button_r.dds b/files/data/textures/omw_switch_button_r.dds new file mode 100644 index 0000000000..f2d5060ca1 Binary files /dev/null and b/files/data/textures/omw_switch_button_r.dds differ diff --git a/files/data/textures/omw_switch_button_zl.dds b/files/data/textures/omw_switch_button_zl.dds new file mode 100644 index 0000000000..c23c54afcc Binary files /dev/null and b/files/data/textures/omw_switch_button_zl.dds differ diff --git a/files/data/textures/omw_switch_button_zr.dds b/files/data/textures/omw_switch_button_zr.dds new file mode 100644 index 0000000000..ba61341ca6 Binary files /dev/null and b/files/data/textures/omw_switch_button_zr.dds differ diff --git a/files/data/textures/omw_xbox_button_lb.dds b/files/data/textures/omw_xbox_button_lb.dds new file mode 100644 index 0000000000..ae73e5014a Binary files /dev/null and b/files/data/textures/omw_xbox_button_lb.dds differ diff --git a/files/data/textures/omw_xbox_button_lt.dds b/files/data/textures/omw_xbox_button_lt.dds new file mode 100644 index 0000000000..ddae70b961 Binary files /dev/null and b/files/data/textures/omw_xbox_button_lt.dds differ diff --git a/files/data/textures/omw_xbox_button_rb.dds b/files/data/textures/omw_xbox_button_rb.dds new file mode 100644 index 0000000000..335ece93a6 Binary files /dev/null and b/files/data/textures/omw_xbox_button_rb.dds differ diff --git a/files/data/textures/omw_xbox_button_rt.dds b/files/data/textures/omw_xbox_button_rt.dds new file mode 100644 index 0000000000..fe7987310c Binary files /dev/null and b/files/data/textures/omw_xbox_button_rt.dds differ diff --git a/files/lang/launcher_de.ts b/files/lang/launcher_de.ts index 83d52c473d..d03a2c0550 100644 --- a/files/lang/launcher_de.ts +++ b/files/lang/launcher_de.ts @@ -1451,6 +1451,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Run Script After Startup: + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + + + + Enable Controller Menus + + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + + + + Show Controller Tooltips By Default + + <html><head/><body><p>Controls the strength of the Doppler effect. Zero means it is completely disabled.</p><p>The Doppler effect increases or decreases the pitch of sounds relative to the velocity of the sound source and the listener.</p></body></html> diff --git a/files/lang/launcher_en.ts b/files/lang/launcher_en.ts index 4c3bd2819d..4924a21fb7 100644 --- a/files/lang/launcher_en.ts +++ b/files/lang/launcher_en.ts @@ -1451,6 +1451,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov <html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html> + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + + + + Enable Controller Menus + + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + + + + Show Controller Tooltips By Default + + <html><head/><body><p>Controls the strength of the Doppler effect. Zero means it is completely disabled.</p><p>The Doppler effect increases or decreases the pitch of sounds relative to the velocity of the sound source and the listener.</p></body></html> diff --git a/files/lang/launcher_fr.ts b/files/lang/launcher_fr.ts index 2f4efe8161..17b9e458fa 100644 --- a/files/lang/launcher_fr.ts +++ b/files/lang/launcher_fr.ts @@ -1454,6 +1454,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Run Script After Startup: Script à lancer après démarrage : + + Enable Controller Menus + Activer les menus du contrôleur + + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + <html><head/><body><p>Faciliter l'utilisation des menus de jeu avec une manette.</p></body></html> + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + <html><head/><body><p>Lorsque vous utilisez les menus du contrôleur, rendez les info-bulles visibles par défaut.</p></body></html> + + + Show Controller Tooltips By Default + Afficher les info-bulles du contrôleur par défaut + <html><head/><body><p>Controls the strength of the Doppler effect. Zero means it is completely disabled.</p><p>The Doppler effect increases or decreases the pitch of sounds relative to the velocity of the sound source and the listener.</p></body></html> diff --git a/files/lang/launcher_ru.ts b/files/lang/launcher_ru.ts index bd7e05d128..25528f4769 100644 --- a/files/lang/launcher_ru.ts +++ b/files/lang/launcher_ru.ts @@ -1466,6 +1466,22 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov Use the Camera as the Sound Listener Использовать камеру как слушателя + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + <html><head/><body><p>Упрощает использование игровых меню на геймпаде.</p></body></html> + + + Enable Controller Menus + Интерфейс для геймпадов + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + <html><head/><body><p>Показывать подсказки интерфейса для геймпадов по умолчанию.</p></body></html> + + + Show Controller Tooltips By Default + Подсказки для геймпадов по умолчанию + <html><head/><body><p>Controls the strength of the Doppler effect. Zero means it is completely disabled.</p><p>The Doppler effect increases or decreases the pitch of sounds relative to the velocity of the sound source and the listener.</p></body></html> <html><head/><body><p>Определяет силу эффекта Доплера. Нулевое значение означает, что эффект отключен полностью.</p><p>Эффект Доплера увеличивает или уменьшает высоту звуков в зависимости от скорости источника звука и слушателя.</p></body></html> diff --git a/files/lang/launcher_sv.ts b/files/lang/launcher_sv.ts index 43bb4ce6e1..efc66a48fa 100644 --- a/files/lang/launcher_sv.ts +++ b/files/lang/launcher_sv.ts @@ -1470,6 +1470,22 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin <html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html> <html><head/><body><p>Vid aktivering gör denna funktion att övergångarna mellan olika animationer och poser blir mycket mjukare. Funktionen gör det också möjligt att konfigurera animationsövergångarna i YAML-filer. Dessa filer kan buntas ihop tillsammans med nya animationsfiler.</p></body></html> + + <html><head/><body><p>Make it easier to use game menus with a controller.</p></body></html> + <html><head/><body><p>Gör det enklare att använda spelmenyer med en handkontroll.</p></body></html> + + + Enable Controller Menus + Aktivera handkontrollmenyer + + + <html><head/><body><p>When using controller menus, make tooltips visible by default.</p></body></html> + <html><head/><body><p>Visar inforutor som standard när handkontrollmenyer används.</p></body></html> + + + Show Controller Tooltips By Default + Visa handkontrollinforutor som standard + <html><head/><body><p>Controls the strength of the Doppler effect. Zero means it is completely disabled.</p><p>The Doppler effect increases or decreases the pitch of sounds relative to the velocity of the sound source and the listener.</p></body></html> <html><head/><body><p>Kontrollerar styrkan på dopplereffekten. Noll innebär helt inaktiverat.</p><p>Dopplereffekten höjer eller sänker tonhöjden på ljud i förhållande till ljudkällans hastighet och lyssnaren.</p></body></html> diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 6d51987ac7..aa5b70c4e0 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -861,6 +861,9 @@ -- @field #boolean canWalk whether the creature can walk -- @field #boolean canUseWeapons whether the creature can use weapons and shields -- @field #boolean isBiped whether the creature is a biped +-- @field #boolean isAutocalc If true, the actors stats will be automatically calculated based on level and class. +-- @field #string primaryFaction Faction ID of the NPCs default faction. Nil if no faction +-- @field #number primaryFactionRank Faction rank of the NPCs default faction. Nil if no faction -- @field #boolean isEssential whether the creature is essential -- @field #boolean isRespawning whether the creature respawns after death -- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx. @@ -875,6 +878,13 @@ -- @field #Actor baseType @{#Actor} -- @field [parent=#NPC] #NpcStats stats +--- +-- Creates an @{#NpcRecord} without adding it to the world database. +-- Use @{openmw_world#(world).createRecord} to add the record to the world. +-- @function [parent=#NPC] createRecordDraft +-- @param #NpcRecord npc A Lua table with the fields of an NpcRecord, with an optional field `template` that accepts an @{#NpcRecord} as a base. +-- @return #NpcRecord A strongly typed NPC record. + --- -- A read-only list of all @{#NpcRecord}s in the world database, may be indexed by recordId. -- Implements [iterables#List](iterables.html#List) of #NpcRecord. diff --git a/files/settings-default.cfg b/files/settings-default.cfg index c33f02ad35..2c4bbab953 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -201,6 +201,12 @@ tooltip delay = 0.0 # Stretch menus, load screens, etc. to the window aspect ratio. stretch menu background = false +# Make menus easier to navigate with a controller. +controller menus = false + +# When true, you do not need to press R3 to show tooltips. +controller tooltips = false + # Subtitles for NPC spoken dialog and some sound effects. subtitles = false