diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 42279482f..03c4e7517 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -17,6 +17,7 @@ set(LIBIMHEX_SOURCES source/api/workspace_manager.cpp source/api/achievement_manager.cpp source/api/localization_manager.cpp + source/api/tutorial_manager.cpp source/data_processor/attribute.cpp source/data_processor/link.cpp diff --git a/lib/libimhex/include/hex/api/tutorial_manager.hpp b/lib/libimhex/include/hex/api/tutorial_manager.hpp new file mode 100644 index 000000000..cf5c18c4d --- /dev/null +++ b/lib/libimhex/include/hex/api/tutorial_manager.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +namespace hex { + + class TutorialManager { + private: + class IDStack { + public: + IDStack(); + + void add(const void *pointer); + void add(const std::string &string); + void add(int value); + + ImGuiID get(); + private: + ImVector idStack; + }; + + public: + enum class Position : u8 { + None = 0, + Top = 1, + Bottom = 2, + Left = 4, + Right = 8 + }; + + struct Tutorial { + Tutorial() = delete; + Tutorial(const std::string &unlocalizedName, const std::string &unlocalizedDescription) : + m_unlocalizedName(unlocalizedName), + m_unlocalizedDescription(unlocalizedDescription) { } + + struct Step { + explicit Step(Tutorial *parent) : m_parent(parent) { } + + /** + * @brief Adds a highlighting with text to a specific element + * @param unlocalizedText Unlocalized text to display next to the highlighting + * @param ids ID of the element to highlight + * @return Current step + */ + Step& addHighlight(const std::string &unlocalizedText, std::initializer_list> &&ids); + + /** + * @brief Adds a highlighting to a specific element + * @param ids ID of the element to highlight + * @return Current step + */ + Step& addHighlight(std::initializer_list> &&ids); + + /** + * @brief Sets the text that will be displayed in the tutorial message box + * @param unlocalizedTitle Title of the message box + * @param unlocalizedMessage Main message of the message box + * @param position Position of the message box + * @return Current step + */ + Step& setMessage(const std::string &unlocalizedTitle, const std::string &unlocalizedMessage, Position position = Position::None); + + /** + * @brief Allows this step to be skipped by clicking on the advance button + * @return Current step + */ + Step& allowSkip(); + + + /** + * @brief Checks if this step is the current step + * @return True if this step is the current step + */ + bool isCurrent() const; + + /** + * @brief Completes this step if it is the current step + */ + void complete() const; + + private: + struct Highlight { + std::string unlocalizedText; + ImGuiID highlightId; + }; + + struct Message { + Position position; + std::string unlocalizedTitle; + std::string unlocalizedMessage; + bool allowSkip; + }; + + private: + void addHighlights() const; + void removeHighlights() const; + + void advance(i32 steps = 1) const; + + friend class TutorialManager; + + Tutorial *m_parent; + std::vector m_highlights; + std::optional m_message; + }; + + Step& addStep(); + + private: + friend class TutorialManager; + + void start(); + + std::string m_unlocalizedName; + std::string m_unlocalizedDescription; + std::list m_steps; + decltype(m_steps)::iterator m_currentStep, m_latestStep; + }; + + + /** + * @brief Creates a new tutorial that can be started later + * @param unlocalizedName Name of the tutorial + * @param unlocalizedDescription + * @return Reference to created tutorial + */ + static Tutorial& createTutorial(const std::string &unlocalizedName, const std::string &unlocalizedDescription); + + /** + * @brief Starts the tutorial with the given name + * @param unlocalizedName Name of tutorial to start + */ + static void startTutorial(const std::string &unlocalizedName); + + + /** + * @brief Draws the tutorial + * @note This function should only be called by the main GUI + */ + static void drawTutorial(); + + /** + * @brief Resets the tutorial manager + */ + static void reset(); + + private: + TutorialManager() = delete; + + static void drawHighlights(); + static void drawMessageBox(std::optional message); + }; + + inline TutorialManager::Position operator|(TutorialManager::Position a, TutorialManager::Position b) { + return static_cast(static_cast(a) | static_cast(b)); + } + + inline TutorialManager::Position operator&(TutorialManager::Position a, TutorialManager::Position b) { + return static_cast(static_cast(a) & static_cast(b)); + } + +} \ No newline at end of file diff --git a/lib/libimhex/source/api/tutorial_manager.cpp b/lib/libimhex/source/api/tutorial_manager.cpp new file mode 100644 index 000000000..380cb35e6 --- /dev/null +++ b/lib/libimhex/source/api/tutorial_manager.cpp @@ -0,0 +1,307 @@ +#include +#include +#include + +#include +#include + +#include + +namespace hex { + + namespace { + + std::map s_tutorials; + decltype(s_tutorials)::iterator s_currentTutorial = s_tutorials.end(); + + std::map s_highlights; + std::vector> s_highlightDisplays; + + + } + + TutorialManager::Tutorial& TutorialManager::createTutorial(const std::string& unlocalizedName, const std::string& unlocalizedDescription) { + return s_tutorials.try_emplace(unlocalizedName, Tutorial(unlocalizedName, unlocalizedDescription)).first->second; + } + + void TutorialManager::startTutorial(const std::string& unlocalizedName) { + s_currentTutorial = s_tutorials.find(unlocalizedName); + if (s_currentTutorial == s_tutorials.end()) + return; + + s_currentTutorial->second.start(); + } + + void TutorialManager::drawHighlights() { + for (const auto &[rect, unlocalizedText] : s_highlightDisplays) { + const auto drawList = ImGui::GetForegroundDrawList(); + + drawList->PushClipRectFullScreen(); + { + auto highlightColor = ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_Highlight); + highlightColor.w *= ImSin(ImGui::GetTime() * 6.0F) / 4.0F + 0.75F; + + drawList->AddRect(rect.Min - ImVec2(5, 5), rect.Max + ImVec2(5, 5), ImColor(highlightColor), 5.0F, ImDrawFlags_None, 2.0F); + } + + { + if (!unlocalizedText.empty()) { + const auto margin = ImGui::GetStyle().WindowPadding; + + const ImVec2 windowPos = { rect.Min.x + 20_scaled, rect.Max.y + 10_scaled }; + ImVec2 windowSize = { std::max(rect.Max.x - rect.Min.x - 40_scaled, 300_scaled), 0 }; + + const char* text = Lang(unlocalizedText); + const auto textSize = ImGui::CalcTextSize(text, nullptr, false, windowSize.x - margin.x * 2); + windowSize.y = textSize.y + margin.y * 2; + + drawList->AddRectFilled(windowPos, windowPos + windowSize, ImGui::GetColorU32(ImGuiCol_WindowBg) | 0xFF000000); + drawList->AddRect(windowPos, windowPos + windowSize, ImGui::GetColorU32(ImGuiCol_Border)); + drawList->AddText(nullptr, 0.0F, windowPos + margin, ImGui::GetColorU32(ImGuiCol_Text), text, nullptr, windowSize.x - margin.x * 2); + } + } + drawList->PopClipRect(); + + } + s_highlightDisplays.clear(); + } + + void TutorialManager::drawMessageBox(std::optional message) { + const auto windowStart = ImHexApi::System::getMainWindowPosition() + scaled({ 10, 10 }); + const auto windowEnd = ImHexApi::System::getMainWindowPosition() + ImHexApi::System::getMainWindowSize() - scaled({ 10, 10 }); + + ImVec2 position = ImHexApi::System::getMainWindowPosition() + ImHexApi::System::getMainWindowSize() / 2.0F; + ImVec2 pivot = { 0.5F, 0.5F }; + + if (!message.has_value()) { + message = Tutorial::Step::Message { + Position::None, + "", + "", + false + }; + } + + if (message->position == Position::None) { + message->position = Position::Bottom | Position::Right; + } + + if ((message->position & Position::Top) == Position::Top) { + position.y = windowStart.y; + pivot.y = 0.0F; + } + if ((message->position & Position::Bottom) == Position::Bottom) { + position.y = windowEnd.y; + pivot.y = 1.0F; + } + if ((message->position & Position::Left) == Position::Left) { + position.x = windowStart.x; + pivot.x = 0.0F; + } + if ((message->position & Position::Right) == Position::Right) { + position.x = windowEnd.x; + pivot.x = 1.0F; + } + + ImGui::SetNextWindowPos(position, ImGuiCond_Always, pivot); + if (ImGui::Begin("##TutorialMessage", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing)) { + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + if (!message->unlocalizedTitle.empty()) + ImGuiExt::Header(Lang(message->unlocalizedTitle), true); + + if (!message->unlocalizedMessage.empty()) { + ImGui::PushTextWrapPos(300_scaled); + ImGui::TextUnformatted(Lang(message->unlocalizedMessage)); + ImGui::PopTextWrapPos(); + ImGui::NewLine(); + } + + ImGui::BeginDisabled(s_currentTutorial->second.m_currentStep == s_currentTutorial->second.m_steps.begin()); + if (ImGui::ArrowButton("Backwards", ImGuiDir_Left)) { + s_currentTutorial->second.m_currentStep->advance(-1); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::BeginDisabled(s_currentTutorial->second.m_currentStep == s_currentTutorial->second.m_steps.end() || (!message->allowSkip && s_currentTutorial->second.m_currentStep == s_currentTutorial->second.m_latestStep)); + if (ImGui::ArrowButton("Forwards", ImGuiDir_Right)) { + s_currentTutorial->second.m_currentStep->advance(1); + } + ImGui::EndDisabled(); + } + ImGui::End(); + } + + void TutorialManager::drawTutorial() { + drawHighlights(); + + if (s_currentTutorial == s_tutorials.end()) + return; + + const auto ¤tStep = s_currentTutorial->second.m_currentStep; + if (currentStep == s_currentTutorial->second.m_steps.end()) + return; + + const auto &message = currentStep->m_message; + drawMessageBox(message); + } + + + + void TutorialManager::reset() { + s_tutorials.clear(); + s_currentTutorial = s_tutorials.end(); + + s_highlights.clear(); + s_highlightDisplays.clear(); + } + + TutorialManager::IDStack::IDStack() { + idStack.push_back(0); + } + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::addStep() { + auto &newStep = this->m_steps.emplace_back(this); + this->m_currentStep = this->m_steps.begin(); + this->m_latestStep = this->m_currentStep; + + return newStep; + } + + void TutorialManager::Tutorial::start() { + this->m_currentStep = m_steps.begin(); + this->m_latestStep = this->m_currentStep; + if (m_currentStep == m_steps.end()) + return; + + m_currentStep->addHighlights(); + } + + void TutorialManager::Tutorial::Step::addHighlights() const { + for (const auto &[text, id] : this->m_highlights) { + s_highlights.emplace(id, text.c_str()); + } + } + + void TutorialManager::Tutorial::Step::removeHighlights() const { + for (const auto &[text, id] : this->m_highlights) { + s_highlights.erase(id); + } + } + + void TutorialManager::Tutorial::Step::advance(i32 steps) const { + m_parent->m_currentStep->removeHighlights(); + std::advance(s_currentTutorial->second.m_currentStep, steps); + + if (m_parent->m_currentStep != m_parent->m_steps.end()) + m_parent->m_currentStep->addHighlights(); + } + + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::addHighlight(const std::string& unlocalizedText, std::initializer_list>&& ids) { + IDStack idStack; + + for (const auto &id : ids) { + std::visit([&idStack](const auto &id) { + idStack.add(id); + }, id); + } + + this->m_highlights.emplace_back( + unlocalizedText, + idStack.get() + ); + + return *this; + } + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::addHighlight(std::initializer_list>&& ids) { + return this->addHighlight("", std::move(ids)); + } + + + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::setMessage(const std::string& unlocalizedTitle, const std::string& unlocalizedMessage, Position position) { + this->m_message = Message { + position, + unlocalizedTitle, + unlocalizedMessage, + false + }; + + return *this; + } + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::allowSkip() { + if (this->m_message.has_value()) { + this->m_message->allowSkip = true; + } else { + this->m_message = Message { + Position::Bottom | Position::Right, + "", + "", + true + }; + } + + return *this; + } + + + + bool TutorialManager::Tutorial::Step::isCurrent() const { + const auto ¤tStep = this->m_parent->m_currentStep; + + if (currentStep == this->m_parent->m_steps.end()) + return false; + + return &*currentStep == this; + } + + void TutorialManager::Tutorial::Step::complete() const { + if (this->isCurrent()) { + this->advance(); + this->m_parent->m_latestStep = this->m_parent->m_currentStep; + } + } + + + void TutorialManager::IDStack::add(const std::string &string) { + const ImGuiID seed = idStack.back(); + const ImGuiID id = ImHashStr(string.c_str(), string.length(), seed); + + idStack.push_back(id); + } + + void TutorialManager::IDStack::add(const void *pointer) { + const ImGuiID seed = idStack.back(); + const ImGuiID id = ImHashData(&pointer, sizeof(pointer), seed); + + idStack.push_back(id); + } + + void TutorialManager::IDStack::add(int value) { + const ImGuiID seed = idStack.back(); + const ImGuiID id = ImHashData(&value, sizeof(value), seed); + + idStack.push_back(id); + } + + ImGuiID TutorialManager::IDStack::get() { + return idStack.back(); + } + +} + +void ImGuiTestEngineHook_ItemAdd(ImGuiContext*, ImGuiID id, const ImRect& bb, const ImGuiLastItemData*) { + const auto element = hex::s_highlights.find(id); + if (element != hex::s_highlights.end()) { + hex::s_highlightDisplays.emplace_back(bb, element->second); + } +} + +void ImGuiTestEngineHook_ItemInfo(ImGuiContext*, ImGuiID, const char*, ImGuiItemStatusFlags) {} +void ImGuiTestEngineHook_Log(ImGuiContext*, const char*, ...) {} +const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext*, ImGuiID) { return nullptr; } \ No newline at end of file diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index 8cbef5c23..0386f293f 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -11,12 +11,15 @@ #define STB_IMAGE_IMPLEMENTATION #include +#include #include #include #include +#include +#include namespace ImGuiExt { @@ -172,7 +175,7 @@ namespace ImGuiExt { GetWindowDrawList()->AddLine(ImVec2(pos.x, pos.y + size.y), pos + size, ImU32(col)); PopStyleColor(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -204,7 +207,7 @@ namespace ImGuiExt { GetWindowDrawList()->AddLine(ImVec2(pos.x, pos.y + size.y), pos + size, ImU32(col)); PopStyleColor(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -239,7 +242,7 @@ namespace ImGuiExt { GetWindowDrawList()->AddLine(bb.Min + ImVec2(g.FontSize * 0.5 + style.FramePadding.x, size.y), pos + size - ImVec2(g.FontSize * 0.5 + style.FramePadding.x, 0), ImU32(col)); PopStyleColor(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -290,7 +293,7 @@ namespace ImGuiExt { // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -345,7 +348,7 @@ namespace ImGuiExt { // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -570,7 +573,7 @@ namespace ImGuiExt { // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -613,7 +616,7 @@ namespace ImGuiExt { // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, symbol, g.LastItemData.StatusFlags); return pressed; } @@ -656,7 +659,7 @@ namespace ImGuiExt { // if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, symbol, g.LastItemData.StatusFlags); return pressed; } diff --git a/lib/third_party/imgui/imgui/include/imconfig.h b/lib/third_party/imgui/imgui/include/imconfig.h index 743527051..446e0b516 100644 --- a/lib/third_party/imgui/imgui/include/imconfig.h +++ b/lib/third_party/imgui/imgui/include/imconfig.h @@ -127,6 +127,8 @@ namespace ImGui } */ +#define IMGUI_ENABLE_TEST_ENGINE + // IMPLOT CONFIG #define IMPLOT_CUSTOM_NUMERIC_TYPES (ImS8)(ImU8)(ImS16)(ImU16)(ImS32)(ImU32)(ImS64)(ImU64)(float)(double)(long double) diff --git a/main/gui/source/init/tasks.cpp b/main/gui/source/init/tasks.cpp index d15e66ff0..e46e94c1a 100644 --- a/main/gui/source/init/tasks.cpp +++ b/main/gui/source/init/tasks.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -137,6 +138,7 @@ namespace hex::init { ThemeManager::reset(); AchievementManager::getAchievements().clear(); + TutorialManager::reset(); ProjectFile::getHandlers().clear(); ProjectFile::getProviderHandlers().clear(); diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index 5ca64b3e7..1c8fdd03a 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -828,6 +829,8 @@ namespace hex { void Window::frameEnd() { EventFrameEnd::post(); + TutorialManager::drawTutorial(); + // Clean up all tasks that are done TaskManager::collectGarbage(); @@ -1194,7 +1197,10 @@ namespace hex { }; handler.UserData = this; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(handler); + + auto context = ImGui::GetCurrentContext(); + context->SettingsHandlers.push_back(handler); + context->TestEngineHookItems = true; io.IniFilename = nullptr; } diff --git a/plugins/builtin/source/content/views/view_information.cpp b/plugins/builtin/source/content/views/view_information.cpp index 8c0f9e023..3a3ef1f24 100644 --- a/plugins/builtin/source/content/views/view_information.cpp +++ b/plugins/builtin/source/content/views/view_information.cpp @@ -160,8 +160,6 @@ namespace hex::plugin::builtin { ImGui::SetCursorPosX(50_scaled); if (ImGuiExt::DimmedButton("hex.builtin.view.information.analyze"_lang, ImVec2(ImGui::GetContentRegionAvail().x - 50_scaled, 0))) this->analyze(); - - } ImGuiExt::EndSubWindow(); ImGui::EndDisabled();