MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
Application.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "Application.h"
20
21#include "ConfigManager.h"
22#include "DialogManager.h"
23#include "ISSTracker.h"
24#include "Localization.h"
25#include "Logger.h"
26#include "UIRenderer.h"
27#include "UpdateChecker.h"
28#include "WindowManager.h"
29#include "version.h"
30
31#include <GLFW/glfw3.h>
32#include <curl/curl.h>
33#include <imgui.h>
34
35#include <cstdlib> // for std::getenv
36
37#ifdef __APPLE__
38#include <mach-o/dyld.h> // for _NSGetExecutablePath
39#endif
40
41namespace MetaImGUI {
42
43Application::Application() : m_statusMessage("Ready") {}
44
48
50 if (m_initialized) {
51 return true;
52 }
53
54 // Initialize logger first - use appropriate location per platform
55#ifdef __APPLE__
56 // macOS: Use ~/Library/Logs/MetaImGUI/ directory
57 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
58 const char* home = std::getenv("HOME");
59 const std::string logPath =
60 (home != nullptr) ? std::string(home) + "/Library/Logs/MetaImGUI/metaimgui.log" : "metaimgui.log";
61#elif defined(_WIN32)
62 // Windows: Use LOCALAPPDATA directory
63 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
64 const char* localAppData = std::getenv("LOCALAPPDATA");
65 const std::string logPath = (localAppData != nullptr)
66 ? std::string(localAppData) + "\\MetaImGUI\\logs\\metaimgui.log"
67 : "logs\\metaimgui.log";
68#else
69 // Linux: Use ~/.local/share/MetaImGUI/logs/ directory
70 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
71 const char* home = std::getenv("HOME");
72 const std::string logPath =
73 (home != nullptr) ? std::string(home) + "/.local/share/MetaImGUI/logs/metaimgui.log" : "logs/metaimgui.log";
74#endif
76 LOG_INFO("Initializing MetaImGUI v{}", Version::VERSION);
77
78 // Initialize libcurl globally (thread-safe) before any CURL handles are created.
79 // Auto-init via curl_easy_init() is NOT thread-safe when called concurrently.
80 curl_global_init(CURL_GLOBAL_DEFAULT);
81
82 // Load configuration
83 m_configManager = std::make_unique<ConfigManager>();
84 if (m_configManager->Load()) {
85 LOG_INFO("Configuration loaded successfully");
86 } else {
87 LOG_INFO("Using default configuration");
88 }
89
90 // Load translations and set language from config
91 // CRITICAL: translations.json MUST be present and valid
92 // Try multiple locations for translations file (for different package formats)
93 std::vector<std::string> translationPaths = {
94 "resources/translations/translations.json", // Development/local build
95 };
96
97 // Check if running from AppImage (METAIMGUI_APPDIR set by custom AppRun)
98 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
99 const char* appdir = std::getenv("METAIMGUI_APPDIR");
100 if (appdir != nullptr) {
101 const std::string appdir_path =
102 std::string(appdir) + "/usr/share/MetaImGUI/resources/translations/translations.json";
103 translationPaths.insert(translationPaths.begin(), appdir_path); // Try AppImage location first
104 }
105
106#ifdef __APPLE__
107 // macOS bundle resources path - use executable path to find bundle location
108 // When launched from Finder, CWD is not reliable, so we need absolute path
109 char executablePath[1024];
110 uint32_t size = sizeof(executablePath);
111 if (_NSGetExecutablePath(executablePath, &size) == 0) {
112 // executablePath is like: /Applications/MetaImGUI.app/Contents/MacOS/MetaImGUI
113 std::string exePath(executablePath);
114 // Go up to Contents directory and into Resources
115 size_t macosPos = exePath.rfind("/MacOS/");
116 if (macosPos != std::string::npos) {
117 std::string bundleResourcePath =
118 exePath.substr(0, macosPos) + "/Resources/resources/translations/translations.json";
119 translationPaths.insert(translationPaths.begin(), bundleResourcePath);
120 LOG_DEBUG("macOS bundle resource path: {}", bundleResourcePath);
121 }
122 }
123 // Fallback: relative path (for terminal launch from MacOS directory)
124 translationPaths.emplace_back("../Resources/resources/translations/translations.json");
125 translationPaths.emplace_back("MetaImGUI.app/Contents/Resources/resources/translations/translations.json");
126#endif
127
128 // Add system installation paths
129 translationPaths.emplace_back(
130 "../share/MetaImGUI/resources/translations/translations.json"); // Installed (relative to bin)
131 translationPaths.emplace_back(
132 "/usr/share/MetaImGUI/resources/translations/translations.json"); // System-wide install
133 translationPaths.emplace_back(
134 "/usr/local/share/MetaImGUI/resources/translations/translations.json"); // Local install
135
136 bool translationsLoaded = false;
137 for (const auto& path : translationPaths) {
139 translationsLoaded = true;
140 break;
141 }
142 }
143
144 if (!translationsLoaded) {
145 LOG_ERROR("========================================");
146 LOG_ERROR("CRITICAL: Failed to load translations!");
147 LOG_ERROR("UI will show translation keys instead of actual text");
148 LOG_ERROR("Tried the following locations:");
149 for (const auto& path : translationPaths) {
150 LOG_ERROR(" - {}", path);
151 }
152 LOG_ERROR("This is a PACKAGING ERROR - file is missing from bundle");
153 LOG_ERROR("========================================");
154 }
155
156 const std::string language = m_configManager->GetString("language").value_or("en");
158
159 // Create and initialize window manager
160 auto windowSize = m_configManager->GetWindowSize();
161 const int width = windowSize ? windowSize->first : DEFAULT_WIDTH;
162 const int height = windowSize ? windowSize->second : DEFAULT_HEIGHT;
163
164 m_windowManager = std::make_unique<WindowManager>(WINDOW_TITLE, width, height);
165 if (!m_windowManager->Initialize()) {
166 LOG_ERROR("Failed to initialize window manager");
167 return false;
168 }
169 LOG_INFO("Window manager initialized");
170
171 // Set up window callbacks
172 m_windowManager->SetFramebufferSizeCallback(
173 [this](int width, int height) { this->OnFramebufferSizeChanged(width, height); });
174 m_windowManager->SetKeyCallback(
175 [this](int key, int scancode, int action, int mods) { this->OnKeyPressed(key, scancode, action, mods); });
176 m_windowManager->SetWindowCloseCallback([this]() { this->OnWindowCloseRequested(); });
177 m_windowManager->SetContextLossCallback([this]() { return this->OnContextLoss(); });
178
179 // Create and initialize UI renderer
180 m_uiRenderer = std::make_unique<UIRenderer>();
181 if (!m_uiRenderer->Initialize(m_windowManager->GetNativeWindow())) {
182 LOG_ERROR("Failed to initialize UI renderer");
183 return false;
184 }
185 LOG_INFO("UI renderer initialized");
186
187 // Initialize dialog manager
188 m_dialogManager = std::make_unique<DialogManager>();
189 LOG_INFO("Dialog manager initialized");
190
191 // Initialize update checker
192 m_updateChecker = std::make_unique<UpdateChecker>("andynicholson", "MetaImGUI");
193 LOG_INFO("Update checker initialized");
194
195 // Initialize ISS tracker
196 m_issTracker = std::make_unique<ISSTracker>();
197 LOG_INFO("ISS tracker initialized");
198
199 // Check for updates asynchronously
200 CheckForUpdates();
201
202 m_initialized = true;
203 LOG_INFO("Application initialized successfully");
204 return true;
205}
206
208 while (!ShouldClose()) {
209 ProcessInput();
210 Render();
211 }
212}
213
215 if (!m_initialized) {
216 return;
217 }
218
219 LOG_INFO("Shutting down application...");
220
221 // Save configuration before shutdown
222 if (m_configManager && m_windowManager) {
223 // Save window size
224 int width = 0;
225 int height = 0;
226 m_windowManager->GetWindowSize(width, height);
227 m_configManager->SetWindowSize(width, height);
228 LOG_INFO("Saving window size: {}x{}", width, height);
229
230 // Save current language
231 m_configManager->SetString("language", Localization::Instance().GetCurrentLanguage());
232
233 if (m_configManager->Save()) {
234 LOG_INFO("Configuration saved successfully");
235 }
236 }
237
238 // Shutdown subsystems in reverse order of initialization
239 if (m_issTracker) {
240 m_issTracker->StopTracking();
241 }
242 m_issTracker.reset();
243 m_updateChecker.reset();
244 m_dialogManager.reset();
245 m_uiRenderer.reset();
246 m_windowManager.reset();
247 m_configManager.reset();
248
249 // Clean up libcurl global state (after all CURL users are destroyed)
250 curl_global_cleanup();
251
252 m_initialized = false;
253 LOG_INFO("Application shut down successfully");
254
255 // Shutdown logger last
257}
258
260 return m_windowManager && m_windowManager->ShouldClose();
261}
262
263void Application::ProcessInput() {
264 if (m_windowManager) {
265 m_windowManager->PollEvents();
266 }
267}
268
269void Application::Render() {
270 if (!m_windowManager || !m_uiRenderer) {
271 return;
272 }
273
274 // Consume any pending update result (thread-safe handoff from worker thread)
275 {
276 const std::lock_guard<std::mutex> lock(m_updateResultMutex);
277 if (m_pendingUpdateResult) {
278 m_updateCheckInProgress = false;
279 m_latestUpdateInfo = std::move(m_pendingUpdateResult);
280 m_showUpdateNotification = true;
281
282 if (m_latestUpdateInfo->updateAvailable) {
283 m_statusMessage = "Update available: v" + m_latestUpdateInfo->latestVersion;
284 LOG_INFO("Update available: v{} (current: v{})", m_latestUpdateInfo->latestVersion,
285 m_latestUpdateInfo->currentVersion);
286 } else {
287 m_statusMessage = "Ready";
288 LOG_INFO("No updates available (current version: v{})", m_latestUpdateInfo->currentVersion);
289 }
290 }
291 }
292
293 // Get frame time for FPS calculation
294 const ImGuiIO& io = ImGui::GetIO();
295 m_lastFrameTime = io.Framerate;
296
297 // Prepare window for rendering
298 m_windowManager->BeginFrame();
299
300 // Start ImGui frame
301 m_uiRenderer->BeginFrame();
302
303 // Create full-screen main window
304 ImGuiViewport* viewport = ImGui::GetMainViewport();
305 ImGui::SetNextWindowPos(viewport->Pos);
306 ImGui::SetNextWindowSize(viewport->Size);
307
308 const ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
309 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
310 ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoBringToFrontOnFocus;
311
312 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
313 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
314 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
315
316 if (ImGui::Begin("MetaImGUI Main", nullptr, window_flags)) {
317 // Render menu bar
318 m_uiRenderer->RenderMenuBar([this]() { this->OnExitRequested(); }, [this]() { this->OnToggleDemoWindow(); },
319 [this]() { this->OnCheckUpdatesRequested(); },
320 [this]() { this->OnShowAboutRequested(); }, m_showDemoWindow,
321 [this]() { this->OnToggleISSTracker(); }, m_showISSTracker);
322
323 // Render main window content
324 m_uiRenderer->RenderMainWindow([this]() { this->OnShowAboutRequested(); },
325 [this]() { m_showDemoWindow = true; },
326 [this]() { this->OnShowInputDialogRequested(); });
327
328 // Render status bar
329 m_uiRenderer->RenderStatusBar(m_statusMessage, m_lastFrameTime, Version::VERSION, m_updateCheckInProgress);
330 }
331 ImGui::End();
332
333 ImGui::PopStyleVar(3);
334
335 // Render additional windows
336 if (m_showAboutWindow) {
337 m_uiRenderer->RenderAboutWindow(m_showAboutWindow);
338 }
339
340 if (m_showDemoWindow) {
341 m_uiRenderer->ShowDemoWindow(m_showDemoWindow);
342 }
343
344 if (m_showUpdateNotification) {
345 m_uiRenderer->RenderUpdateNotification(m_showUpdateNotification, m_latestUpdateInfo.get());
346 }
347
348 if (m_showISSTracker) {
349 m_uiRenderer->RenderISSTrackerWindow(m_showISSTracker, m_issTracker.get());
350 }
351
352 // Render exit confirmation dialog
353 // Consume m_showExitDialog immediately so ShowConfirmation() is called
354 // exactly once. The dialog is then managed by DialogManager internally
355 // until the user interacts with it.
356 if (m_showExitDialog) {
357 m_showExitDialog = false;
358
359 auto& loc = Localization::Instance();
360 const std::string title = loc.Tr("exit.title");
361 const std::string message = loc.Tr("exit.message");
362
363 m_dialogManager->ShowConfirmation(title, message, [this](bool confirmed) {
364 if (confirmed && m_windowManager) {
365 m_windowManager->RequestClose();
366 } else if (m_windowManager) {
367 // User cancelled - make sure close flag is cleared
368 m_windowManager->CancelClose();
369 }
370 });
371 }
372
373 // Render dialogs
374 if (m_dialogManager) {
375 m_dialogManager->Render();
376 }
377
378 // End ImGui frame and render
379 m_uiRenderer->EndFrame();
380
381 // Present the frame
382 m_windowManager->EndFrame();
383}
384
385// Event Handlers
386
387void Application::OnWindowCloseRequested() {
388 // Intercept window close button - cancel the close and show dialog
389 if (m_windowManager) {
390 m_windowManager->CancelClose();
391 }
392 m_showExitDialog = true;
393}
394
395void Application::OnExitRequested() {
396 // Show exit confirmation dialog instead of closing immediately
397 m_showExitDialog = true;
398}
399
400void Application::OnToggleDemoWindow() {
401 m_showDemoWindow = !m_showDemoWindow;
402}
403
404void Application::OnCheckUpdatesRequested() {
405 CheckForUpdates();
406}
407
408void Application::OnShowAboutRequested() {
409 m_showAboutWindow = true;
410}
411
412void Application::OnShowInputDialogRequested() {
413 if (m_dialogManager) {
414 auto& loc = Localization::Instance();
415 m_dialogManager->ShowInputDialog(loc.Tr("input_dialog.title"), loc.Tr("input_dialog.prompt"), "",
416 [this](const std::string& result) {
417 auto& loc = Localization::Instance();
418 if (!result.empty()) {
419 m_statusMessage = loc.Tr("status.input_received") + " " + result;
420 LOG_INFO("User input: {}", result);
421 } else {
422 m_statusMessage = loc.Tr("status.input_cancelled");
423 }
424 });
425 }
426}
427
428void Application::OnToggleISSTracker() {
429 m_showISSTracker = !m_showISSTracker;
430}
431
432// Input Callbacks
433
434void Application::OnFramebufferSizeChanged(int width, int height) {
435 // Handle framebuffer size changes if needed
436 // Currently handled automatically by WindowManager
437 (void)width;
438 (void)height;
439}
440
441void Application::OnKeyPressed(int key, int scancode, int action, int mods) {
442 (void)scancode; // Unused parameter
443
444 if (action == GLFW_PRESS) {
445 switch (key) {
446 case GLFW_KEY_ESCAPE:
447 OnExitRequested();
448 break;
449 case GLFW_KEY_A:
450 if ((mods & GLFW_MOD_CONTROL) != 0) {
451 OnShowAboutRequested();
452 }
453 break;
454 case GLFW_KEY_F9:
455 // DEBUG: Simulate context loss for testing
456 if ((mods & GLFW_MOD_SHIFT) != 0) {
457 LOG_WARNING("DEBUG: User triggered context loss simulation via Shift+F9");
458 if (OnContextLoss()) {
459 m_statusMessage = "DEBUG: Context recovery successful";
460 } else {
461 m_statusMessage = "DEBUG: Context recovery failed";
462 }
463 }
464 break;
465 default:
466 // Ignore other keys
467 break;
468 }
469 }
470}
471
472// Update Checking
473
474void Application::CheckForUpdates() {
475 if (!m_updateChecker || m_updateCheckInProgress) {
476 return;
477 }
478
479 m_updateCheckInProgress = true;
480 m_statusMessage = "Checking for updates...";
481
482 // Check asynchronously
483 m_updateChecker->CheckForUpdatesAsync([this](const UpdateInfo& info) { this->OnUpdateCheckComplete(info); });
484}
485
486void Application::OnUpdateCheckComplete(const UpdateInfo& updateInfo) {
487 // Thread-safe: store result for main thread to consume in Render()
488 // This callback runs on the worker thread, so we must not write to
489 // main-thread state directly. Instead, we store the result under a
490 // mutex and let the main thread pick it up.
491 const std::lock_guard<std::mutex> lock(m_updateResultMutex);
492 m_pendingUpdateResult = std::make_unique<UpdateInfo>(updateInfo);
493}
494
495bool Application::OnContextLoss() {
496 LOG_WARNING("Application handling context loss - attempting to recreate UI renderer");
497
498 // Shutdown UI renderer (this destroys ImGui/ImPlot contexts)
499 if (m_uiRenderer) {
500 m_uiRenderer->Shutdown();
501 }
502
503 // Recreate UI renderer with new contexts
504 if (!m_uiRenderer->Initialize(m_windowManager->GetNativeWindow())) {
505 LOG_ERROR("Failed to reinitialize UI renderer after context loss");
506 m_statusMessage = "ERROR: Failed to recover from context loss";
507 return false;
508 }
509
510 LOG_INFO("UI renderer successfully reinitialized after context loss");
511 m_statusMessage = "Recovered from display context loss";
512 return true;
513}
514
515} // namespace MetaImGUI
#define LOG_INFO(...)
Definition Logger.h:218
#define LOG_DEBUG(...)
Definition Logger.h:217
#define LOG_ERROR(...)
Definition Logger.h:220
#define LOG_WARNING(...)
Definition Logger.h:219
void Shutdown()
Shutdown the application and cleanup resources.
bool ShouldClose() const
Check if the application should close.
bool Initialize()
Initialize the application and all subsystems.
void Run()
Run the main application loop.
static Localization & Instance()
Get singleton instance.
void SetLanguage(const std::string &languageCode)
Set current language.
bool LoadTranslations(const std::string &filepath)
Load translations from JSON file.
void Shutdown()
Shutdown logger and flush buffers.
Definition Logger.cpp:72
void Initialize(const std::filesystem::path &logFilePath="", LogLevel minLevel=LogLevel::Info)
Initialize logger with optional log file.
Definition Logger.cpp:39
static Logger & Instance()
Get singleton instance.
Definition Logger.cpp:34
@ Info
Informational messages.