mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-28 07:47:03 -05:00
<!-- Please provide as much information as possible about what your PR aims to do. PRs with no description will most likely be closed until more information is provided. If you're planing on changing fundamental behaviour or add big new features, please open a GitHub Issue first before starting to work on it. If it's not something big and you still want to contact us about it, feel free to do so ! --> ### Problem description <!-- Describe the bug that you fixed/feature request that you implemented, or link to an existing issue describing it --> ### Implementation description <!-- Explain what you did to correct the problem --> ### Screenshots <!-- If your change is visual, take a screenshot showing it. Ideally, make before/after sceenshots --> ### Additional things <!-- Anything else you would like to say -->
546 lines
19 KiB
C++
546 lines
19 KiB
C++
#include <hex/api/tutorial_manager.hpp>
|
|
#include <hex/api/imhex_api/system.hpp>
|
|
#include <hex/api/localization_manager.hpp>
|
|
#include <hex/api/task_manager.hpp>
|
|
#include <hex/api/events/events_gui.hpp>
|
|
|
|
#include <hex/helpers/auto_reset.hpp>
|
|
|
|
#include <imgui_internal.h>
|
|
#include <hex/helpers/utils.hpp>
|
|
#include <hex/helpers/scaling.hpp>
|
|
#include <wolv/utils/core.hpp>
|
|
|
|
#include <map>
|
|
|
|
#include <imgui.h>
|
|
|
|
namespace hex {
|
|
|
|
namespace {
|
|
|
|
AutoReset<std::map<std::string, TutorialManager::Tutorial>> s_tutorials;
|
|
auto s_currentTutorial = s_tutorials->end();
|
|
|
|
AutoReset<std::map<ImGuiID, std::string>> s_highlights;
|
|
AutoReset<std::vector<std::pair<ImRect, std::string>>> s_highlightDisplays;
|
|
AutoReset<std::map<ImGuiID, ImRect>> s_interactiveHelpDisplays;
|
|
|
|
AutoReset<std::map<ImGuiID, std::function<void()>>> s_interactiveHelpItems;
|
|
ImRect s_hoveredRect;
|
|
ImGuiID s_hoveredId;
|
|
ImGuiID s_activeHelpId;
|
|
bool s_helpHoverActive = false;
|
|
|
|
AutoReset<std::function<std::function<void()>(const std::string &)>> s_renderer;
|
|
|
|
|
|
class IDStack {
|
|
public:
|
|
IDStack() {
|
|
idStack.push_back(0);
|
|
}
|
|
|
|
void add(const char *string) {
|
|
const ImGuiID seed = idStack.back();
|
|
const ImGuiID id = ImHashStr(string, 0, seed);
|
|
|
|
idStack.push_back(id);
|
|
}
|
|
|
|
void 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 add(const void *pointer) {
|
|
const ImGuiID seed = idStack.back();
|
|
const ImGuiID id = ImHashData((const void*) &pointer, sizeof(pointer), seed);
|
|
|
|
idStack.push_back(id);
|
|
}
|
|
|
|
void add(int value) {
|
|
const ImGuiID seed = idStack.back();
|
|
const ImGuiID id = ImHashData(&value, sizeof(value), seed);
|
|
|
|
idStack.push_back(id);
|
|
}
|
|
|
|
ImGuiID get() {
|
|
return idStack.back();
|
|
}
|
|
private:
|
|
ImVector<ImGuiID> idStack;
|
|
};
|
|
|
|
ImGuiID calculateId(const auto &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);
|
|
}
|
|
|
|
return idStack.get();
|
|
}
|
|
|
|
}
|
|
|
|
void TutorialManager::init() {
|
|
EventImGuiElementRendered::subscribe([](ImGuiID id, const std::array<float, 4> bb){
|
|
const auto boundingBox = ImRect(bb[0], bb[1], bb[2], bb[3]);
|
|
|
|
if (!ImGui::IsRectVisible(boundingBox.Min, boundingBox.Max))
|
|
return;
|
|
|
|
{
|
|
const auto element = hex::s_highlights->find(id);
|
|
if (element != hex::s_highlights->end()) {
|
|
hex::s_highlightDisplays->emplace_back(boundingBox, element->second);
|
|
|
|
const auto window = ImGui::GetCurrentWindow();
|
|
if (window != nullptr && window->DockNode != nullptr && window->DockNode->TabBar != nullptr)
|
|
window->DockNode->TabBar->NextSelectedTabId = window->TabId;
|
|
}
|
|
}
|
|
|
|
{
|
|
const auto element = s_interactiveHelpItems->find(id);
|
|
if (element != s_interactiveHelpItems->end()) {
|
|
(*s_interactiveHelpDisplays)[id] = boundingBox;
|
|
}
|
|
|
|
}
|
|
|
|
if (id != 0 && boundingBox.Contains(ImGui::GetMousePos())) {
|
|
if ((s_hoveredRect.GetArea() == 0 || boundingBox.GetArea() < s_hoveredRect.GetArea()) && s_interactiveHelpItems->contains(id)) {
|
|
s_hoveredRect = boundingBox;
|
|
s_hoveredId = id;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (*s_renderer == nullptr) {
|
|
*s_renderer = [](const std::string &message) {
|
|
return [message] {
|
|
ImGui::PushTextWrapPos(300_scaled);
|
|
ImGui::TextUnformatted(message.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::NewLine();
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
const std::map<std::string, TutorialManager::Tutorial>& TutorialManager::getTutorials() {
|
|
return s_tutorials;
|
|
}
|
|
|
|
std::map<std::string, TutorialManager::Tutorial>::iterator TutorialManager::getCurrentTutorial() {
|
|
return s_currentTutorial;
|
|
}
|
|
|
|
|
|
TutorialManager::Tutorial& TutorialManager::createTutorial(const UnlocalizedString &unlocalizedName, const UnlocalizedString &unlocalizedDescription) {
|
|
return s_tutorials->try_emplace(unlocalizedName, Tutorial(unlocalizedName, unlocalizedDescription)).first->second;
|
|
}
|
|
|
|
void TutorialManager::startHelpHover() {
|
|
TaskManager::doLater([]{
|
|
s_helpHoverActive = true;
|
|
});
|
|
}
|
|
|
|
void TutorialManager::addInteractiveHelpText(std::initializer_list<std::variant<Lang, std::string, int>> &&ids, UnlocalizedString unlocalizedString) {
|
|
auto id = calculateId(ids);
|
|
|
|
s_interactiveHelpItems->emplace(id, [text = std::move(unlocalizedString)]{
|
|
log::info("{}", Lang(text).get());
|
|
});
|
|
}
|
|
|
|
void TutorialManager::addInteractiveHelpLink(std::initializer_list<std::variant<Lang, std::string, int>> &&ids, std::string link) {
|
|
auto id = calculateId(ids);
|
|
|
|
s_interactiveHelpItems->emplace(id, [link = std::move(link)]{
|
|
hex::openWebpage(link);
|
|
});
|
|
}
|
|
|
|
void TutorialManager::setLastItemInteractiveHelpPopup(std::function<void()> callback) {
|
|
auto id = ImGui::GetItemID();
|
|
|
|
if (!s_interactiveHelpItems->contains(id)) {
|
|
s_interactiveHelpItems->emplace(id, [id]{
|
|
s_activeHelpId = id;
|
|
});
|
|
}
|
|
|
|
if (id == s_activeHelpId) {
|
|
ImGui::SetNextWindowSize(scaled({ 400, 0 }));
|
|
if (ImGui::BeginTooltip()) {
|
|
callback();
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) || ImGui::IsKeyPressed(ImGuiKey_Escape))
|
|
s_activeHelpId = 0;
|
|
}
|
|
}
|
|
|
|
void TutorialManager::setLastItemInteractiveHelpLink(std::string link) {
|
|
auto id = ImGui::GetItemID();
|
|
|
|
if (s_interactiveHelpItems->contains(id))
|
|
return;
|
|
|
|
s_interactiveHelpItems->emplace(id, [link = std::move(link)]{
|
|
hex::openWebpage(link);
|
|
});
|
|
}
|
|
|
|
|
|
void TutorialManager::startTutorial(const UnlocalizedString &unlocalizedName) {
|
|
s_currentTutorial = s_tutorials->find(unlocalizedName);
|
|
if (s_currentTutorial == s_tutorials->end())
|
|
return;
|
|
|
|
s_currentTutorial->second.start();
|
|
}
|
|
|
|
void TutorialManager::stopCurrentTutorial() {
|
|
s_currentTutorial = s_tutorials->end();
|
|
}
|
|
|
|
void TutorialManager::drawHighlights() {
|
|
if (s_helpHoverActive) {
|
|
const auto &drawList = ImGui::GetForegroundDrawList(ImGui::GetMainViewport());
|
|
drawList->AddText(ImGui::GetMousePos() + scaled({ 10, -5, }), ImGui::GetColorU32(ImGuiCol_Text), "?");
|
|
|
|
for (const auto &[id, boundingBox] : *s_interactiveHelpDisplays) {
|
|
drawList->AddRect(
|
|
boundingBox.Min - ImVec2(5, 5),
|
|
boundingBox.Max + ImVec2(5, 5),
|
|
ImGui::GetColorU32(ImGuiCol_PlotHistogram),
|
|
5.0F,
|
|
ImDrawFlags_None,
|
|
2.0F
|
|
);
|
|
}
|
|
|
|
s_interactiveHelpDisplays->clear();
|
|
|
|
const bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
|
if (s_hoveredId != 0) {
|
|
drawList->AddRectFilled(s_hoveredRect.Min, s_hoveredRect.Max, 0x30FFFFFF);
|
|
|
|
if (mouseClicked) {
|
|
auto it = s_interactiveHelpItems->find(s_hoveredId);
|
|
if (it != s_interactiveHelpItems->end()) {
|
|
it->second();
|
|
}
|
|
}
|
|
|
|
s_hoveredId = 0;
|
|
s_hoveredRect = {};
|
|
}
|
|
|
|
if (mouseClicked || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
|
s_helpHoverActive = false;
|
|
}
|
|
|
|
// Discard mouse click so it doesn't activate clicked item
|
|
ImGui::GetIO().MouseDown[ImGuiMouseButton_Left] = false;
|
|
ImGui::GetIO().MouseReleased[ImGuiMouseButton_Left] = false;
|
|
ImGui::GetIO().MouseClicked[ImGuiMouseButton_Left] = false;
|
|
}
|
|
|
|
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;
|
|
ImHexApi::System::unlockFrameRate();
|
|
|
|
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 mainWindowPos = ImHexApi::System::getMainWindowPosition();
|
|
const auto mainWindowSize = ImHexApi::System::getMainWindowSize();
|
|
|
|
const auto margin = ImGui::GetStyle().WindowPadding;
|
|
|
|
ImVec2 windowPos = { rect.Min.x + 20_scaled, rect.Max.y + 10_scaled };
|
|
ImVec2 windowSize = { std::max<float>(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 - 15_scaled;
|
|
if (windowPos.y < mainWindowPos.y)
|
|
windowPos.y = rect.Min.y + 10_scaled;
|
|
|
|
ImGui::SetNextWindowPos(windowPos);
|
|
ImGui::SetNextWindowSize(windowSize);
|
|
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
|
if (ImGui::Begin(unlocalizedText.c_str(), nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) {
|
|
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindowRead());
|
|
ImGuiExt::TextFormattedWrapped("{}", text);
|
|
}
|
|
ImGui::End();
|
|
}
|
|
}
|
|
drawList->PopClipRect();
|
|
|
|
}
|
|
s_highlightDisplays->clear();
|
|
}
|
|
|
|
void TutorialManager::drawMessageBox(std::optional<Tutorial::Step::Message> 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=Position::None,
|
|
.unlocalizedTitle="",
|
|
.unlocalizedMessage="",
|
|
.allowSkip=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);
|
|
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
|
ImGui::SetNextWindowSize(ImVec2(300_scaled, 0));
|
|
|
|
bool open = true;
|
|
if (ImGui::Begin(message->unlocalizedTitle.empty() ? "##TutorialMessage" : Lang(message->unlocalizedTitle), &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing)) {
|
|
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindowRead());
|
|
|
|
auto &step = s_currentTutorial->second.m_currentStep;
|
|
if (!message->unlocalizedMessage.empty()) {
|
|
step->m_drawFunction();
|
|
ImGui::NewLine();
|
|
ImGui::NewLine();
|
|
}
|
|
|
|
ImGui::BeginDisabled(step == s_currentTutorial->second.m_steps.begin());
|
|
if (ImGuiExt::DimmedArrowButton("Backwards", ImGuiDir_Left)) {
|
|
s_currentTutorial->second.m_currentStep->advance(-1);
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::SameLine();
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - ImGui::GetFrameHeight() - ImGui::GetStyle().WindowPadding.x);
|
|
ImGui::BeginDisabled(!message->allowSkip && step == s_currentTutorial->second.m_latestStep);
|
|
if (ImGuiExt::DimmedArrowButton("Forwards", ImGuiDir_Right)) {
|
|
step->advance(1);
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
ImGui::End();
|
|
|
|
if (!open) {
|
|
stopCurrentTutorial();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void TutorialManager::setRenderer(std::function<DrawFunction(const std::string &)> renderer) {
|
|
s_renderer = std::move(renderer);
|
|
}
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::addStep() {
|
|
auto &newStep = m_steps.emplace_back(this);
|
|
m_currentStep = m_steps.end();
|
|
m_latestStep = m_currentStep;
|
|
|
|
return newStep;
|
|
}
|
|
|
|
void TutorialManager::Tutorial::start() {
|
|
m_currentStep = m_steps.begin();
|
|
m_latestStep = m_currentStep;
|
|
if (m_currentStep == m_steps.end())
|
|
return;
|
|
|
|
m_currentStep->addHighlights();
|
|
|
|
if (m_currentStep->m_message.has_value())
|
|
m_currentStep->m_drawFunction = (*s_renderer)(Lang(m_currentStep->m_message->unlocalizedMessage));
|
|
}
|
|
|
|
void TutorialManager::Tutorial::Step::addHighlights() const {
|
|
if (m_onAppear)
|
|
m_onAppear();
|
|
|
|
for (const auto &[text, ids] : m_highlights) {
|
|
s_highlights->emplace(calculateId(ids), text);
|
|
}
|
|
}
|
|
|
|
void TutorialManager::Tutorial::Step::removeHighlights() const {
|
|
for (const auto &[text, ids] : m_highlights) {
|
|
s_highlights->erase(calculateId(ids));
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
m_parent->m_currentStep->addHighlights();
|
|
|
|
if (m_message.has_value())
|
|
m_parent->m_currentStep->m_drawFunction = (*s_renderer)(Lang(m_parent->m_currentStep->m_message->unlocalizedMessage));
|
|
}
|
|
else
|
|
s_currentTutorial = s_tutorials->end();
|
|
}
|
|
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::addHighlight(const UnlocalizedString &unlocalizedText, std::initializer_list<std::variant<Lang, std::string, int>>&& ids) {
|
|
m_highlights.emplace_back(
|
|
unlocalizedText,
|
|
ids
|
|
);
|
|
|
|
return *this;
|
|
}
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::addHighlight(std::initializer_list<std::variant<Lang, std::string, int>>&& ids) {
|
|
return this->addHighlight("", std::forward<decltype(ids)>(ids));
|
|
}
|
|
|
|
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::setMessage(const UnlocalizedString &unlocalizedTitle, const UnlocalizedString &unlocalizedMessage, Position position) {
|
|
m_message = Message {
|
|
.position=position,
|
|
.unlocalizedTitle=unlocalizedTitle,
|
|
.unlocalizedMessage=unlocalizedMessage,
|
|
.allowSkip=false
|
|
};
|
|
|
|
return *this;
|
|
}
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::allowSkip() {
|
|
if (m_message.has_value()) {
|
|
m_message->allowSkip = true;
|
|
} else {
|
|
m_message = Message {
|
|
.position=Position::Bottom | Position::Right,
|
|
.unlocalizedTitle="",
|
|
.unlocalizedMessage="",
|
|
.allowSkip=true
|
|
};
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::onAppear(std::function<void()> callback) {
|
|
m_onAppear = std::move(callback);
|
|
|
|
return *this;
|
|
}
|
|
|
|
TutorialManager::Tutorial::Step& TutorialManager::Tutorial::Step::onComplete(std::function<void()> callback) {
|
|
m_onComplete = std::move(callback);
|
|
|
|
return *this;
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TutorialManager::Tutorial::Step::isCurrent() const {
|
|
const auto ¤tStep = m_parent->m_currentStep;
|
|
|
|
if (currentStep == m_parent->m_steps.end())
|
|
return false;
|
|
|
|
return &*currentStep == this;
|
|
}
|
|
|
|
void TutorialManager::Tutorial::Step::complete() const {
|
|
if (this->isCurrent()) {
|
|
this->advance();
|
|
|
|
if (m_onComplete) {
|
|
TaskManager::doLater([this] {
|
|
m_onComplete();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
} |