From f000b6bc0afc9dbe65dac8eece7c649685b41b9f Mon Sep 17 00:00:00 2001 From: WerWolv Date: Wed, 13 Dec 2023 23:03:39 +0100 Subject: [PATCH] feat: Added basic introduction tutorial --- .../include/hex/api/event_manager.hpp | 7 +- .../include/hex/api/tutorial_manager.hpp | 6 +- lib/libimhex/include/hex/ui/view.hpp | 4 +- lib/libimhex/source/api/tutorial_manager.cpp | 90 +++++++++++---- lib/libimhex/source/ui/view.cpp | 7 ++ main/gui/source/window/window.cpp | 2 + plugins/builtin/CMakeLists.txt | 3 + .../include/content/views/view_tutorials.hpp | 2 +- plugins/builtin/romfs/lang/en_US.json | 13 +++ .../source/content/main_menu_items.cpp | 3 +- .../source/content/tutorials/introduction.cpp | 106 ++++++++++++++++++ .../source/content/tutorials/tutorials.cpp | 9 ++ .../content/views/view_pattern_editor.cpp | 5 + .../source/content/views/view_tutorials.cpp | 3 + .../builtin/source/content/welcome_screen.cpp | 12 ++ plugins/builtin/source/plugin_builtin.cpp | 2 + 16 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 plugins/builtin/source/content/tutorials/introduction.cpp create mode 100644 plugins/builtin/source/content/tutorials/tutorials.cpp diff --git a/lib/libimhex/include/hex/api/event_manager.hpp b/lib/libimhex/include/hex/api/event_manager.hpp index 8f6a99f99..a710e4d8f 100644 --- a/lib/libimhex/include/hex/api/event_manager.hpp +++ b/lib/libimhex/include/hex/api/event_manager.hpp @@ -32,7 +32,10 @@ /* Forward declarations */ struct GLFWwindow; -namespace hex { class Achievement; } +namespace hex { + class Achievement; + class View; +} namespace hex { @@ -248,6 +251,7 @@ namespace hex { EVENT_DEF(EventImHexClosing); EVENT_DEF(EventAchievementUnlocked, const Achievement&); EVENT_DEF(EventSearchBoxClicked); + EVENT_DEF(EventViewOpened, View*); EVENT_DEF(EventProviderDataModified, prv::Provider *, u64, u64, const u8*); EVENT_DEF(EventProviderDataInserted, prv::Provider *, u64, u64); @@ -269,6 +273,7 @@ namespace hex { EVENT_DEF(RequestAddBookmark, Region, std::string, std::string, color_t, u64*); EVENT_DEF(RequestRemoveBookmark, u64); EVENT_DEF(RequestSetPatternLanguageCode, std::string); + EVENT_DEF(RequestRunPatternCode); EVENT_DEF(RequestLoadPatternLanguageFile, std::fs::path); EVENT_DEF(RequestSavePatternLanguageFile, std::fs::path); EVENT_DEF(RequestUpdateWindowTitle); diff --git a/lib/libimhex/include/hex/api/tutorial_manager.hpp b/lib/libimhex/include/hex/api/tutorial_manager.hpp index 4ec21a7d9..c517db3c0 100644 --- a/lib/libimhex/include/hex/api/tutorial_manager.hpp +++ b/lib/libimhex/include/hex/api/tutorial_manager.hpp @@ -61,6 +61,9 @@ namespace hex { */ Step& allowSkip(); + Step& onAppear(std::function callback); + Step& onComplete(std::function callback); + /** * @brief Checks if this step is the current step @@ -76,7 +79,7 @@ namespace hex { private: struct Highlight { std::string unlocalizedText; - ImGuiID highlightId; + std::vector> highlightIds; }; struct Message { @@ -97,6 +100,7 @@ namespace hex { Tutorial *m_parent; std::vector m_highlights; std::optional m_message; + std::function m_onAppear, m_onComplete; }; Step& addStep(); diff --git a/lib/libimhex/include/hex/ui/view.hpp b/lib/libimhex/include/hex/ui/view.hpp index 866372205..28c0135cc 100644 --- a/lib/libimhex/include/hex/ui/view.hpp +++ b/lib/libimhex/include/hex/ui/view.hpp @@ -92,6 +92,8 @@ namespace hex { [[nodiscard]] bool didWindowJustOpen(); void setWindowJustOpened(bool state); + void trackViewOpenState(); + static void discardNavigationRequests(); [[nodiscard]] static std::string toWindowName(const std::string &unlocalizedName); @@ -104,7 +106,7 @@ namespace hex { private: std::string m_unlocalizedViewName; - bool m_windowOpen = false; + bool m_windowOpen = false, m_prevWindowOpen = false; std::map m_shortcuts; bool m_windowJustOpened = false; diff --git a/lib/libimhex/source/api/tutorial_manager.cpp b/lib/libimhex/source/api/tutorial_manager.cpp index 92ea2b775..a3097c6ed 100644 --- a/lib/libimhex/source/api/tutorial_manager.cpp +++ b/lib/libimhex/source/api/tutorial_manager.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -91,15 +92,23 @@ namespace hex { { if (!unlocalizedText.empty()) { - const auto margin = ImGui::GetStyle().WindowPadding; + const auto mainWindowPos = ImHexApi::System::getMainWindowPosition(); + const auto mainWindowSize = ImHexApi::System::getMainWindowSize(); - const ImVec2 windowPos = { rect.Min.x + 20_scaled, rect.Max.y + 10_scaled }; + const auto margin = ImGui::GetStyle().WindowPadding; + + 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; + if (windowPos.y + windowSize.y > mainWindowPos.y + mainWindowSize.y) + windowPos.y = rect.Min.y - windowSize.y - 10_scaled; + if (windowPos.y < mainWindowPos.y) + windowPos.y = mainWindowPos.y + 10_scaled; + 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); @@ -170,7 +179,7 @@ namespace hex { 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)); + ImGui::BeginDisabled(!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); } @@ -221,19 +230,51 @@ namespace hex { } void TutorialManager::Tutorial::Step::addHighlights() const { - for (const auto &[text, id] : this->m_highlights) { - s_highlights.emplace(id, text.c_str()); + if (this->m_onAppear) + this->m_onAppear(); + + for (const auto &[text, ids] : this->m_highlights) { + IDStack idStack; + + for (const auto &id : ids) { + std::visit(wolv::util::overloaded { + [&idStack](const Lang &id) { + idStack.add(id.get()); + }, + [&idStack](const auto &id) { + idStack.add(id); + } + }, id); + } + + s_highlights.emplace(idStack.get(), text.c_str()); } } void TutorialManager::Tutorial::Step::removeHighlights() const { - for (const auto &[text, id] : this->m_highlights) { - s_highlights.erase(id); + for (const auto &[text, ids] : this->m_highlights) { + IDStack idStack; + + for (const auto &id : ids) { + std::visit(wolv::util::overloaded { + [&idStack](const Lang &id) { + idStack.add(id.get()); + }, + [&idStack](const auto &id) { + idStack.add(id); + } + }, id); + } + + s_highlights.erase(idStack.get()); } } void TutorialManager::Tutorial::Step::advance(i32 steps) const { m_parent->m_currentStep->removeHighlights(); + + if (m_parent->m_currentStep == m_parent->m_latestStep && steps > 0) + std::advance(m_parent->m_latestStep, steps); std::advance(m_parent->m_currentStep, steps); if (m_parent->m_currentStep != m_parent->m_steps.end()) @@ -244,22 +285,9 @@ namespace hex { TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::addHighlight(const std::string& unlocalizedText, std::initializer_list>&& ids) { - IDStack idStack; - - for (const auto &id : ids) { - std::visit(wolv::util::overloaded { - [&idStack](const Lang &id) { - idStack.add(id.get()); - }, - [&idStack](const auto &id) { - idStack.add(id); - } - }, id); - } - this->m_highlights.emplace_back( unlocalizedText, - idStack.get() + ids ); return *this; @@ -297,6 +325,19 @@ namespace hex { return *this; } + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::onAppear(std::function callback) { + this->m_onAppear = std::move(callback); + + return *this; + } + + TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::onComplete(std::function callback) { + this->m_onComplete = std::move(callback); + + return *this; + } + + bool TutorialManager::Tutorial::Step::isCurrent() const { @@ -311,7 +352,12 @@ namespace hex { void TutorialManager::Tutorial::Step::complete() const { if (this->isCurrent()) { this->advance(); - this->m_parent->m_latestStep = this->m_parent->m_currentStep; + + if (this->m_onComplete) { + TaskManager::doLater([this] { + this->m_onComplete(); + }); + } } } diff --git a/lib/libimhex/source/ui/view.cpp b/lib/libimhex/source/ui/view.cpp index 6430d2dd0..8abb364f9 100644 --- a/lib/libimhex/source/ui/view.cpp +++ b/lib/libimhex/source/ui/view.cpp @@ -59,6 +59,13 @@ namespace hex { this->m_windowJustOpened = state; } + void View::trackViewOpenState() { + if (this->m_windowOpen && !this->m_prevWindowOpen) + this->setWindowJustOpened(true); + this->m_prevWindowOpen = this->m_windowOpen; + } + + void View::discardNavigationRequests() { if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index 1c8fdd03a..99b06f007 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -789,6 +789,7 @@ namespace hex { // Draw view view->draw(); + view->trackViewOpenState(); if (view->getWindowOpenState()) { auto window = ImGui::FindWindowByName(view->getName().c_str()); @@ -806,6 +807,7 @@ namespace hex { // Dock the window if it's not already docked if (view->didWindowJustOpen() && !ImGui::IsWindowDocked()) { ImGui::DockBuilderDockWindow(windowName.c_str(), ImHexApi::System::getMainDockSpaceId()); + EventViewOpened::post(view.get()); } ImGui::End(); diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 261cad505..23678db08 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -82,6 +82,9 @@ add_imhex_plugin( source/content/tools/tcp_client_server.cpp source/content/tools/wiki_explainer.cpp + source/content/tutorials/tutorials.cpp + source/content/tutorials/introduction.cpp + source/content/pl_visualizers/line_plot.cpp source/content/pl_visualizers/scatter_plot.cpp source/content/pl_visualizers/image.cpp diff --git a/plugins/builtin/include/content/views/view_tutorials.hpp b/plugins/builtin/include/content/views/view_tutorials.hpp index 736c0132a..834f29186 100644 --- a/plugins/builtin/include/content/views/view_tutorials.hpp +++ b/plugins/builtin/include/content/views/view_tutorials.hpp @@ -17,7 +17,7 @@ namespace hex::plugin::builtin { [[nodiscard]] bool hasViewMenuItemEntry() const override { return false; } ImVec2 getMinSize() const override { - return scaled({ 400, 300 }); + return scaled({ 600, 400 }); } ImVec2 getMaxSize() const override { diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 1849fbb71..0427855a0 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -740,6 +740,18 @@ "hex.builtin.tools.wiki_explain.invalid_response": "Invalid response from Wikipedia!", "hex.builtin.tools.wiki_explain.results": "Results", "hex.builtin.tools.wiki_explain.search": "Search", + "hex.builtin.tutorial.introduction": "Introduction to ImHex", + "hex.builtin.tutorial.introduction.description": "This tutorial will guide you through the basic usage of ImHex to get you started.", + "hex.builtin.tutorial.introduction.step1.title": "Welcome to ImHex!", + "hex.builtin.tutorial.introduction.step1.description": "ImHex is a Reverse Engineering Suite and Hex Editor with its main focus on visualizing binary data for easy comprehension.\n\nYou can continue to the next step by clicking the right arrow button below.", + "hex.builtin.tutorial.introduction.step2.title": "Opening Data", + "hex.builtin.tutorial.introduction.step2.description": "ImHex supports loading data from a variety of sources. This includes Files, Raw disks, another Process's memory and more.\n\nAll these options can be found on the Welcome screen or under the File menu.", + "hex.builtin.tutorial.introduction.step2.highlight": "Let's create a new empty file by clicking on the 'New File' button.", + "hex.builtin.tutorial.introduction.step3.highlight": "This is the Hex Editor. It displays the individual bytes of the loaded data and also allows you to edit them by double clicking one.\n\nYou can navigate the data by using the arrow keys or the mouse wheel.", + "hex.builtin.tutorial.introduction.step4.highlight": "This is the Data Inspector. It displays the data of the currently selected bytes in a more readable format.\n\nYou can also edit the data here by double clicking on a row.", + "hex.builtin.tutorial.introduction.step5.highlight.pattern_editor": "This is the Pattern Editor. It allows you to write code using the Pattern Language which can highlight and decode binary data structures inside of your loaded data.\n\nYou can learn more about the Pattern Language in the documentation.", + "hex.builtin.tutorial.introduction.step5.highlight.pattern_data": "This view contains a tree view representing the data structures you defined using the Pattern Language.", + "hex.builtin.tutorial.introduction.step6.highlight": "You can find more tutorials and documentation in the Help menu.", "hex.builtin.undo_operation.insert": "Inserted {0}", "hex.builtin.undo_operation.remove": "Removed {0}", "hex.builtin.undo_operation.write": "Wrote {0}", @@ -1180,6 +1192,7 @@ "hex.builtin.popup.safety_backup.report_error": "Send crash log to developers", "hex.builtin.popup.safety_backup.restore": "Yes, Restore", "hex.builtin.popup.safety_backup.title": "Restore lost data", + "hex.builtin.popup.play_tutorial.desc": "As this is your first time launching ImHex, would you like to play through the interactive Tutorial?", "hex.builtin.welcome.start.create_file": "Create New File", "hex.builtin.welcome.start.open_file": "Open File", "hex.builtin.welcome.start.open_other": "Other Providers", diff --git a/plugins/builtin/source/content/main_menu_items.cpp b/plugins/builtin/source/content/main_menu_items.cpp index aff91d370..1854cd9e5 100644 --- a/plugins/builtin/source/content/main_menu_items.cpp +++ b/plugins/builtin/source/content/main_menu_items.cpp @@ -522,8 +522,7 @@ namespace hex::plugin::builtin { if (view->hasViewMenuItemEntry()) { auto &state = view->getWindowOpenState(); - if (ImGui::MenuItem(Lang(view->getUnlocalizedName()), "", &state, ImHexApi::Provider::isValid() && !LayoutManager::isLayoutLocked())) - view->setWindowJustOpened(state); + ImGui::MenuItem(Lang(view->getUnlocalizedName()), "", &state, ImHexApi::Provider::isValid() && !LayoutManager::isLayoutLocked()); } } diff --git a/plugins/builtin/source/content/tutorials/introduction.cpp b/plugins/builtin/source/content/tutorials/introduction.cpp new file mode 100644 index 000000000..5c5ae0212 --- /dev/null +++ b/plugins/builtin/source/content/tutorials/introduction.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +namespace hex::plugin::builtin { + + void registerIntroductionTutorial() { + using enum TutorialManager::Position; + auto &tutorial = TutorialManager::createTutorial("hex.builtin.tutorial.introduction", "hex.builtin.tutorial.introduction.description"); + + { + tutorial.addStep() + .setMessage( + "hex.builtin.tutorial.introduction.step1.title", + "hex.builtin.tutorial.introduction.step1.description", + Bottom | Right + ) + .allowSkip(); + } + + { + auto &step = tutorial.addStep(); + + step.setMessage( + "hex.builtin.tutorial.introduction.step2.title", + "hex.builtin.tutorial.introduction.step2.description", + Bottom | Right + ) + .addHighlight("hex.builtin.tutorial.introduction.step2.highlight", + { + "Welcome Screen/Start##SubWindow_685A2CE9", + Lang("hex.builtin.welcome.start.create_file") + }) + .onAppear([&step] { + EventProviderOpened::subscribe(&step, [&step](prv::Provider *provider) { + if (dynamic_cast(provider)) + step.complete(); + }); + }) + .onComplete([&step] { + EventProviderOpened::unsubscribe(&step); + }); + } + + { + tutorial.addStep() + .addHighlight("hex.builtin.tutorial.introduction.step3.highlight", { + View::toWindowName("hex.builtin.view.hex_editor.name") + }) + .allowSkip(); + } + + { + tutorial.addStep() + .addHighlight("hex.builtin.tutorial.introduction.step4.highlight", { + View::toWindowName("hex.builtin.view.data_inspector.name") + }) + .onAppear([]{ + ImHexApi::HexEditor::setSelection(Region { 0, 1 }); + }) + .allowSkip(); + } + + { + tutorial.addStep() + .addHighlight("hex.builtin.tutorial.introduction.step5.highlight.pattern_editor", { + View::toWindowName("hex.builtin.view.pattern_editor.name") + }) + .addHighlight("hex.builtin.tutorial.introduction.step5.highlight.pattern_data", { + View::toWindowName("hex.builtin.view.pattern_data.name") + }) + .onAppear([] { + RequestSetPatternLanguageCode::post("\n\n\n\n\n\nstruct Test {\n u8 value;\n};\n\nTest test @ 0x00;"); + RequestRunPatternCode::post(); + }) + .allowSkip(); + } + + { + auto &step = tutorial.addStep(); + + step.addHighlight("hex.builtin.tutorial.introduction.step6.highlight", { + "##MainMenuBar", + "##menubar", + Lang("hex.builtin.menu.help") + }) + .addHighlight({ + "##Menu_00", + Lang("hex.builtin.view.tutorials.name") + }) + .onAppear([&step] { + EventViewOpened::subscribe([&step](View *view){ + if (view->getUnlocalizedName() == "hex.builtin.view.tutorials.name") + step.complete(); + }); + }) + .onComplete([&step]{ + EventViewOpened::unsubscribe(&step); + }) + .allowSkip(); + } + } + +} diff --git a/plugins/builtin/source/content/tutorials/tutorials.cpp b/plugins/builtin/source/content/tutorials/tutorials.cpp new file mode 100644 index 000000000..00b198f32 --- /dev/null +++ b/plugins/builtin/source/content/tutorials/tutorials.cpp @@ -0,0 +1,9 @@ +namespace hex::plugin::builtin { + + void registerIntroductionTutorial(); + + void registerTutorials() { + registerIntroductionTutorial(); + } + +} \ No newline at end of file diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index 4f9b4a6ca..9eeaf80bc 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -145,6 +145,7 @@ namespace hex::plugin::builtin { ViewPatternEditor::~ViewPatternEditor() { RequestSetPatternLanguageCode::unsubscribe(this); + RequestRunPatternCode::unsubscribe(this); EventFileLoaded::unsubscribe(this); EventProviderChanged::unsubscribe(this); EventProviderClosed::unsubscribe(this); @@ -1103,6 +1104,10 @@ namespace hex::plugin::builtin { this->loadPatternFile(path, ImHexApi::Provider::get()); }); + RequestRunPatternCode::subscribe(this, [this] { + this->m_triggerAutoEvaluate = true; + }); + RequestSavePatternLanguageFile::subscribe(this, [this](const std::fs::path &path) { wolv::io::File file(path, wolv::io::File::Mode::Create); file.writeString(wolv::util::trim(this->m_textEditor.GetText())); diff --git a/plugins/builtin/source/content/views/view_tutorials.cpp b/plugins/builtin/source/content/views/view_tutorials.cpp index 25ba2d8de..bcb2fbf6a 100644 --- a/plugins/builtin/source/content/views/view_tutorials.cpp +++ b/plugins/builtin/source/content/views/view_tutorials.cpp @@ -27,6 +27,9 @@ namespace hex::plugin::builtin { if (ImGui::BeginTable("Tutorials", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImGui::GetContentRegionAvail())) { for (const auto &tutorial : tutorials | std::views::values) { + if (this->m_selectedTutorial == nullptr) + this->m_selectedTutorial = &tutorial; + ImGui::TableNextRow(); ImGui::TableNextColumn(); diff --git a/plugins/builtin/source/content/welcome_screen.cpp b/plugins/builtin/source/content/welcome_screen.cpp index df19952d5..3482284fa 100644 --- a/plugins/builtin/source/content/welcome_screen.cpp +++ b/plugins/builtin/source/content/welcome_screen.cpp @@ -30,6 +30,8 @@ #include #include +#include +#include #include namespace hex::plugin::builtin { @@ -539,6 +541,16 @@ namespace hex::plugin::builtin { PopupTelemetryRequest::open(); #endif } + + if (ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.prev_launch_version", "") == "") { + PopupQuestion::open("hex.builtin.popup.play_tutorial.desc"_lang, + []{ + TutorialManager::startTutorial("hex.builtin.tutorial.introduction"); + }, + []{ }); + } + + ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.prev_launch_version", ImHexApi::System::getImHexVersion()); }); // Clear project context if we go back to the welcome screen diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index b7cc3a155..9f0354d9d 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -39,6 +39,7 @@ namespace hex::plugin::builtin { void registerProjectHandlers(); void registerAchievements(); void registerReportGenerators(); + void registerTutorials(); void loadWorkspaces(); void addFooterItems(); @@ -109,6 +110,7 @@ IMHEX_PLUGIN_SETUP("Built-in", "WerWolv", "Default ImHex functionality") { registerCommandForwarders(); registerAchievements(); registerReportGenerators(); + registerTutorials(); loadWorkspaces(); addFooterItems();