From 060c01d121155cc706672f1feddb19dde202b9d2 Mon Sep 17 00:00:00 2001 From: Matthias Melcher Date: Sun, 16 Aug 2026 22:33:39 +0200 Subject: [PATCH] Fluid: improve user dialog for new widgets * foundation only * if a window can;t be created, give user a choice to fix the issue instead of just an error --- fluid/Fluid.h | 18 ++++++++ fluid/io/Project_Reader.cxx | 14 +++++- fluid/message.cxx | 87 +++++++++++++++++++++++++++++++++++-- fluid/message.h | 4 +- fluid/nodes/Window_Node.cxx | 28 +++++++++++- 5 files changed, 143 insertions(+), 8 deletions(-) diff --git a/fluid/Fluid.h b/fluid/Fluid.h index 45bd97f77..cd8f1c9f8 100644 --- a/fluid/Fluid.h +++ b/fluid/Fluid.h @@ -28,6 +28,7 @@ #include #include +#include constexpr int BROWSERWIDTH = 300; constexpr int BROWSERHEIGHT = 500; @@ -54,6 +55,23 @@ namespace widget { class App_Menu_Bar; } + +class Exception : public std::runtime_error { +public: + explicit Exception(const std::string& msg) : std::runtime_error(msg) {} +}; + +class UserCanceledException : public Exception { +public: + UserCanceledException() : Exception("Operation cancelled by user") {} +}; + +class ReadException : public Exception { +public: + explicit ReadException(const std::string& msg) : Exception("Read error: " + msg) {} +}; + + /** Indicate the storage location for tools like layout suites and shell macros. \see class Fd_Shell_Command, class Layout_Suite diff --git a/fluid/io/Project_Reader.cxx b/fluid/io/Project_Reader.cxx index a4f8000e5..62533f584 100644 --- a/fluid/io/Project_Reader.cxx +++ b/fluid/io/Project_Reader.cxx @@ -412,7 +412,15 @@ int Project_Reader::read_project(const char *filename, int merge, Strategy strat deselect(); else proj_.reset(); - read_children(Fluid.proj.tree.current, merge, strategy); + + try { + read_children(Fluid.proj.tree.current, merge, strategy); + } catch (const fluid::UserCanceledException&) { + // User chose abort - silently return or log + } catch (const fluid::ReadException& e) { + fluid_alert("Error reading file: %s", e.what()); + } + // clear this Fluid.proj.tree.current = nullptr; // Force menu items to be rebuilt... @@ -448,7 +456,9 @@ int Project_Reader::read_project(const char *filename, int merge, Strategy strat void Project_Reader::read_error(const char *format, ...) { va_list args; va_start(args, format); - fluid_alert(format, args); + if (fluid_choice("Fluid: ERROR reading project file", format, "Abort", "Ignore", nullptr, args) == msg::ABORT) { + throw fluid::UserCanceledException(); + } va_end(args); } diff --git a/fluid/message.cxx b/fluid/message.cxx index 8a1442b6e..0dee3f372 100644 --- a/fluid/message.cxx +++ b/fluid/message.cxx @@ -18,6 +18,11 @@ #include "Fluid.h" #include +#include +#include +#include +#include + #include "../src/flstring.h" #undef min @@ -97,14 +102,18 @@ int fluid_choice(const char *fmt, const char *b0, const char *b1, const char *b2 } int fluid_choice(const char *fmt, const char *b0, const char *b1, const char *b2, va_list ap) { + return fluid_choice("Fluid", fmt, b0, b1, b2, ap); +} + +int fluid_choice(const char *title, const char *fmt, const char *b0, const char *b1, const char *b2, va_list ap) { char buffer[8096]; vsnprintf(buffer, sizeof(buffer), fmt, ap); if (b0 && b1 && b2) { - return fluid::choice("Fluid", buffer, { {b0, b0[0]}, {b1, b1[0]}, {b2, b2[0]} }); + return fluid::choice(title, buffer, { {b0, b0[0]}, {b1, b1[0]}, {b2, b2[0]} }); } else if (b0 && b1) { - return fluid::choice("Fluid", buffer, { {b0, b0[0]}, {b1, b1[0]} }); + return fluid::choice(title, buffer, { {b0, b0[0]}, {b1, b1[0]} }); } else if (b0) { - return fluid::choice("Fluid", buffer, { {b0, b0[0]} }); + return fluid::choice(title, buffer, { {b0, b0[0]} }); } else { return -1; } @@ -220,3 +229,75 @@ int fluid::choice(const std::string &title, const std::string &message, } } +int fluid::big_choice(const std::string &title, const std::string &message, + const std::vector &option) +{ + if (Fluid.console_mode()) return fluid::choice(title, message, option); + struct Dialog { + Fl_Window* win; + Fl_Box* symbol, *msg; + Fl_Button* ok; + Fl_Button* cancel; + Fl_Check_Button* choice[3]; + int result = msg::ESC; + } dlg; + dlg.win = new Fl_Window(400, 400, title.c_str()); + dlg.win->set_modal(); + dlg.win->callback( + [](Fl_Widget* w, void* data) { + Fl_Window* win = (Fl_Window*)w; + Dialog* dlg = (Dialog*)data; + dlg->result = msg::ESC; + win->hide(); + }, &dlg); + dlg.symbol = new Fl_Box(10, 10, 50, 50, "?"); + dlg.symbol->box(FL_THIN_UP_BOX); + dlg.symbol->labelfont(FL_COURIER_BOLD); + dlg.symbol->labelsize(42); + dlg.symbol->labeltype(FL_SHADOW_LABEL); + dlg.symbol->align(FL_ALIGN_CENTER | FL_ALIGN_INSIDE); + dlg.symbol->labelcolor(FL_BLUE); + dlg.symbol->color(FL_WHITE); + fl_font(FL_HELVETICA, 12); + int w = 320, h = 0; + fl_measure(message.c_str(), w, h, 0); + if (h < 50) h = 50; + dlg.msg = new Fl_Box(70, 10, 320, h, message.c_str()); + dlg.msg->box(FL_FLAT_BOX); + dlg.msg->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP); + dlg.win->size(400, 10 + h + 18 + option.size() * 28 + 10 + 25 + 10); + for (size_t i = 0; i < option.size(); ++i) { + dlg.choice[i] = new Fl_Check_Button(70, 10 + h + 18 + i * 28, 320, 25, option[i].label.c_str()); + dlg.choice[i]->type(FL_RADIO_BUTTON); + dlg.choice[i]->callback( + [](Fl_Widget* w, void* data) { + Fl_Window* win = w->window(); + Dialog* dlg = (Dialog*)win->user_data(); + dlg->result = fl_int(data); + dlg->ok->activate(); + }, fl_voidptr(i)); + dlg.choice[i]->shortcut(option[i].key); + } + dlg.ok = new Fl_Return_Button(dlg.win->w()-220, dlg.win->h() - 10 - 25, 100, 25, "OK"); + dlg.ok->deactivate(); + dlg.ok->callback( + [](Fl_Widget* w, void* data) { + w->window()->hide(); + }, &dlg); + dlg.cancel = new Fl_Button(dlg.win->w()-110, dlg.win->h() - 10 - 25, 100, 25, "Cancel"); + dlg.cancel->callback( + [](Fl_Widget* w, void* data) { + Dialog* dlg = (Dialog*)data; + dlg->result = msg::ESC; + w->window()->hide(); + }, &dlg); + dlg.win->show(); + Fl_Window* gg = Fl::grab(); + if (gg) Fl::grab(nullptr); + while (dlg.win->shown()) { + Fl::wait(); + } + delete dlg.win; + if (gg) Fl::grab(gg); + return dlg.result; +} diff --git a/fluid/message.h b/fluid/message.h index f14abfc0e..4806a9de9 100644 --- a/fluid/message.h +++ b/fluid/message.h @@ -31,6 +31,7 @@ extern void fluid_message(const char *fmt, ...); extern void fluid_message(const char *fmt, va_list ap); extern int fluid_choice(const char *fmt, const char *b0, const char *b1, const char *b2, ...); extern int fluid_choice(const char *fmt, const char *b0, const char *b1, const char *b2, va_list ap); +extern int fluid_choice(const char *title, const char *fmt, const char *b0, const char *b1, const char *b2, va_list ap); // fl_input namespace fluid { @@ -50,7 +51,8 @@ void alert(const std::string &title, const std::string &message); int error_choice(const std::string &title, const std::string &message); int choice(const std::string &title, const std::string &message, const std::vector &option); - +int big_choice(const std::string &title, const std::string &message, + const std::vector &option); } // namespace fluid #endif // FLUID_MESSAGE_H diff --git a/fluid/nodes/Window_Node.cxx b/fluid/nodes/Window_Node.cxx index 2038cb791..6786efdd1 100644 --- a/fluid/nodes/Window_Node.cxx +++ b/fluid/nodes/Window_Node.cxx @@ -239,8 +239,32 @@ Node *Window_Node::make(Strategy strategy) { p = p->parent; } if (!p) { - fluid_message("Please select a function"); - return nullptr; + if (strategy.source() == Strategy::FROM_FILE) { + return nullptr; // trigger a file read error + } + int ret = fluid::big_choice( + "Fluid: Window Container Required", + "A Window can only be created inside a Function or a Widget Class container.\n\n" + "Would you like to create a new container or select an existing one?", + { + {"Create a &Function and add the window", 'f'}, + {"Create a &Widget Class and add the window", 'w'}, + {"&Cancel and let me select an existing container", 'c'} + } ); + switch (ret) { + case 0: + p = add_new_widget_from_user("function", Strategy::AFTER_CURRENT, false); + Fluid.proj.tree.current = anchor = p; + strategy.placement(Strategy::AS_LAST_CHILD); + break; + case 1: + p = add_new_widget_from_user("widget_class", Strategy::AFTER_CURRENT, true); + Fluid.proj.tree.current = anchor = p; + strategy.placement(Strategy::AS_LAST_CHILD); + break; + default: + return nullptr; + } } Window_Node *myo = new Window_Node(); if (!this->o) {// template widget