Line data Source code
1 : #include "gui/gui_manager.h"
2 : #include "gui/pedalboard/pedal_board.h"
3 : #include "gui/dialogs/file_dialog.h"
4 : #include "gui/theme/theme.h"
5 : #include "preset_manager.h"
6 : #include "audio/effects/tuner.h"
7 : #include <imgui.h>
8 : #include <SDL2/SDL.h>
9 : #include <cstdio>
10 : #include <string>
11 : #ifdef __APPLE__
12 : #include <TargetConditionals.h>
13 : #endif
14 : #ifdef __EMSCRIPTEN__
15 : #include <emscripten.h>
16 : #endif
17 : #if defined(_WIN32)
18 : #include <windows.h>
19 : #include <shellapi.h>
20 : #elif defined(__APPLE__) && !TARGET_OS_IOS
21 : #include <unistd.h>
22 : #include <sys/wait.h>
23 : #include <fcntl.h>
24 : #elif defined(__linux__)
25 : #include <unistd.h>
26 : #include <sys/wait.h>
27 : #include <fcntl.h>
28 : #endif
29 :
30 : namespace Amplitron {
31 :
32 : // Safe URL opener that avoids shell injection
33 0 : static void open_url_safe(const std::string& url) {
34 : #if defined(_WIN32)
35 0 : ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
36 : #elif defined(__APPLE__) && !TARGET_OS_IOS
37 : // Use fork+exec to invoke open without shell
38 : int pipefd[2];
39 0 : if (pipe(pipefd) != 0) return;
40 0 : pid_t pid = fork();
41 0 : if (pid < 0) {
42 0 : close(pipefd[0]);
43 0 : close(pipefd[1]);
44 0 : return;
45 : }
46 0 : if (pid == 0) {
47 0 : close(pipefd[0]);
48 0 : close(pipefd[1]);
49 0 : int devnull = open("/dev/null", O_WRONLY);
50 0 : if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); }
51 0 : execl("/usr/bin/open", "open", url.c_str(), nullptr);
52 0 : _exit(1);
53 : }
54 0 : close(pipefd[0]);
55 0 : close(pipefd[1]);
56 0 : int status = 0;
57 0 : waitpid(pid, &status, 0);
58 : #elif defined(__linux__)
59 : // Use fork+exec to invoke xdg-open without shell
60 : int pipefd[2];
61 0 : if (pipe(pipefd) != 0) return;
62 0 : pid_t pid = fork();
63 0 : if (pid < 0) {
64 0 : close(pipefd[0]);
65 0 : close(pipefd[1]);
66 0 : return;
67 : }
68 0 : if (pid == 0) {
69 0 : close(pipefd[0]);
70 0 : close(pipefd[1]);
71 0 : int devnull = open("/dev/null", O_WRONLY);
72 0 : if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); }
73 0 : execl("/usr/bin/xdg-open", "xdg-open", url.c_str(), nullptr);
74 0 : _exit(1);
75 : }
76 0 : close(pipefd[0]);
77 0 : close(pipefd[1]);
78 0 : int status = 0;
79 0 : waitpid(pid, &status, 0);
80 : #endif
81 0 : }
82 :
83 6 : void GuiManager::render_menu_bar() {
84 6 : if (ImGui::BeginMainMenuBar()) {
85 6 : if (ImGui::BeginMenu("File")) {
86 0 : if (ImGui::MenuItem("New Preset...")) {
87 0 : gui_presets_.begin_new_preset();
88 0 : show_save_preset_ = true;
89 0 : }
90 0 : if (ImGui::MenuItem("Save Preset...", "Ctrl+S")) {
91 0 : gui_presets_.begin_save_preset();
92 0 : show_save_preset_ = true;
93 0 : }
94 0 : if (ImGui::MenuItem("Load Preset...", "Ctrl+O")) {
95 0 : show_load_preset_ = true;
96 0 : gui_presets_.ensure_factory_presets();
97 0 : gui_presets_.refresh_presets(true);
98 0 : }
99 0 : bool has_selected_preset = gui_presets_.selected_preset_index() >= 0 &&
100 0 : gui_presets_.selected_preset_index() < gui_presets_.preset_count();
101 0 : if (ImGui::MenuItem("Delete Selected Preset", nullptr, false, has_selected_preset)) {
102 0 : ImGui::OpenPopup("Confirm Delete Preset");
103 0 : }
104 :
105 0 : if (ImGui::BeginPopupModal("Confirm Delete Preset", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
106 0 : ImGui::Text("Are you sure you want to delete the selected preset?\nThis action cannot be undone.");
107 0 : ImGui::Separator();
108 0 : if (ImGui::Button("Delete", ImVec2(120, 0))) {
109 0 : gui_presets_.delete_preset_by_index(gui_presets_.selected_preset_index());
110 0 : ImGui::CloseCurrentPopup();
111 0 : }
112 0 : ImGui::SameLine();
113 0 : if (ImGui::Button("Cancel", ImVec2(120, 0))) {
114 0 : ImGui::CloseCurrentPopup();
115 0 : }
116 0 : ImGui::EndPopup();
117 0 : }
118 0 : if (ImGui::MenuItem("Copy Preset to Clipboard")) {
119 0 : std::string json_string = gui_presets_.serialise_current_preset_to_json();
120 0 : if (!json_string.empty()) {
121 : #ifdef __EMSCRIPTEN__
122 : // Web build — use browser Clipboard API
123 : EM_ASM({
124 : var text = UTF8ToString($0);
125 : navigator.clipboard.writeText(text).then(function() {
126 : // success
127 : }).catch(function(err) {
128 : console.error("Clipboard write failed: ", err);
129 : });
130 : }, json_string.c_str());
131 : #else
132 : // Native build — ImGui clipboard works fine
133 0 : ImGui::SetClipboardText(json_string.c_str());
134 : #endif
135 0 : toast_message_ = "Preset copied to clipboard!";
136 0 : toast_timer_ = 2.0f;
137 0 : } else {
138 0 : toast_message_ = "Failed to copy: empty preset.";
139 0 : toast_timer_ = 2.0f;
140 : }
141 0 : }
142 0 : ImGui::Separator();
143 : #ifndef AMPLITRON_NO_DESKTOP_SHELL
144 0 : if (ImGui::MenuItem("Change Presets Directory...")) {
145 0 : std::string chosen = show_folder_dialog("Select Presets Directory");
146 0 : if (!chosen.empty()) {
147 0 : PresetManager::set_presets_dir(chosen);
148 0 : PresetManager::save_config();
149 0 : gui_presets_.refresh_presets(false);
150 0 : }
151 0 : }
152 0 : if (ImGui::MenuItem("Reset to Default Presets Directory")) {
153 0 : ImGui::OpenPopup("Confirm Reset Presets Dir");
154 0 : }
155 :
156 0 : if (ImGui::BeginPopupModal("Confirm Reset Presets Dir", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
157 0 : ImGui::Text("Reset presets directory to the default internal path?");
158 0 : ImGui::Separator();
159 0 : if (ImGui::Button("Reset", ImVec2(120, 0))) {
160 0 : PresetManager::set_presets_dir("");
161 0 : PresetManager::save_config();
162 0 : gui_presets_.refresh_presets(false);
163 0 : ImGui::CloseCurrentPopup();
164 0 : }
165 0 : ImGui::SameLine();
166 0 : if (ImGui::Button("Cancel", ImVec2(120, 0))) {
167 0 : ImGui::CloseCurrentPopup();
168 0 : }
169 0 : ImGui::EndPopup();
170 0 : }
171 : #endif
172 0 : ImGui::Separator();
173 0 : if (ImGui::MenuItem("Settings")) show_settings_ = true;
174 0 : ImGui::Separator();
175 0 : if (ImGui::MenuItem("Quit", "Alt+F4")) {
176 0 : SDL_Event quit_event;
177 0 : quit_event.type = SDL_QUIT;
178 0 : SDL_PushEvent(&quit_event);
179 0 : }
180 0 : ImGui::EndMenu();
181 0 : }
182 4 : if (ImGui::BeginMenu("Edit")) {
183 0 : bool can_undo = command_history_.can_undo();
184 0 : bool can_redo = command_history_.can_redo();
185 :
186 0 : const char* undo_label = command_history_.undo_description();
187 0 : char undo_buf[64] = "Undo";
188 0 : if (undo_label) snprintf(undo_buf, sizeof(undo_buf), "Undo %s", undo_label);
189 :
190 0 : const char* redo_label = command_history_.redo_description();
191 0 : char redo_buf[64] = "Redo";
192 0 : if (redo_label) snprintf(redo_buf, sizeof(redo_buf), "Redo %s", redo_label);
193 :
194 0 : if (ImGui::MenuItem(undo_buf, "Ctrl+Z", false, can_undo)) {
195 0 : if (command_history_.undo() && pedal_board_) {
196 0 : pedal_board_->rebuild_widgets();
197 0 : }
198 0 : }
199 0 : if (ImGui::MenuItem(redo_buf, "Ctrl+Shift+Z", false, can_redo)) {
200 0 : if (command_history_.redo() && pedal_board_) {
201 0 : pedal_board_->rebuild_widgets();
202 0 : }
203 0 : }
204 0 : ImGui::EndMenu();
205 0 : }
206 4 : if (ImGui::BeginMenu("Audio")) {
207 0 : if (engine_.is_running()) {
208 0 : if (ImGui::MenuItem("Stop Audio", "M")) engine_.stop();
209 0 : }
210 : else {
211 0 : if (ImGui::MenuItem("Start Audio", "M")) {
212 0 : engine_.restart();
213 0 : }
214 : }
215 0 : ImGui::Separator();
216 0 : if (ImGui::MenuItem("Restart Audio")) {
217 0 : engine_.restart();
218 0 : }
219 0 : ImGui::EndMenu();
220 0 : }
221 4 : if (ImGui::BeginMenu("Utilities")) {
222 0 : if (ImGui::MenuItem("Open Tuner", nullptr, show_tuner_)) {
223 0 : set_show_tuner(!show_tuner_);
224 0 : }
225 0 : if (ImGui::MenuItem("MIDI Settings", nullptr, show_midi_)) {
226 0 : show_midi_ = !show_midi_;
227 0 : }
228 0 : ImGui::EndMenu();
229 0 : }
230 :
231 : // Status bar (right-aligned items computed dynamically)
232 6 : float bar_w = ImGui::GetWindowWidth();
233 6 : float padding = 8.0f;
234 :
235 : // Build a vector of status items from right to left (for right-alignment)
236 28 : struct StatusItem {
237 : std::string label;
238 : bool is_clickable = false;
239 : };
240 6 : std::vector<StatusItem> items;
241 :
242 : // Preset status with dirty indicator
243 6 : std::string preset_label = "Preset: " + gui_presets_.current_preset_name();
244 6 : if (gui_presets_.is_dirty()) {
245 0 : preset_label += " *";
246 0 : }
247 6 : items.push_back({preset_label, false});
248 :
249 : // Sample rate (rightmost)
250 2 : char sr_buf[16];
251 6 : snprintf(sr_buf, sizeof(sr_buf), "%dHz", engine_.get_sample_rate());
252 6 : items.push_back({sr_buf, false});
253 :
254 : // Audio status (LIVE/STOPPED)
255 8 : items.push_back({engine_.is_running() ? "LIVE" : "STOPPED", false});
256 :
257 : // Recording indicator if recording
258 6 : if (engine_.recorder().is_recording()) {
259 0 : char rec_dur[32];
260 0 : snprintf(rec_dur, sizeof(rec_dur), "%.1fs", engine_.recorder().get_duration());
261 0 : items.push_back({rec_dur, false});
262 0 : items.push_back({"REC", false});
263 0 : }
264 :
265 : // MIDI status
266 8 : items.push_back({midi_manager_.is_port_open() ? "MIDI" : "MIDI", true});
267 :
268 : // Check for update (leftmost of right-aligned group)
269 6 : bool show_update = false;
270 6 : std::string update_version;
271 6 : std::string update_url;
272 2 : {
273 6 : std::lock_guard<std::mutex> lock(update_mutex_);
274 6 : if (has_new_release_) {
275 3 : show_update = true;
276 3 : update_version = new_release_version_;
277 5 : update_url = new_release_url_;
278 1 : }
279 3 : }
280 :
281 5 : if (show_update) {
282 3 : std::string release_text = "New Release Available: " + update_version;
283 3 : items.push_back({release_text, true}); // Clickable
284 3 : }
285 :
286 : // Measure widths and compute right-aligned positions
287 6 : float x_pos = bar_w - padding;
288 33 : for (auto it = items.rbegin(); it != items.rend(); ++it) {
289 27 : ImVec2 text_size = ImGui::CalcTextSize(it->label.c_str());
290 27 : x_pos -= text_size.x + padding;
291 9 : }
292 :
293 : // Render items from left to right
294 4 : x_pos = bar_w - padding;
295 33 : for (auto it = items.rbegin(); it != items.rend(); ++it) {
296 27 : ImVec2 text_size = ImGui::CalcTextSize(it->label.c_str());
297 27 : x_pos -= text_size.x + padding;
298 :
299 27 : ImGui::SameLine(x_pos);
300 :
301 27 : if (it->label.find("MIDI") == 0) {
302 6 : if (midi_manager_.is_port_open()) {
303 0 : ImGui::TextColored(Theme::Live(), "%s", it->label.c_str());
304 0 : } else {
305 6 : ImGui::TextColored(ImVec4(0.4f, 0.4f, 0.4f, 1.0f), "%s", it->label.c_str());
306 : }
307 6 : if (ImGui::IsItemHovered()) {
308 0 : ImGui::SetTooltip(midi_manager_.is_port_open() ? "MIDI Connected. Click for settings." : "MIDI Disconnected. Click for settings.");
309 0 : ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
310 0 : }
311 6 : if (ImGui::IsItemClicked()) {
312 0 : show_midi_ = !show_midi_;
313 0 : }
314 23 : } else if (it->label == "LIVE") {
315 4 : ImGui::TextColored(Theme::Live(), "%s", it->label.c_str());
316 19 : } else if (it->label == "STOPPED") {
317 2 : ImGui::TextColored(Theme::Stopped(), "%s", it->label.c_str());
318 15 : } else if (it->label == "REC") {
319 0 : float t = static_cast<float>(ImGui::GetTime());
320 0 : ImGui::TextColored(Theme::RecBlink(t), "%s", it->label.c_str());
321 15 : } else if (it->label.find("New Release") == 0) {
322 3 : ImGui::TextColored(Theme::GoldHot(), "%s", it->label.c_str());
323 3 : if (ImGui::IsItemHovered()) {
324 0 : ImGui::SetTooltip("Click to open release in browser");
325 0 : ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
326 0 : }
327 3 : if (ImGui::IsItemClicked()) {
328 0 : open_url_safe(update_url);
329 0 : }
330 1 : } else {
331 12 : ImGui::Text("%s", it->label.c_str());
332 : }
333 9 : }
334 :
335 6 : ImGui::EndMainMenuBar();
336 6 : }
337 :
338 : // Error banner when audio is stopped
339 8 : if (!engine_.is_running()) {
340 2 : ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.35f, 0.08f, 0.08f, 0.95f));
341 2 : ImGui::BeginChild("AudioErrorBanner", ImVec2(0, 36), true);
342 2 : ImGui::TextColored(Theme::Stopped(), "Audio stream is STOPPED.");
343 2 : ImGui::SameLine();
344 2 : if (ImGui::SmallButton("Restart Audio")) {
345 0 : engine_.restart();
346 0 : }
347 2 : ImGui::SameLine();
348 2 : if (ImGui::SmallButton("Settings")) {
349 0 : show_settings_ = true;
350 0 : }
351 2 : std::string err = engine_.get_last_error();
352 2 : if (!err.empty()) {
353 2 : ImGui::SameLine();
354 2 : ImGui::TextColored(Theme::GoldHot(), " %s", err.c_str());
355 0 : }
356 2 : ImGui::EndChild();
357 2 : ImGui::PopStyleColor();
358 2 : }
359 39 : }
360 :
361 : } // namespace Amplitron
|