1
0
Fork 0
forked from hanez/xtc
xtc/src/gui/main_window.cpp

240 lines
9.4 KiB
C++
Raw Normal View History

2026-10-26 22:51:25 +01:00
#include "main_window.hpp"
#include "xtc/xtc_version.h"
2026-10-26 22:51:25 +01:00
#include <FL/Fl.H>
2026-10-26 22:51:25 +01:00
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
2026-10-26 22:51:25 +01:00
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Tabs.H>
#include <FL/fl_ask.H>
#include <cstdio>
#include <cstring>
2026-10-26 22:51:25 +01:00
namespace {
void set_label(Fl_Box *box, const char *text)
{
if (box->label() == nullptr || std::strcmp(box->label(), text) != 0)
box->copy_label(text);
}
2026-10-26 22:51:25 +01:00
}
MainWindow::MainWindow()
: Fl_Double_Window(1000, 680, XTC_PROJECT_NAME " — modular audio workstation")
2026-10-26 22:51:25 +01:00
{
begin();
auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
menu->add("File/New (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
menu->add("File/Open (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
menu->add("File/Save (not implemented)", 0, nullptr, nullptr, FL_MENU_INACTIVE);
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
menu->add("Audio/Start engine", FL_F + 5, play_callback, this);
menu->add("Audio/Stop engine", FL_F + 8, stop_callback, this);
menu->add("Audio/Reconnect", FL_F + 6, reconnect_callback, this);
2026-10-26 22:51:25 +01:00
play_ = new Fl_Button(12, 40, 80, 30, "Play");
play_->callback(play_callback, this);
play_->tooltip("Start the audio engine (F5 or Space). No sequencer content yet.");
stop_ = new Fl_Button(100, 40, 80, 30, "Stop");
stop_->callback(stop_callback, this);
stop_->tooltip("Stop the audio engine (F8 or Space).");
auto *reconnect = new Fl_Button(188, 40, 110, 30, "Reconnect");
reconnect->callback(reconnect_callback, this);
reconnect->tooltip("Close and reopen JACK, leaving the engine stopped (F6).");
indicator_ = new Fl_Box(316, 40, 672, 30, "Audio: disconnected");
indicator_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
indicator_->labelfont(FL_BOLD);
auto *hint = new Fl_Box(12, 78, 976, 24,
"Engine controls only. Playback content and recording are not implemented; no ports are connected automatically.");
hint->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
hint->labelsize(12);
2026-10-26 22:51:25 +01:00
tabs_ = new Fl_Tabs(8, 114, 984, 470);
tabs_->begin();
auto *sequencer = new Fl_Group(8, 140, 984, 444, "Sequencer");
2026-10-26 22:51:25 +01:00
sequencer->begin();
new Fl_Box(24, 164, 940, 40, "Sequencer placeholder — timeline and clips are not implemented yet.");
2026-10-26 22:51:25 +01:00
sequencer->end();
auto *mixer = new Fl_Group(8, 140, 984, 444, "Mixer");
2026-10-26 22:51:25 +01:00
mixer->begin();
new Fl_Box(24, 164, 940, 40, "Mixer placeholder — channels and meters are not implemented yet.");
2026-10-26 22:51:25 +01:00
mixer->end();
auto *graph = new Fl_Group(8, 140, 984, 444, "Modular Graph");
2026-10-26 22:51:25 +01:00
graph->begin();
new Fl_Box(24, 164, 940, 40, "Modular graph placeholder — visual graph editing is not implemented yet.");
2026-10-26 22:51:25 +01:00
graph->end();
tabs_->end();
tabs_->value(sequencer);
menu->add("View/Sequencer", FL_CTRL + '1', view_callback, sequencer);
menu->add("View/Mixer", FL_CTRL + '2', view_callback, mixer);
menu->add("View/Modular Graph", FL_CTRL + '3', view_callback, graph);
menu->add("Help/About", 0, about_callback);
2026-10-26 22:51:25 +01:00
status_ = new Fl_Box(12, 594, 976, 26, "JACK: not connected");
status_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
status_->labelsize(12);
error_ = new Fl_Box(12, 624, 976, 44, "");
error_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
error_->labelcolor(fl_rgb_color(255, 155, 120));
error_->labelsize(12);
2026-10-26 22:51:25 +01:00
end();
resizable(tabs_);
size_range(800, 540);
callback(close_callback, this);
open_backend();
Fl::add_timeout(0.1, timer_callback, this);
2026-10-26 22:51:25 +01:00
}
MainWindow::~MainWindow()
2026-10-26 22:51:25 +01:00
{
Fl::remove_timeout(timer_callback, this);
// Normal window/menu close already synchronized callbacks. This fallback
// covers exceptional event-loop exit. A failed close must retain its C
// callback context until process exit, rather than free active audio state.
if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK)
std::fputs("Failed to close JACK during GUI destruction; backend retained until process exit.\n", stderr);
}
void MainWindow::report_error(const char *operation, xtc_result result)
{
char text[384];
const char *hint = result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support." :
"Check the JACK-compatible server and use Reconnect to retry.";
std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint);
set_label(error_, text);
}
void MainWindow::open_backend()
{
xtc_backend_config config = {XTC_BACKEND_JACK, XTC_PROJECT_NAME, 65536U, 1U};
xtc_result result = xtc_backend_open(&config, &backend_);
ready_ = result == XTC_OK;
running_ = false;
if (result != XTC_OK) report_error("Open JACK failed", result);
else set_label(error_, "");
refresh_status();
}
void MainWindow::start_engine()
{
if (!ready_) {
report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE);
return;
}
if (running_) return;
xtc_result result = xtc_backend_start(backend_);
if (result != XTC_OK) report_error("Start engine failed", result);
else set_label(error_, "");
refresh_status();
}
void MainWindow::stop_engine()
{
if (!ready_) return;
xtc_result result = xtc_backend_stop(backend_);
if (result != XTC_OK) report_error("Stop engine failed", result);
else set_label(error_, "");
refresh_status();
}
void MainWindow::reconnect_engine()
{
xtc_result result = xtc_backend_close(backend_);
if (result != XTC_OK) {
report_error("Reconnect could not close JACK", result);
return; // Preserve the existing handle for a later retry.
}
backend_ = nullptr;
ready_ = false;
running_ = false;
open_backend();
}
void MainWindow::refresh_status()
{
bool can_play = false;
if (!ready_) {
set_label(indicator_, "Audio: disconnected");
set_label(status_, "JACK: not connected — use Reconnect after starting the server");
indicator_->labelcolor(fl_rgb_color(255, 155, 120));
} else {
xtc_backend_status status = {};
xtc_result result = xtc_backend_get_status(backend_, &status);
if (result != XTC_OK) {
report_error("Read engine status failed", result);
set_label(indicator_, "Audio: status unavailable");
set_label(status_, "Unable to read JACK status — use Reconnect");
indicator_->labelcolor(fl_rgb_color(255, 155, 120));
} else {
running_ = status.running != 0;
const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK;
can_play = !running_ && !fault;
const char *state = status.server_lost ? "Audio: server lost — Reconnect required" :
status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" :
status.process_result != XTC_OK ? "Audio: processing error" :
running_ ? "Audio: running (silence — no playback content)" : "Audio: ready / stopped";
set_label(indicator_, state);
indicator_->labelcolor(fault ? fl_rgb_color(255, 155, 120) :
running_ ? fl_rgb_color(125, 225, 160) : fl_rgb_color(215, 215, 215));
char text[256];
std::snprintf(text, sizeof(text), "JACK | %u Hz | %u frames | mono | %u blocks | %u xruns | %s",
status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns,
xtc_result_string(status.process_result));
set_label(status_, text);
}
xtc_backend_collect(backend_);
}
if (can_play) play_->activate(); else play_->deactivate();
if (ready_ && running_) stop_->activate(); else stop_->deactivate();
}
bool MainWindow::close_window()
{
xtc_result result = xtc_backend_close(backend_);
if (result != XTC_OK) {
report_error("Close JACK failed; window kept open for retry", result);
return false;
}
backend_ = nullptr;
ready_ = false;
running_ = false;
closing_ = true;
Fl::remove_timeout(timer_callback, this);
hide();
return true;
}
int MainWindow::handle(int event)
{
if (event == FL_SHORTCUT && Fl::event_key() == ' ' &&
(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) {
if (running_) stop_engine(); else start_engine();
return 1;
}
return Fl_Double_Window::handle(event);
}
void MainWindow::play_callback(Fl_Widget *, void *context)
{ static_cast<MainWindow *>(context)->start_engine(); }
void MainWindow::stop_callback(Fl_Widget *, void *context)
{ static_cast<MainWindow *>(context)->stop_engine(); }
void MainWindow::reconnect_callback(Fl_Widget *, void *context)
{ static_cast<MainWindow *>(context)->reconnect_engine(); }
void MainWindow::close_callback(Fl_Widget *, void *context)
{ (void)static_cast<MainWindow *>(context)->close_window(); }
void MainWindow::view_callback(Fl_Widget *widget, void *view)
{
auto *window = static_cast<MainWindow *>(widget->window());
window->tabs_->value(static_cast<Fl_Widget *>(view));
window->tabs_->redraw();
}
void MainWindow::about_callback(Fl_Widget *, void *)
{
fl_message("%s %s\nModular audio workstation\nPhase 3: JACK engine controls; editors remain placeholders.",
XTC_PROJECT_NAME, XTC_VERSION_STRING);
}
void MainWindow::timer_callback(void *context)
{
auto *window = static_cast<MainWindow *>(context);
window->refresh_status();
if (!window->closing_) Fl::repeat_timeout(0.1, timer_callback, context);
2026-10-26 22:51:25 +01:00
}