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 "Coroutine.h"
23#include "DialogManager.h"
24#include "ISSTracker.h"
25#include "Localization.h"
26#include "Logger.h"
27#include "UIRenderer.h"
28#include "UpdateChecker.h"
29#include "WindowManager.h"
30#include "version.h"
31
32#include <GLFW/glfw3.h>
33#include <curl/curl.h>
34#include <imgui.h>
35
36#include <cstdlib> // for std::getenv
37
38#ifdef __APPLE__
39#include <mach-o/dyld.h> // for _NSGetExecutablePath
40#endif
41
42namespace MetaImGUI {
43
44Application::Application() : m_statusMessage("Ready") {}
45
49
51 if (m_initialized) {
52 return true;
53 }
54
55 // Initialize logger first - use appropriate location per platform
56#ifdef __APPLE__
57 // macOS: Use ~/Library/Logs/MetaImGUI/ directory
58 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
59 const char* home = std::getenv("HOME");
60 const std::string logPath =
61 (home != nullptr) ? std::string(home) + "/Library/Logs/MetaImGUI/metaimgui.log" : "metaimgui.log";
62#elif defined(_WIN32)
63 // Windows: Use LOCALAPPDATA directory
64 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
65 const char* localAppData = std::getenv("LOCALAPPDATA");
66 const std::string logPath = (localAppData != nullptr)
67 ? std::string(localAppData) + "\\MetaImGUI\\logs\\metaimgui.log"
68 : "logs\\metaimgui.log";
69#else
70 // Linux: Use ~/.local/share/MetaImGUI/logs/ directory
71 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
72 const char* home = std::getenv("HOME");
73 const std::string logPath =
74 (home != nullptr) ? std::string(home) + "/.local/share/MetaImGUI/logs/metaimgui.log" : "logs/metaimgui.log";
75#endif
77 LOG_INFO("Initializing MetaImGUI v{}", Version::VERSION);
78
79 // Initialize libcurl globally (thread-safe) before any CURL handles are created.
80 // Auto-init via curl_easy_init() is NOT thread-safe when called concurrently.
81 curl_global_init(CURL_GLOBAL_DEFAULT);
82
83 // Load configuration
84 m_configManager = std::make_unique<ConfigManager>();
85 if (m_configManager->Load()) {
86 LOG_INFO("Configuration loaded successfully");
87 } else {
88 LOG_INFO("Using default configuration");
89 }
90
91 // Load translations and set language from config
92 // CRITICAL: translations.json MUST be present and valid
93 // Try multiple locations for translations file (for different package formats)
94 std::vector<std::string> translationPaths = {
95 "resources/translations/translations.json", // Development/local build
96 };
97
98 // Check if running from AppImage (METAIMGUI_APPDIR set by custom AppRun)
99 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
100 const char* appdir = std::getenv("METAIMGUI_APPDIR");
101 if (appdir != nullptr) {
102 const std::string appdir_path =
103 std::string(appdir) + "/usr/share/MetaImGUI/resources/translations/translations.json";
104 translationPaths.insert(translationPaths.begin(), appdir_path); // Try AppImage location first
105 }
106
107#ifdef __APPLE__
108 // macOS bundle resources path - use executable path to find bundle location
109 // When launched from Finder, CWD is not reliable, so we need absolute path
110 char executablePath[1024];
111 uint32_t size = sizeof(executablePath);
112 if (_NSGetExecutablePath(executablePath, &size) == 0) {
113 // executablePath is like: /Applications/MetaImGUI.app/Contents/MacOS/MetaImGUI
114 std::string exePath(executablePath);
115 // Go up to Contents directory and into Resources
116 size_t macosPos = exePath.rfind("/MacOS/");
117 if (macosPos != std::string::npos) {
118 std::string bundleResourcePath =
119 exePath.substr(0, macosPos) + "/Resources/resources/translations/translations.json";
120 translationPaths.insert(translationPaths.begin(), bundleResourcePath);
121 LOG_DEBUG("macOS bundle resource path: {}", bundleResourcePath);
122 }
123 }
124 // Fallback: relative path (for terminal launch from MacOS directory)
125 translationPaths.emplace_back("../Resources/resources/translations/translations.json");
126 translationPaths.emplace_back("MetaImGUI.app/Contents/Resources/resources/translations/translations.json");
127#endif
128
129 // Add system installation paths
130 translationPaths.emplace_back(
131 "../share/MetaImGUI/resources/translations/translations.json"); // Installed (relative to bin)
132 translationPaths.emplace_back(
133 "/usr/share/MetaImGUI/resources/translations/translations.json"); // System-wide install
134 translationPaths.emplace_back(
135 "/usr/local/share/MetaImGUI/resources/translations/translations.json"); // Local install
136
137 bool translationsLoaded = false;
138 for (const auto& path : translationPaths) {
140 translationsLoaded = true;
141 break;
142 }
143 }
144
145 if (!translationsLoaded) {
146 LOG_ERROR("========================================");
147 LOG_ERROR("CRITICAL: Failed to load translations!");
148 LOG_ERROR("UI will show translation keys instead of actual text");
149 LOG_ERROR("Tried the following locations:");
150 for (const auto& path : translationPaths) {
151 LOG_ERROR(" - {}", path);
152 }
153 LOG_ERROR("This is a PACKAGING ERROR - file is missing from bundle");
154 LOG_ERROR("========================================");
155 }
156
157 const std::string language = m_configManager->GetString("language").value_or("en");
159
160 // Create and initialize window manager
161 auto windowSize = m_configManager->GetWindowSize();
162 const int width = windowSize ? windowSize->first : DEFAULT_WIDTH;
163 const int height = windowSize ? windowSize->second : DEFAULT_HEIGHT;
164
165 m_windowManager = std::make_unique<WindowManager>(WINDOW_TITLE, width, height);
166 if (!m_windowManager->Initialize()) {
167 LOG_ERROR("Failed to initialize window manager");
168 return false;
169 }
170 LOG_INFO("Window manager initialized");
171
172 // Set up window callbacks
173 m_windowManager->SetFramebufferSizeCallback(
174 [this](int width, int height) { this->OnFramebufferSizeChanged(width, height); });
175 m_windowManager->SetKeyCallback(
176 [this](int key, int scancode, int action, int mods) { this->OnKeyPressed(key, scancode, action, mods); });
177 m_windowManager->SetWindowCloseCallback([this]() { this->OnWindowCloseRequested(); });
178 m_windowManager->SetContextLossCallback([this]() { return this->OnContextLoss(); });
179
180 // Create and initialize UI renderer
181 m_uiRenderer = std::make_unique<UIRenderer>();
182 if (!m_uiRenderer->Initialize(m_windowManager->GetNativeWindow())) {
183 LOG_ERROR("Failed to initialize UI renderer");
184 return false;
185 }
186 LOG_INFO("UI renderer initialized");
187
188 // Initialize dialog manager
189 m_dialogManager = std::make_unique<DialogManager>();
190 LOG_INFO("Dialog manager initialized");
191
192 // Initialize update checker
193 m_updateChecker = std::make_unique<UpdateChecker>("andynicholson", "MetaImGUI");
194 LOG_INFO("Update checker initialized");
195
196 // Initialize ISS tracker
197 m_issTracker = std::make_unique<ISSTracker>();
198 LOG_INFO("ISS tracker initialized");
199
200 // Wire UI events to handlers. Connection lifetimes are owned by
201 // m_uiConnections, so they auto-disconnect when Application is destroyed.
202 m_uiConnections.push_back(m_uiEvents.exitRequested.Connect([this]() { OnExitRequested(); }));
203 m_uiConnections.push_back(m_uiEvents.toggleDemoWindow.Connect([this]() { OnToggleDemoWindow(); }));
204 m_uiConnections.push_back(m_uiEvents.showDemoWindow.Connect([this]() { m_showDemoWindow = true; }));
205 m_uiConnections.push_back(m_uiEvents.checkUpdatesRequested.Connect([this]() { OnCheckUpdatesRequested(); }));
206 m_uiConnections.push_back(m_uiEvents.showAboutRequested.Connect([this]() { OnShowAboutRequested(); }));
207 m_uiConnections.push_back(m_uiEvents.showInputDialogRequested.Connect([this]() { OnShowInputDialogRequested(); }));
208 m_uiConnections.push_back(m_uiEvents.toggleISSTracker.Connect([this]() { OnToggleISSTracker(); }));
209
210 // Check for updates asynchronously
211 CheckForUpdates();
212
213 m_initialized = true;
214 LOG_INFO("Application initialized successfully");
215 return true;
216}
217
219 while (!ShouldClose()) {
220 ProcessInput();
221 Render();
222 }
223}
224
226 if (!m_initialized) {
227 return;
228 }
229
230 LOG_INFO("Shutting down application...");
231
232 // Save configuration before shutdown
233 if (m_configManager && m_windowManager) {
234 // Save window size
235 int width = 0;
236 int height = 0;
237 m_windowManager->GetWindowSize(width, height);
238 m_configManager->SetWindowSize(width, height);
239 LOG_INFO("Saving window size: {}x{}", width, height);
240
241 // Save current language
242 m_configManager->SetString("language", Localization::Instance().GetCurrentLanguage());
243
244 if (m_configManager->Save()) {
245 LOG_INFO("Configuration saved successfully");
246 }
247 }
248
249 // Shutdown subsystems in reverse order of initialization
250 if (m_issTracker) {
251 m_issTracker->StopTracking();
252 }
253 m_issTracker.reset();
254 m_updateChecker.reset();
255 m_dialogManager.reset();
256 m_uiRenderer.reset();
257 m_windowManager.reset();
258 m_configManager.reset();
259
260 // Clean up libcurl global state (after all CURL users are destroyed)
261 curl_global_cleanup();
262
263 m_initialized = false;
264 LOG_INFO("Application shut down successfully");
265
266 // Shutdown logger last
268}
269
271 return m_windowManager && m_windowManager->ShouldClose();
272}
273
274void Application::ProcessInput() {
275 if (m_windowManager) {
276 m_windowManager->PollEvents();
277 }
278}
279
280void Application::Render() {
281 if (!m_windowManager || !m_uiRenderer) {
282 return;
283 }
284
285 PollAsyncResults();
286
287 // Frame time for FPS readout in the status bar.
288 m_lastFrameTime = ImGui::GetIO().Framerate;
289
290 m_windowManager->BeginFrame();
291 m_uiRenderer->BeginFrame();
292
293 RenderMainViewport();
294 RenderFloatingWindows();
295 RenderDialogs();
296
297 m_uiRenderer->EndFrame();
298 m_windowManager->EndFrame();
299}
300
301void Application::PollAsyncResults() {
302 // Thread-safe handoff: the update worker thread parks its result behind
303 // the mutex and we consume it on the UI thread.
304 const std::lock_guard<std::mutex> lock(m_updateResultMutex);
305 if (!m_pendingUpdateResult) {
306 return;
307 }
308
309 m_updateCheckInProgress = false;
310 m_latestUpdateInfo = std::move(m_pendingUpdateResult);
311
312 // Only surface the notification window when there's something
313 // actionable; rate-limit / network issues only update the status bar.
314 switch (m_latestUpdateInfo->status) {
316 m_showUpdateNotification = true;
317 m_statusMessage = "Update available: v" + m_latestUpdateInfo->latestVersion;
318 LOG_INFO("Update available: v{} (current: v{})", m_latestUpdateInfo->latestVersion,
319 m_latestUpdateInfo->currentVersion);
320 break;
322 m_statusMessage = "Ready";
323 LOG_INFO("No updates available (current version: v{})", m_latestUpdateInfo->currentVersion);
324 break;
326 m_statusMessage = "Update check rate-limited; retry later";
327 break;
329 m_statusMessage = "Update check failed (network)";
330 break;
332 m_statusMessage = "Update check failed (response)";
333 break;
335 m_statusMessage = "Update check cancelled";
336 break;
338 m_statusMessage = "Ready";
339 break;
340 }
341}
342
343void Application::RenderMainViewport() {
344 ImGuiViewport* viewport = ImGui::GetMainViewport();
345 ImGui::SetNextWindowPos(viewport->Pos);
346 ImGui::SetNextWindowSize(viewport->Size);
347
348 constexpr ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
349 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
350 ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoBringToFrontOnFocus;
351
352 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
353 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
354 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
355
356 if (ImGui::Begin("MetaImGUI Main", nullptr, window_flags)) {
357 m_uiRenderer->RenderMenuBar(m_uiEvents, m_showDemoWindow, m_showISSTracker);
358 m_uiRenderer->RenderMainWindow(m_uiEvents);
359 m_uiRenderer->RenderStatusBar(m_statusMessage, m_lastFrameTime, Version::VERSION, m_updateCheckInProgress);
360 }
361 ImGui::End();
362
363 ImGui::PopStyleVar(3);
364}
365
366void Application::RenderFloatingWindows() {
367 if (m_showAboutWindow) {
368 m_uiRenderer->RenderAboutWindow(m_showAboutWindow);
369 }
370 if (m_showDemoWindow) {
371 m_uiRenderer->ShowDemoWindow(m_showDemoWindow);
372 }
373 if (m_showUpdateNotification) {
374 m_uiRenderer->RenderUpdateNotification(m_showUpdateNotification, m_latestUpdateInfo.get());
375 }
376 if (m_showISSTracker) {
377 m_uiRenderer->RenderISSTrackerWindow(m_showISSTracker, m_issTracker.get());
378 }
379}
380
381void Application::RenderDialogs() {
382 if (m_dialogManager) {
383 m_dialogManager->Render();
384 }
385}
386
387// Event Handlers
388
389void Application::OnWindowCloseRequested() {
390 // Intercept window close button - cancel the close and show dialog
391 if (m_windowManager) {
392 m_windowManager->CancelClose();
393 }
394 StartExitFlow();
395}
396
397void Application::OnExitRequested() {
398 // Show exit confirmation dialog instead of closing immediately
399 StartExitFlow();
400}
401
402void Application::StartExitFlow() {
403 if (m_exitDialogActive || !m_dialogManager) {
404 return;
405 }
406 m_exitDialogActive = true;
407
408 auto& loc = Localization::Instance();
409 std::string title = loc.Tr("exit.title");
410 std::string message = loc.Tr("exit.message");
411
412 // Fire-and-forget coroutine: awaits the user, then acts on the answer.
413 // The Task return type's `suspend_never` finals destroy the frame
414 // automatically once the lambda returns.
415 [](Application* self, std::string title, std::string message) -> Task {
416 const bool confirmed = co_await self->m_dialogManager->AwaitConfirmation(std::move(title), std::move(message));
417 self->m_exitDialogActive = false;
418 if (self->m_windowManager) {
419 if (confirmed) {
420 self->m_windowManager->RequestClose();
421 } else {
422 self->m_windowManager->CancelClose();
423 }
424 }
425 }(this, std::move(title), std::move(message));
426}
427
428void Application::OnToggleDemoWindow() {
429 m_showDemoWindow = !m_showDemoWindow;
430}
431
432void Application::OnCheckUpdatesRequested() {
433 CheckForUpdates();
434}
435
436void Application::OnShowAboutRequested() {
437 m_showAboutWindow = true;
438}
439
440void Application::OnShowInputDialogRequested() {
441 if (m_dialogManager) {
442 auto& loc = Localization::Instance();
443 m_dialogManager->ShowInputDialog(loc.Tr("input_dialog.title"), loc.Tr("input_dialog.prompt"), "",
444 [this](const std::string& result) {
445 auto& loc = Localization::Instance();
446 if (!result.empty()) {
447 m_statusMessage = loc.Tr("status.input_received") + " " + result;
448 LOG_INFO("User input: {}", result);
449 } else {
450 m_statusMessage = loc.Tr("status.input_cancelled");
451 }
452 });
453 }
454}
455
456void Application::OnToggleISSTracker() {
457 m_showISSTracker = !m_showISSTracker;
458}
459
460// Input Callbacks
461
462void Application::OnFramebufferSizeChanged([[maybe_unused]] int width, [[maybe_unused]] int height) {
463 // WindowManager handles the actual viewport resize; this hook exists
464 // for app-level reactions like camera adjustments.
465}
466
467void Application::OnKeyPressed(int key, [[maybe_unused]] int scancode, int action, int mods) {
468 if (action == GLFW_PRESS) {
469 switch (key) {
470 case GLFW_KEY_ESCAPE:
471 OnExitRequested();
472 break;
473 case GLFW_KEY_A:
474 if ((mods & GLFW_MOD_CONTROL) != 0) {
475 OnShowAboutRequested();
476 }
477 break;
478 case GLFW_KEY_F9:
479 // DEBUG: Simulate context loss for testing
480 if ((mods & GLFW_MOD_SHIFT) != 0) {
481 LOG_WARNING("DEBUG: User triggered context loss simulation via Shift+F9");
482 if (OnContextLoss()) {
483 m_statusMessage = "DEBUG: Context recovery successful";
484 } else {
485 m_statusMessage = "DEBUG: Context recovery failed";
486 }
487 }
488 break;
489 default:
490 // Ignore other keys
491 break;
492 }
493 }
494}
495
496// Update Checking
497
498void Application::CheckForUpdates() {
499 if (!m_updateChecker || m_updateCheckInProgress) {
500 return;
501 }
502
503 m_updateCheckInProgress = true;
504 m_statusMessage = "Checking for updates...";
505
506 // Check asynchronously
507 m_updateChecker->CheckForUpdatesAsync([this](const UpdateInfo& info) { this->OnUpdateCheckComplete(info); });
508}
509
510void Application::OnUpdateCheckComplete(const UpdateInfo& updateInfo) {
511 // Thread-safe: store result for main thread to consume in Render()
512 // This callback runs on the worker thread, so we must not write to
513 // main-thread state directly. Instead, we store the result under a
514 // mutex and let the main thread pick it up.
515 const std::lock_guard<std::mutex> lock(m_updateResultMutex);
516 m_pendingUpdateResult = std::make_unique<UpdateInfo>(updateInfo);
517}
518
519bool Application::OnContextLoss() {
520 LOG_WARNING("Application handling context loss - attempting to recreate UI renderer");
521
522 // Shutdown UI renderer (this destroys ImGui/ImPlot contexts)
523 if (m_uiRenderer) {
524 m_uiRenderer->Shutdown();
525 }
526
527 // Recreate UI renderer with new contexts
528 if (!m_uiRenderer->Initialize(m_windowManager->GetNativeWindow())) {
529 LOG_ERROR("Failed to reinitialize UI renderer after context loss");
530 m_statusMessage = "ERROR: Failed to recover from context loss";
531 return false;
532 }
533
534 LOG_INFO("UI renderer successfully reinitialized after context loss");
535 m_statusMessage = "Recovered from display context loss";
536 return true;
537}
538
539} // namespace MetaImGUI
#define LOG_INFO(...)
Definition Logger.h:166
#define LOG_DEBUG(...)
Definition Logger.h:165
#define LOG_ERROR(...)
Definition Logger.h:168
#define LOG_WARNING(...)
Definition Logger.h:167
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 Initialize(const std::filesystem::path &logFilePath, LogLevel minLevel=LogLevel::Info)
Definition Logger.cpp:105
static Logger & Instance()
Definition Logger.cpp:100
Connection Connect(Slot slot)
Subscribe a slot.
Definition Signal.h:122
@ Info
Informational messages.
@ UpdateFound
A newer release is available.
@ NetworkError
Network/HTTP failure.
@ RateLimited
GitHub API rate limit hit (HTTP 403)
@ UpToDate
Current version is the latest.
@ Unknown
No check has run, or result not yet available.
@ Cancelled
Stop requested before the check completed.
@ ParseError
Response did not contain a parseable version.
Signal toggleISSTracker
Definition UIEvents.h:40
Signal toggleDemoWindow
Definition UIEvents.h:35
Signal checkUpdatesRequested
Definition UIEvents.h:37
Signal showDemoWindow
Definition UIEvents.h:36
Signal showAboutRequested
Definition UIEvents.h:38
Signal showInputDialogRequested
Definition UIEvents.h:39