mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-28 15:57: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 -->
887 lines
44 KiB
C++
887 lines
44 KiB
C++
#include <algorithm>
|
|
#include <hex.hpp>
|
|
|
|
#include <hex/api/workspace_manager.hpp>
|
|
#include <hex/api/content_registry/settings.hpp>
|
|
#include <hex/api/content_registry/views.hpp>
|
|
#include <hex/api/content_registry/provider.hpp>
|
|
#include <hex/api/content_registry/user_interface.hpp>
|
|
#include <hex/api/localization_manager.hpp>
|
|
#include <hex/api/theme_manager.hpp>
|
|
#include <hex/api/layout_manager.hpp>
|
|
#include <hex/api/achievement_manager.hpp>
|
|
#include <hex/api_urls.hpp>
|
|
|
|
#include <hex/api/events/events_provider.hpp>
|
|
#include <hex/api/events/events_gui.hpp>
|
|
#include <hex/api/events/requests_gui.hpp>
|
|
|
|
#include <hex/ui/view.hpp>
|
|
#include <toasts/toast_notification.hpp>
|
|
|
|
#include <hex/helpers/http_requests.hpp>
|
|
#include <hex/helpers/fs.hpp>
|
|
#include <hex/helpers/logger.hpp>
|
|
#include <hex/helpers/default_paths.hpp>
|
|
|
|
#include <hex/api/project_file_manager.hpp>
|
|
|
|
#include <imgui.h>
|
|
#include <hex/ui/imgui_imhex_extensions.h>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <romfs/romfs.hpp>
|
|
|
|
#include <wolv/io/file.hpp>
|
|
#include <wolv/io/fs.hpp>
|
|
|
|
#include <fonts/vscode_icons.hpp>
|
|
|
|
#include <content/recent.hpp>
|
|
|
|
#include <string>
|
|
#include <random>
|
|
#include <banners/banner_button.hpp>
|
|
#include <banners/banner_icon.hpp>
|
|
#include <fonts/fonts.hpp>
|
|
|
|
namespace hex::plugin::builtin {
|
|
|
|
namespace {
|
|
AutoReset<ImGuiExt::Texture> s_bannerTexture, s_nightlyTexture, s_backdropTexture, s_infoBannerTexture;
|
|
|
|
std::string s_tipOfTheDay;
|
|
|
|
bool s_simplifiedWelcomeScreen = false;
|
|
|
|
class PopupRestoreBackup : public Popup<PopupRestoreBackup> {
|
|
private:
|
|
std::fs::path m_logFilePath;
|
|
bool m_hasAutoBackups;
|
|
std::function<void()> m_restoreCallback;
|
|
std::function<void()> m_deleteCallback;
|
|
bool m_reportError = true;
|
|
public:
|
|
PopupRestoreBackup(std::fs::path logFilePath, bool hasAutoBackups, const std::function<void()> &restoreCallback, const std::function<void()> &deleteCallback)
|
|
: Popup("hex.builtin.popup.safety_backup.title"),
|
|
m_logFilePath(std::move(logFilePath)),
|
|
m_hasAutoBackups(hasAutoBackups),
|
|
m_restoreCallback(restoreCallback),
|
|
m_deleteCallback(deleteCallback) {
|
|
|
|
m_reportError = ContentRegistry::Settings::read<bool>("hex.builtin.setting.general", "hex.builtin.setting.general.upload_crash_logs", true);
|
|
}
|
|
|
|
void drawContent() override {
|
|
ImGui::TextUnformatted("hex.builtin.popup.safety_backup.desc"_lang);
|
|
if (!m_logFilePath.empty()) {
|
|
ImGui::NewLine();
|
|
ImGui::TextUnformatted("hex.builtin.popup.safety_backup.log_file"_lang);
|
|
ImGui::SameLine(0, 2_scaled);
|
|
if (ImGuiExt::Hyperlink(m_logFilePath.filename().string().c_str())) {
|
|
fs::openFolderWithSelectionExternal(m_logFilePath);
|
|
}
|
|
|
|
ImGui::Checkbox("hex.builtin.popup.safety_backup.report_error"_lang, &m_reportError);
|
|
ImGui::NewLine();
|
|
}
|
|
|
|
auto width = ImGui::GetWindowWidth();
|
|
ImGui::SetCursorPosX(width / 9);
|
|
if (ImGui::Button("hex.builtin.popup.safety_backup.restore"_lang, ImVec2(width / 3, 0))) {
|
|
m_restoreCallback();
|
|
m_deleteCallback();
|
|
|
|
if (m_reportError) {
|
|
wolv::io::File logFile(m_logFilePath, wolv::io::File::Mode::Read);
|
|
if (logFile.isValid()) {
|
|
// Read current log file data
|
|
auto data = logFile.readString();
|
|
|
|
// Anonymize the log file
|
|
{
|
|
for (const auto &paths : paths::All) {
|
|
for (auto &folder : paths->all()) {
|
|
auto parent = wolv::util::toUTF8String(folder.parent_path());
|
|
data = wolv::util::replaceStrings(data, parent, "<*****>");
|
|
}
|
|
}
|
|
}
|
|
|
|
TaskManager::createBackgroundTask("hex.builtin.task.uploading_crash", [path = m_logFilePath, data](auto&){
|
|
HttpRequest request("POST", ImHexApiURL + std::string("/crash_upload"));
|
|
request.uploadFile(std::vector<u8>(data.begin(), data.end()), "file", path.filename()).wait();
|
|
});
|
|
}
|
|
}
|
|
|
|
ContentRegistry::Settings::write<int>("hex.builtin.setting.general", "hex.builtin.setting.general.upload_crash_logs", m_reportError);
|
|
|
|
this->close();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::SetCursorPosX(width / 9 * 5);
|
|
if (ImGui::Button("hex.builtin.popup.safety_backup.delete"_lang, ImVec2(width / 3, 0)) || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
|
m_deleteCallback();
|
|
|
|
this->close();
|
|
}
|
|
|
|
if (m_hasAutoBackups) {
|
|
if (ImGui::Button("hex.builtin.popup.safety_backup.show_auto_backups"_lang, ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
|
|
recent::PopupAutoBackups::open();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
class PopupTipOfTheDay : public Popup<PopupTipOfTheDay> {
|
|
public:
|
|
PopupTipOfTheDay() : Popup("hex.builtin.popup.tip_of_the_day.title", true, false) { }
|
|
|
|
void drawContent() override {
|
|
ImGuiExt::Header("hex.builtin.welcome.tip_of_the_day"_lang, true);
|
|
|
|
ImGuiExt::TextFormattedWrapped("{}", s_tipOfTheDay.c_str());
|
|
ImGui::NewLine();
|
|
|
|
static bool dontShowAgain = false;
|
|
if (ImGui::Checkbox("hex.ui.common.dont_show_again"_lang, &dontShowAgain)) {
|
|
ContentRegistry::Settings::write<bool>("hex.builtin.setting.general", "hex.builtin.setting.general.show_tips", !dontShowAgain);
|
|
}
|
|
|
|
ImGui::SameLine((ImGui::GetMainViewport()->Size / 3 - ImGui::CalcTextSize("hex.ui.common.close"_lang) - ImGui::GetStyle().FramePadding).x);
|
|
|
|
if (ImGui::Button("hex.ui.common.close"_lang) || ImGui::IsKeyPressed(ImGuiKey_Escape))
|
|
Popup::close();
|
|
}
|
|
};
|
|
|
|
void loadDefaultLayout() {
|
|
LayoutManager::loadFromString(std::string(romfs::get("layouts/default.hexlyt").string()));
|
|
}
|
|
|
|
bool isAnyViewOpen() {
|
|
const auto &views = ContentRegistry::Views::impl::getEntries();
|
|
return std::ranges::any_of(views,
|
|
[](const auto &entry) {
|
|
return entry.second->getWindowOpenState();
|
|
});
|
|
}
|
|
|
|
void drawTiles(ImDrawList *drawList, ImVec2 position, ImVec2 size, float lineDistance) {
|
|
const auto tileCount = size / lineDistance;
|
|
|
|
struct Segment {
|
|
i32 x, y;
|
|
bool operator==(const Segment&) const = default;
|
|
};
|
|
static std::list<Segment> segments;
|
|
static std::optional<Segment> colTile;
|
|
static ImGuiDir direction;
|
|
static auto rng = std::mt19937(std::random_device{}());
|
|
static bool over = true;
|
|
static i32 overCounter = 0;
|
|
static u32 spaceCount = 0;
|
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Space, false)) {
|
|
spaceCount += 1;
|
|
|
|
if (spaceCount >= 5) {
|
|
spaceCount = 0;
|
|
segments = { { 10, 10 }, { 10, 11 }, { 10, 12 } };
|
|
direction = ImGuiDir_Right;
|
|
colTile.reset();
|
|
over = false;
|
|
}
|
|
}
|
|
|
|
if (over)
|
|
return;
|
|
|
|
ImHexApi::System::unlockFrameRate();
|
|
|
|
const auto drawTile = [&](u32 x, u32 y) {
|
|
drawList->AddRectFilled(
|
|
{
|
|
position.x + (float(x) * lineDistance),
|
|
position.y + (float(y) * lineDistance)
|
|
},
|
|
{
|
|
position.x + (float(x + 1) * lineDistance) - 1_scaled,
|
|
position.y + (float(y + 1) * lineDistance) - 1_scaled
|
|
}, ImGui::GetColorU32(ImGuiCol_Text, 0.1F)
|
|
);
|
|
};
|
|
|
|
if (colTile.has_value()) {
|
|
drawTile(colTile->x, colTile->y);
|
|
}
|
|
|
|
for (const auto &[x, y] : segments) {
|
|
drawTile(x, y);
|
|
}
|
|
|
|
if (overCounter != 0) {
|
|
for (u32 x = 0; x < u32(tileCount.x); x += 1) {
|
|
for (u32 y = 0; y < u32(tileCount.y); y += 1) {
|
|
if ((x + y) % 2 == u32(overCounter % 2))
|
|
drawTile(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
static double lastTick = 0;
|
|
double tick = ImGui::GetTime();
|
|
if (lastTick + 0.2 < tick) {
|
|
Segment nextSegment = segments.front();
|
|
switch (direction) {
|
|
case ImGuiDir_Up: nextSegment.y -= 1; break;
|
|
case ImGuiDir_Down: nextSegment.y += 1; break;
|
|
case ImGuiDir_Left: nextSegment.x -= 1; break;
|
|
case ImGuiDir_Right: nextSegment.x += 1; break;
|
|
default: break;
|
|
}
|
|
|
|
if (overCounter == 0) {
|
|
for (const auto &segment : segments) {
|
|
if (segment == nextSegment) overCounter = 5;
|
|
if (segment.x < 0 || segment.y < 0) overCounter = 5;
|
|
if (segment.x > i32(tileCount.x) || segment.y > i32(tileCount.y)) overCounter = 5;
|
|
}
|
|
|
|
segments.push_front(nextSegment);
|
|
|
|
if (colTile.has_value() && nextSegment != *colTile) {
|
|
segments.pop_back();
|
|
} else {
|
|
colTile = { .x=i32(rng() % u32(tileCount.x)), .y=i32(rng() % u32(tileCount.x)) };
|
|
}
|
|
} else {
|
|
overCounter -= 1;
|
|
if (overCounter <= 0)
|
|
over = true;
|
|
}
|
|
|
|
lastTick = tick;
|
|
}
|
|
|
|
if (ImGui::IsKeyDown(ImGuiKey_UpArrow) && direction != ImGuiDir_Down) direction = ImGuiDir_Up;
|
|
if (ImGui::IsKeyDown(ImGuiKey_DownArrow) && direction != ImGuiDir_Up) direction = ImGuiDir_Down;
|
|
if (ImGui::IsKeyDown(ImGuiKey_LeftArrow) && direction != ImGuiDir_Right) direction = ImGuiDir_Left;
|
|
if (ImGui::IsKeyDown(ImGuiKey_RightArrow) && direction != ImGuiDir_Left) direction = ImGuiDir_Right;
|
|
}
|
|
|
|
void drawWelcomeScreenBackground() {
|
|
const auto position = ImGui::GetWindowPos();
|
|
const auto size = ImGui::GetWindowSize();
|
|
auto drawList = ImGui::GetWindowDrawList();
|
|
|
|
const auto lineDistance = 20_scaled;
|
|
const auto lineColor = ImGui::GetColorU32(ImGuiCol_Text, 0.03F);
|
|
|
|
for (auto x = position.x; x < position.x + size.x + lineDistance; x += lineDistance) {
|
|
drawList->AddLine({ x, position.y }, { x, position.y + size.y }, lineColor);
|
|
}
|
|
for (auto y = position.y; y < position.y + size.y + lineDistance; y += lineDistance) {
|
|
drawList->AddLine({ position.x, y }, { position.x + size.x, y }, lineColor);
|
|
}
|
|
|
|
drawTiles(drawList, position, size, lineDistance);
|
|
}
|
|
|
|
void drawWelcomeScreenContentSimplified() {
|
|
const ImVec2 backdropSize = scaled({ 350, 350 });
|
|
ImGui::SetCursorPos((ImGui::GetContentRegionAvail() - backdropSize) / 2);
|
|
ImGui::Image(*s_backdropTexture, backdropSize);
|
|
|
|
ImGuiExt::TextFormattedCentered("hex.builtin.welcome.drop_file"_lang);
|
|
}
|
|
|
|
void drawWelcomeScreenContentFull() {
|
|
const ImVec2 margin = scaled({ 30, 20 });
|
|
|
|
ImGui::SetCursorPos(margin);
|
|
if (ImGui::BeginTable("Welcome Outer", 1, ImGuiTableFlags_None, ImGui::GetContentRegionAvail() - margin)) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_PopupBg));
|
|
ON_SCOPE_EXIT { ImGui::PopStyleColor(); };
|
|
|
|
const auto availableSpace = ImGui::GetContentRegionAvail();
|
|
if (ImGui::BeginTable("Welcome Left", 1, ImGuiTableFlags_NoBordersInBody, ImVec2(availableSpace.x / 2, 0))) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::Image(*s_bannerTexture, s_bannerTexture->getSize());
|
|
|
|
if (ImHexApi::System::isNightlyBuild()) {
|
|
auto cursor = ImGui::GetCursorPos();
|
|
|
|
ImGui::SameLine(0);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() - 15_scaled);
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5_scaled);
|
|
|
|
ImGui::Image(*s_nightlyTexture, s_nightlyTexture->getSize());
|
|
ImGuiExt::InfoTooltip(fmt::format("{0}\n\n", "hex.builtin.welcome.nightly_build"_lang).c_str());
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
fonts::Default().push(0.75F);
|
|
ImGuiExt::InfoTooltip(fmt::format("{0}@{1}", ImHexApi::System::getCommitBranch(), ImHexApi::System::getCommitHash(true)).c_str());
|
|
fonts::Default().pop();
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::SetCursorPos(cursor);
|
|
}
|
|
|
|
ImGui::NewLine();
|
|
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 6);
|
|
ImGui::TableNextColumn();
|
|
|
|
static bool otherProvidersVisible = false;
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5_scaled);
|
|
|
|
{
|
|
auto startPos = ImGui::GetCursorPos();
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.start"_lang, nullptr, ImVec2(), ImGuiChildFlags_AutoResizeX)) {
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_NEW_FILE, "hex.builtin.welcome.start.create_file"_lang)) {
|
|
auto newProvider = hex::ImHexApi::Provider::createProvider("hex.builtin.provider.mem_file", true);
|
|
if (newProvider != nullptr && newProvider->open().isFailure())
|
|
hex::ImHexApi::Provider::remove(newProvider.get());
|
|
else
|
|
EventProviderOpened::post(newProvider.get());
|
|
}
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_GO_TO_FILE, "hex.builtin.welcome.start.open_file"_lang))
|
|
RequestOpenWindow::post("Open File");
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_NOTEBOOK, "hex.builtin.welcome.start.open_project"_lang))
|
|
RequestOpenWindow::post("Open Project");
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_TELESCOPE, "hex.builtin.welcome.start.open_other"_lang))
|
|
otherProvidersVisible = !otherProvidersVisible;
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
auto endPos = ImGui::GetCursorPos();
|
|
|
|
if (otherProvidersVisible) {
|
|
ImGui::SameLine(0, 2_scaled);
|
|
ImGui::SetCursorPos(ImGui::GetCursorPos() + ImVec2(0, (endPos - startPos).y / 2));
|
|
ImGui::TextUnformatted(ICON_VS_ARROW_RIGHT);
|
|
ImGui::SameLine(0, 2_scaled);
|
|
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.start.open_other"_lang, nullptr, ImVec2(200_scaled, ImGui::GetTextLineHeightWithSpacing() * 5.8), ImGuiChildFlags_AutoResizeX)) {
|
|
for (const auto &[unlocalizedProviderName, icon] : ContentRegistry::Provider::impl::getEntries()) {
|
|
if (ImGuiExt::IconHyperlink(icon, Lang(unlocalizedProviderName))) {
|
|
ImHexApi::Provider::createProvider(unlocalizedProviderName);
|
|
otherProvidersVisible = false;
|
|
}
|
|
}
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
}
|
|
}
|
|
|
|
// Draw recent entries
|
|
ImGui::Dummy({});
|
|
|
|
#if !defined(OS_WEB)
|
|
recent::draw();
|
|
#endif
|
|
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 6);
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 5_scaled);
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.help"_lang, nullptr, ImVec2(), ImGuiChildFlags_AutoResizeX)) {
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_GITHUB, "hex.builtin.welcome.help.repo"_lang)) hex::openWebpage("hex.builtin.welcome.help.repo.link"_lang);
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_ORGANIZATION, "hex.builtin.welcome.help.gethelp"_lang)) hex::openWebpage("hex.builtin.welcome.help.gethelp.link"_lang);
|
|
if (ImGuiExt::IconHyperlink(ICON_VS_COMMENT_DISCUSSION, "hex.builtin.welcome.help.discord"_lang)) hex::openWebpage("hex.builtin.welcome.help.discord.link"_lang);
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::BeginTable("Welcome Right", 1, ImGuiTableFlags_NoBordersInBody, ImVec2(availableSpace.x / 2, 0))) {
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 5);
|
|
ImGui::TableNextColumn();
|
|
|
|
ImGui::NewLine();
|
|
ImGui::NewLine();
|
|
ImGui::NewLine();
|
|
ImGui::NewLine();
|
|
ImGui::NewLine();
|
|
|
|
auto windowPadding = ImGui::GetStyle().WindowPadding.x * 3;
|
|
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.customize"_lang, nullptr, ImVec2(ImGui::GetContentRegionAvail().x - windowPadding, 0), ImGuiChildFlags_AutoResizeX)) {
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.customize.settings.title"_lang, "hex.builtin.welcome.customize.settings.desc"_lang, ICON_VS_SETTINGS_GEAR, ImVec2(ImGui::GetContentRegionAvail().x, 0)))
|
|
RequestOpenWindow::post("Settings");
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 5);
|
|
ImGui::TableNextColumn();
|
|
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.learn"_lang, nullptr, ImVec2(ImGui::GetContentRegionAvail().x - windowPadding, 0), ImGuiChildFlags_AutoResizeX)) {
|
|
const auto size = ImVec2(ImGui::GetContentRegionAvail().x, 0);
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.learn.latest.title"_lang, "hex.builtin.welcome.learn.latest.desc"_lang, ICON_VS_GITHUB, size))
|
|
hex::openWebpage("hex.builtin.welcome.learn.latest.link"_lang);
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.learn.imhex.title"_lang, "hex.builtin.welcome.learn.imhex.desc"_lang, ICON_VS_BOOK, size)) {
|
|
AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.docs.name");
|
|
hex::openWebpage("hex.builtin.welcome.learn.imhex.link"_lang);
|
|
}
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.learn.pattern.title"_lang, "hex.builtin.welcome.learn.pattern.desc"_lang, ICON_VS_SYMBOL_NAMESPACE, size))
|
|
hex::openWebpage("hex.builtin.welcome.learn.pattern.link"_lang);
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.learn.plugins.title"_lang, "hex.builtin.welcome.learn.plugins.desc"_lang, ICON_VS_PLUG, size))
|
|
hex::openWebpage("hex.builtin.welcome.learn.plugins.link"_lang);
|
|
|
|
ImGui::SeparatorEx(ImGuiSeparatorFlags_Horizontal, 3_scaled);
|
|
|
|
if (ImGuiExt::DescriptionButton("hex.builtin.welcome.learn.interactive_tutorial.title"_lang, "hex.builtin.welcome.learn.interactive_tutorial.desc"_lang, ICON_VS_COMPASS, size)) {
|
|
RequestOpenWindow::post("Tutorials");
|
|
}
|
|
if (auto [unlocked, total] = AchievementManager::getProgress(); unlocked != total) {
|
|
if (ImGuiExt::DescriptionButtonProgress("hex.builtin.welcome.learn.achievements.title"_lang, "hex.builtin.welcome.learn.achievements.desc"_lang, ICON_VS_SPARKLE, float(unlocked) / float(total), size)) {
|
|
RequestOpenWindow::post("Achievements");
|
|
}
|
|
}
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
auto extraWelcomeScreenEntries = ContentRegistry::UserInterface::impl::getWelcomeScreenEntries();
|
|
if (!extraWelcomeScreenEntries.empty()) {
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_None, ImGui::GetTextLineHeightWithSpacing() * 5);
|
|
ImGui::TableNextColumn();
|
|
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.various"_lang, nullptr, ImVec2(ImGui::GetContentRegionAvail().x - windowPadding, 0))) {
|
|
for (const auto &callback : extraWelcomeScreenEntries)
|
|
callback();
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
}
|
|
|
|
if (s_infoBannerTexture->isValid()) {
|
|
static bool hovered = false;
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetStyleColorVec4(hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Border));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.info"_lang, nullptr, ImVec2(), ImGuiChildFlags_AutoResizeX)) {
|
|
const auto height = 80_scaled;
|
|
ImGui::Image(*s_infoBannerTexture, ImVec2(height * s_infoBannerTexture->getAspectRatio(), height));
|
|
hovered = ImGui::IsItemHovered();
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
hex::openWebpage(ImHexApiURL + fmt::format("/info/{}/link", hex::toLower(ImHexApi::System::getOSName())));
|
|
}
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
ImGui::SetCursorPos(ImVec2(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x * 2, ImGui::GetStyle().FramePadding.y * 2 - 1));
|
|
if (ImGuiExt::DimmedIconButton(ICON_VS_CLOSE, ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_ToolbarRed))) {
|
|
auto provider = ImHexApi::Provider::createProvider("hex.builtin.provider.null");
|
|
if (provider != nullptr)
|
|
std::ignore = provider->open();
|
|
}
|
|
}
|
|
|
|
void drawWelcomeScreen() {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowShadow, 0x00);
|
|
if (ImGui::Begin("ImHexDockSpace", nullptr, ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
|
if (!ImHexApi::Provider::isValid()) {
|
|
static auto title = []{
|
|
std::array<char, 256> result = {};
|
|
ImFormatString(result.data(), result.size(), "%s/DockSpace_%08X", ImGui::GetCurrentWindowRead()->Name, ImGui::GetID("ImHexMainDock"));
|
|
return result;
|
|
}();
|
|
|
|
if (ImGui::Begin(title.data(), nullptr, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
|
ImGui::Dummy({});
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, scaled({ 10, 10 }));
|
|
|
|
ImGui::SetNextWindowScroll({ 0.0F, -1.0F });
|
|
ImGui::SetNextWindowSize(ImGui::GetContentRegionAvail() + scaled({ 0, 10 }));
|
|
ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos() - ImVec2(0, ImGui::GetStyle().FramePadding.y + 2_scaled));
|
|
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
|
if (ImGui::Begin("Welcome Screen", nullptr, ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) {
|
|
ImGui::BringWindowToDisplayBack(ImGui::GetCurrentWindowRead());
|
|
|
|
if (const auto &fullScreenView = ContentRegistry::Views::impl::getFullScreenView(); fullScreenView != nullptr) {
|
|
fullScreenView->draw();
|
|
} else {
|
|
drawWelcomeScreenBackground();
|
|
|
|
if (s_simplifiedWelcomeScreen)
|
|
drawWelcomeScreenContentSimplified();
|
|
else
|
|
drawWelcomeScreenContentFull();
|
|
|
|
static bool hovered = false;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, hovered ? 1.0F : 0.3F);
|
|
{
|
|
const auto &quickSettings = ContentRegistry::UserInterface::impl::getWelcomeScreenQuickSettingsToggles();
|
|
if (!quickSettings.empty()) {
|
|
const auto padding = ImGui::GetStyle().FramePadding.y;
|
|
const ImVec2 windowSize = { 150_scaled, 2 * ImGui::GetTextLineHeightWithSpacing() + padding + std::ceil(quickSettings.size() / 5.0F) * (ImGui::GetTextLineHeightWithSpacing() + padding) };
|
|
ImGui::SetCursorScreenPos(ImGui::GetWindowPos() + ImGui::GetWindowSize() - windowSize - ImGui::GetStyle().WindowPadding);
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
|
|
if (ImGuiExt::BeginSubWindow("hex.builtin.welcome.header.quick_settings"_lang, nullptr, windowSize, ImGuiChildFlags_AutoResizeY)) {
|
|
u32 id = 0;
|
|
for (auto &[onIcon, offIcon, unlocalizedTooltip, toggleCallback, state] : quickSettings) {
|
|
ImGui::PushID(id + 1);
|
|
if (ImGuiExt::DimmedIconToggle(onIcon.c_str(), offIcon.c_str(), &state)) {
|
|
ContentRegistry::Settings::write<bool>("hex.builtin.settings.quick_settings", unlocalizedTooltip, state);
|
|
}
|
|
if (id % 5 > 0)
|
|
ImGui::SameLine();
|
|
ImGui::SetItemTooltip("%s", Lang(unlocalizedTooltip).get());
|
|
ImGui::PopID();
|
|
id += 1;
|
|
}
|
|
}
|
|
ImGuiExt::EndSubWindow();
|
|
|
|
ImGui::PopStyleColor();
|
|
hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
|
}
|
|
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
ImGui::End();
|
|
ImGui::BringWindowToDisplayBack(ImGui::GetCurrentWindowRead());
|
|
}
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
/**
|
|
* @brief Draw some default background if there are no views available in the current layout
|
|
*/
|
|
void drawNoViewsBackground() {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowShadow, 0x00);
|
|
if (ImGui::Begin("ImHexDockSpace", nullptr, ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
|
static std::array<char, 256> title;
|
|
ImFormatString(title.data(), title.size(), "%s/DockSpace_%08X", ImGui::GetCurrentWindowRead()->Name, ImGui::GetID("ImHexMainDock"));
|
|
if (ImGui::Begin(title.data())) {
|
|
ImGui::Dummy({});
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, scaled({ 10, 10 }));
|
|
|
|
ImGui::SetNextWindowScroll({ 0.0F, -1.0F });
|
|
ImGui::SetNextWindowSize(ImGui::GetContentRegionAvail() + scaled({ 0, 10 }));
|
|
ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos() - ImVec2(0, ImGui::GetStyle().FramePadding.y + 2_scaled));
|
|
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
|
if (ImGui::Begin("Welcome Screen", nullptr, ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) {
|
|
auto imageSize = scaled(ImVec2(350, 350));
|
|
auto imagePos = (ImGui::GetContentRegionAvail() - imageSize) / 2;
|
|
|
|
ImGui::SetCursorPos(imagePos);
|
|
ImGui::Image(*s_backdropTexture, imageSize);
|
|
|
|
auto loadDefaultText = "hex.builtin.layouts.none.restore_default"_lang;
|
|
auto textSize = ImGui::CalcTextSize(loadDefaultText);
|
|
|
|
auto textPos = ImVec2(
|
|
(ImGui::GetContentRegionAvail().x - textSize.x) / 2,
|
|
imagePos.y + imageSize.y
|
|
);
|
|
|
|
ImGui::SetCursorPos(textPos);
|
|
if (ImGuiExt::DimmedButton(loadDefaultText)){
|
|
loadDefaultLayout();
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
ImGui::End();
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Registers the event handlers related to the welcome screen
|
|
* should only be called once, at startup
|
|
*/
|
|
void createWelcomeScreen() {
|
|
recent::registerEventHandlers();
|
|
recent::updateRecentEntries();
|
|
|
|
EventFrameBegin::subscribe(drawWelcomeScreen);
|
|
|
|
// Sets a background when they are no views
|
|
EventFrameBegin::subscribe([]{
|
|
if (ImHexApi::Provider::isValid() && !isAnyViewOpen())
|
|
drawNoViewsBackground();
|
|
});
|
|
|
|
ContentRegistry::Settings::onChange("hex.builtin.setting.interface", "hex.builtin.setting.interface.color", [](const ContentRegistry::Settings::SettingsValue &value) {
|
|
auto theme = value.get<std::string>("Dark");
|
|
if (theme != ThemeManager::NativeTheme) {
|
|
static std::string lastTheme;
|
|
|
|
if (theme != lastTheme) {
|
|
RequestChangeTheme::post(theme);
|
|
lastTheme = theme;
|
|
}
|
|
}
|
|
});
|
|
ContentRegistry::Settings::onChange("hex.builtin.setting.interface", "hex.builtin.setting.interface.simplified_welcome_screen", [](const ContentRegistry::Settings::SettingsValue &value) {
|
|
s_simplifiedWelcomeScreen = value.get<bool>(false);
|
|
});
|
|
ContentRegistry::Settings::onChange("hex.builtin.setting.interface", "hex.builtin.setting.interface.language", [](const ContentRegistry::Settings::SettingsValue &value) {
|
|
auto language = value.get<std::string>("en-US");
|
|
if (language != LocalizationManager::getSelectedLanguageId())
|
|
LocalizationManager::setLanguage(language);
|
|
});
|
|
ContentRegistry::Settings::onChange("hex.builtin.setting.interface", "hex.builtin.setting.interface.fps", [](const ContentRegistry::Settings::SettingsValue &value) {
|
|
ImHexApi::System::setTargetFPS(static_cast<float>(value.get<int>(14)));
|
|
});
|
|
|
|
|
|
ContentRegistry::UserInterface::addWelcomeScreenQuickSettingsToggle(ICON_VS_COMPASS_ACTIVE, ICON_VS_COMPASS, "hex.builtin.welcome.quick_settings.simplified", false, [](bool state) {
|
|
s_simplifiedWelcomeScreen = state;
|
|
WorkspaceManager::switchWorkspace(s_simplifiedWelcomeScreen ? "Minimal" : "Default");
|
|
});
|
|
|
|
EventImHexStartupFinished::subscribe([]() {
|
|
for (const auto &quickSetting : ContentRegistry::UserInterface::impl::getWelcomeScreenQuickSettingsToggles()) {
|
|
auto &setting = quickSetting.unlocalizedTooltip;
|
|
ContentRegistry::Settings::onChange("hex.builtin.settings.quick_settings", setting, [setting](const ContentRegistry::Settings::SettingsValue &value) {
|
|
for (auto &[onIcon, offIcon, unlocalizedTooltip, toggleCallback, state] : ContentRegistry::UserInterface::impl::getWelcomeScreenQuickSettingsToggles()) {
|
|
if (unlocalizedTooltip == setting) {
|
|
state = value.get<bool>(state);
|
|
toggleCallback(state);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
bool state = ContentRegistry::Settings::read<bool>("hex.builtin.settings.quick_settings", quickSetting.unlocalizedTooltip, quickSetting.state);
|
|
quickSetting.state = state;
|
|
quickSetting.callback(state);
|
|
}
|
|
});
|
|
|
|
static auto updateTextures = [](float scale) {
|
|
auto changeTexture = [&](const std::string &path) {
|
|
return ImGuiExt::Texture::fromImage(romfs::get(path).span(), ImGuiExt::Texture::Filter::Nearest);
|
|
};
|
|
|
|
auto changeTextureSvg = [&](const std::string &path, float width) {
|
|
return ImGuiExt::Texture::fromSVG(romfs::get(path).span(), width, 0, ImGuiExt::Texture::Filter::Linear);
|
|
};
|
|
|
|
s_bannerTexture = changeTextureSvg(fmt::format("assets/{}/banner.svg", ThemeManager::getImageTheme()), 300 * scale);
|
|
s_nightlyTexture = changeTextureSvg(fmt::format("assets/{}/nightly.svg", "common"), 35 * scale);
|
|
s_backdropTexture = changeTexture(fmt::format("assets/{}/backdrop.png", ThemeManager::getImageTheme()));
|
|
};
|
|
|
|
RequestChangeTheme::subscribe([]() { updateTextures(ImHexApi::System::getGlobalScale()); });
|
|
EventDPIChanged::subscribe([](float oldScale, float newScale) {
|
|
if (oldScale == newScale)
|
|
return;
|
|
|
|
updateTextures(newScale);
|
|
});
|
|
|
|
// Clear project context if we go back to the welcome screen
|
|
EventProviderChanged::subscribe([](const hex::prv::Provider *oldProvider, const hex::prv::Provider *newProvider) {
|
|
std::ignore = oldProvider;
|
|
if (newProvider == nullptr) {
|
|
ProjectFile::clearPath();
|
|
RequestUpdateWindowTitle::post();
|
|
}
|
|
});
|
|
|
|
|
|
recent::addMenuItems();
|
|
|
|
// Check for crash backup
|
|
constexpr static auto CrashFileName = "crash.json";
|
|
constexpr static auto BackupFileName = "crash_backup.hexproj";
|
|
bool hasCrashed = false;
|
|
|
|
for (const auto &path : paths::Config.read()) {
|
|
if (auto crashFilePath = std::fs::path(path) / CrashFileName; wolv::io::fs::exists(crashFilePath)) {
|
|
hasCrashed = true;
|
|
|
|
log::info("Found crash.json file at {}", wolv::util::toUTF8String(crashFilePath));
|
|
wolv::io::File crashFile(crashFilePath, wolv::io::File::Mode::Read);
|
|
nlohmann::json crashFileData;
|
|
try {
|
|
crashFileData = nlohmann::json::parse(crashFile.readString());
|
|
} catch (nlohmann::json::exception &e) {
|
|
log::error("Failed to parse crash.json file: {}", e.what());
|
|
crashFile.remove();
|
|
continue;
|
|
}
|
|
|
|
bool hasProject = !crashFileData.value("project", "").empty();
|
|
|
|
auto backupFilePath = path / BackupFileName;
|
|
auto backupFilePathOld = path / BackupFileName;
|
|
backupFilePathOld.replace_extension(".hexproj.old");
|
|
|
|
bool autoBackupsEnabled = ContentRegistry::Settings::read<int>("hex.builtin.setting.general", "hex.builtin.setting.general.backups.auto_backup_time", 0) > 0;
|
|
auto autoBackups = recent::PopupAutoBackups::getAutoBackups();
|
|
bool hasAutoBackups = autoBackupsEnabled && !autoBackups.empty();
|
|
|
|
bool hasBackupFile = wolv::io::fs::exists(backupFilePath);
|
|
|
|
if (!hasProject && !hasBackupFile) {
|
|
log::warn("No project file or backup file found in crash.json file");
|
|
|
|
crashFile.close();
|
|
|
|
// Delete crash.json file
|
|
wolv::io::fs::remove(crashFilePath);
|
|
|
|
// Delete old backup file
|
|
wolv::io::fs::remove(backupFilePathOld);
|
|
|
|
// Try to move current backup file to the old backup location
|
|
if (wolv::io::fs::copyFile(backupFilePath, backupFilePathOld)) {
|
|
wolv::io::fs::remove(backupFilePath);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
PopupRestoreBackup::open(
|
|
// Path of log file
|
|
crashFileData.value("logFile", ""),
|
|
hasAutoBackups,
|
|
|
|
// Restore callback
|
|
[crashFileData, backupFilePath, hasProject, hasBackupFile, hasAutoBackups, autoBackups = std::move(autoBackups)] {
|
|
if (hasBackupFile) {
|
|
if (ProjectFile::load(backupFilePath)) {
|
|
if (hasProject) {
|
|
ProjectFile::setPath(crashFileData["project"].get<std::string>());
|
|
} else if (hasAutoBackups) {
|
|
ProjectFile::setPath(autoBackups.front().path);
|
|
} else {
|
|
ProjectFile::setPath("");
|
|
}
|
|
RequestUpdateWindowTitle::post();
|
|
} else {
|
|
ui::ToastError::open(fmt::format("hex.builtin.popup.error.project.load"_lang, wolv::util::toUTF8String(backupFilePath)));
|
|
}
|
|
} else {
|
|
if (hasProject) {
|
|
ProjectFile::setPath(crashFileData["project"].get<std::string>());
|
|
} else if (hasAutoBackups) {
|
|
ProjectFile::setPath(autoBackups.front().path);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Delete callback (also executed after restore)
|
|
[crashFilePath, backupFilePath] {
|
|
wolv::io::fs::remove(crashFilePath);
|
|
wolv::io::fs::remove(backupFilePath);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
// Tip of the day
|
|
auto tipsData = romfs::get("tips.json");
|
|
if (!hasCrashed && tipsData.valid()) {
|
|
auto tipsCategories = nlohmann::json::parse(tipsData.string());
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
auto daysSinceEpoch = std::chrono::duration_cast<std::chrono::days>(now.time_since_epoch());
|
|
std::mt19937 random(daysSinceEpoch.count());
|
|
|
|
auto chosenCategory = tipsCategories[random()%tipsCategories.size()].at("tips");
|
|
const auto& chosenTip = chosenCategory[random()%chosenCategory.size()];
|
|
s_tipOfTheDay = chosenTip.get<std::string>();
|
|
|
|
bool showTipOfTheDay = ContentRegistry::Settings::read<bool>("hex.builtin.setting.general", "hex.builtin.setting.general.show_tips", false);
|
|
if (showTipOfTheDay)
|
|
PopupTipOfTheDay::open();
|
|
}
|
|
|
|
if (hasCrashed) {
|
|
TaskManager::doLater([]{
|
|
AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.crash.name");
|
|
});
|
|
} else {
|
|
std::random_device rd;
|
|
if (ImHexApi::System::isCorporateEnvironment()) {
|
|
if (rd() % 25 == 0) {
|
|
ui::BannerButton::open(ICON_VS_HEART, "Using ImHex for professional work? Ask your boss to sponsor us and get private E-Mail support and more!", ImColor(0x68, 0xA7, 0x70), "Donate Now!", [] {
|
|
hex::openWebpage("https://imhex.werwolv.net/donate_work");
|
|
});
|
|
}
|
|
} else {
|
|
if (rd() % 75 == 0) {
|
|
ui::BannerButton::open(ICON_VS_HEART, "ImHex needs your help to stay alive! Donate now to fund infrastructure and further development", ImColor(0x68, 0xA7, 0x70), "Donate Now!", [] {
|
|
hex::openWebpage("https://github.com/sponsors/WerWolv");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load info banner texture either locally or from the server
|
|
TaskManager::doLater([] {
|
|
for (const auto &defaultPath : paths::Resources.read()) {
|
|
const auto infoBannerPath = defaultPath / "info_banner.png";
|
|
if (wolv::io::fs::exists(infoBannerPath)) {
|
|
s_infoBannerTexture = ImGuiExt::Texture::fromImage(infoBannerPath, ImGuiExt::Texture::Filter::Linear);
|
|
|
|
if (s_infoBannerTexture->isValid())
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto allowNetworking = ContentRegistry::Settings::read<bool>("hex.builtin.setting.general", "hex.builtin.setting.general.network_interface", false)
|
|
&& ContentRegistry::Settings::read<int>("hex.builtin.setting.general", "hex.builtin.setting.general.server_contact", 0) != 0;
|
|
if (!s_infoBannerTexture->isValid() && allowNetworking) {
|
|
TaskManager::createBackgroundTask("hex.builtin.task.loading_banner", [](auto&) {
|
|
HttpRequest request("GET",
|
|
ImHexApiURL + fmt::format("/info/{}/image", hex::toLower(ImHexApi::System::getOSName())));
|
|
|
|
auto response = request.downloadFile().get();
|
|
|
|
if (response.isSuccess()) {
|
|
const auto &data = response.getData();
|
|
if (!data.empty()) {
|
|
TaskManager::doLater([data] {
|
|
s_infoBannerTexture = ImGuiExt::Texture::fromImage(data.data(), data.size(), ImGuiExt::Texture::Filter::Linear);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|