From cb40b5697bb0461128460fd0389e275d33786173 Mon Sep 17 00:00:00 2001 From: michyaraque Date: Sat, 24 Jan 2026 22:51:45 +0100 Subject: [PATCH 01/10] feat: allow sprite creation in map overlay, improve listener prevents --- source/lua/lua_script_manager.cpp | 27 +++++++++++++++++++++++++++ source/lua/lua_script_manager.h | 8 ++++++-- source/map_drawer.cpp | 15 +++++++++++++++ source/map_overlay.h | 3 +++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/source/lua/lua_script_manager.cpp b/source/lua/lua_script_manager.cpp index 7ae90d3b0..17c14e938 100644 --- a/source/lua/lua_script_manager.cpp +++ b/source/lua/lua_script_manager.cpp @@ -18,6 +18,7 @@ #include "main.h" #include "lua_script_manager.h" #include "lua_api.h" +#include "lua_api_image.h" #include "../gui.h" #include "../tile.h" @@ -362,6 +363,32 @@ void LuaScriptManager::collectMapOverlayCommands(const MapViewInfo& view, std::v } }; + ctx["image"] = [&, getOptsTable](sol::variadic_args va) { + sol::table opts = getOptsTable(va); + if (!opts.valid()) { + return; + } + + if (opts["image"].valid() && opts["image"].is()) { + LuaAPI::LuaImage img = opts["image"].get(); + if(img.isSpriteSource()) { + MapOverlayCommand cmd; + cmd.type = MapOverlayCommand::Type::Sprite; + cmd.sprite_id = img.getSpriteId(); + cmd.screen_space = opts.get_or(std::string("screen"), false); + cmd.x = opts.get_or(std::string("x"), 0); + cmd.y = opts.get_or(std::string("y"), 0); + cmd.z = opts.get_or(std::string("z"), view.floor); + + // Opacity handling + double opacity = opts.get_or(std::string("opacity"), 1.0); + cmd.color = wxColor(255, 255, 255, static_cast(opacity * 255)); + + out.push_back(cmd); + } + } + }; + std::vector sorted = mapOverlays; std::sort(sorted.begin(), sorted.end(), [](const MapOverlay& a, const MapOverlay& b) { if (a.order == b.order) { diff --git a/source/lua/lua_script_manager.h b/source/lua/lua_script_manager.h index 41b12ba1e..cbfe11243 100644 --- a/source/lua/lua_script_manager.h +++ b/source/lua/lua_script_manager.h @@ -111,7 +111,9 @@ class LuaScriptManager { return; } - for (auto& listener : eventListeners) { + // Iterate over a copy to allow callbacks to modify the listener list safely + std::vector listenersCopy = eventListeners; + for (const auto& listener : listenersCopy) { if (listener.eventName == eventName && listener.callback.valid()) { try { listener.callback(std::forward(args)...); @@ -128,8 +130,10 @@ class LuaScriptManager { return false; } + // Iterate over a copy to allow callbacks to modify the listener list safely + std::vector listenersCopy = eventListeners; bool consumed = false; - for (auto& listener : eventListeners) { + for (const auto& listener : listenersCopy) { if (listener.eventName == eventName && listener.callback.valid()) { try { sol::object result = listener.callback(std::forward(args)...); diff --git a/source/map_drawer.cpp b/source/map_drawer.cpp index 81cefe737..2213ff538 100644 --- a/source/map_drawer.cpp +++ b/source/map_drawer.cpp @@ -380,6 +380,21 @@ bool MapDrawer::drawOverlayCommands(const std::vector& comman if (cmd.dashed) { glDisable(GL_LINE_STIPPLE); } + } else if (cmd.type == MapOverlayCommand::Type::Sprite) { + if (cmd.sprite_id != 0) { + if (isScreenSpace) { + // Screen space sprite drawing - not implemented fully yet + // Need to setup matrix, etc. + } else { + int screen_x = 0; + int screen_y = 0; + if (mapToScreen(this, cmd.x, cmd.y, cmd.z, screen_x, screen_y)) { + glEnable(GL_TEXTURE_2D); + BlitSpriteType(screen_x, screen_y, cmd.sprite_id, cmd.color.Red(), cmd.color.Green(), cmd.color.Blue(), cmd.color.Alpha()); + glDisable(GL_TEXTURE_2D); + } + } + } } else if (cmd.type == MapOverlayCommand::Type::Text) { if (!cmd.text.empty()) { if (isScreenSpace) { diff --git a/source/map_overlay.h b/source/map_overlay.h index 36510731e..71b542ded 100644 --- a/source/map_overlay.h +++ b/source/map_overlay.h @@ -41,6 +41,7 @@ struct MapOverlayCommand { Rect, Line, Text, + Sprite, }; Type type = Type::Rect; @@ -58,6 +59,8 @@ struct MapOverlayCommand { int y2 = 0; int z2 = 0; + uint32_t sprite_id = 0; + std::string text; wxColor color = wxColor(255, 255, 255, 255); }; From 8b8eead5a227b7c4d8ebaa8cfc9d57e6128ff224 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 24 Jan 2026 21:52:23 +0000 Subject: [PATCH 02/10] Code format - (Clang-format) --- source/lua/lua_script_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/lua/lua_script_manager.cpp b/source/lua/lua_script_manager.cpp index 17c14e938..ca3be8025 100644 --- a/source/lua/lua_script_manager.cpp +++ b/source/lua/lua_script_manager.cpp @@ -371,7 +371,7 @@ void LuaScriptManager::collectMapOverlayCommands(const MapViewInfo& view, std::v if (opts["image"].valid() && opts["image"].is()) { LuaAPI::LuaImage img = opts["image"].get(); - if(img.isSpriteSource()) { + if (img.isSpriteSource()) { MapOverlayCommand cmd; cmd.type = MapOverlayCommand::Type::Sprite; cmd.sprite_id = img.getSpriteId(); From b2762ee05ec6b51e164f47932259d383dcb12933 Mon Sep 17 00:00:00 2001 From: Michael Araque <32944677+michyaraque@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:29:05 +0100 Subject: [PATCH 03/10] refactor: add custom safe dofile implementation in lua_engine Implemented a custom safe dofile function to restrict file access and prevent directory traversal. --- source/lua/lua_engine.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/source/lua/lua_engine.cpp b/source/lua/lua_engine.cpp index 848eff053..fa2e3381d 100644 --- a/source/lua/lua_engine.cpp +++ b/source/lua/lua_engine.cpp @@ -107,9 +107,37 @@ void LuaEngine::setupSandbox() { } } - // Disable accessing arbitrary files - lua["dofile"] = sol::nil; - lua["loadfile"] = sol::nil; + // Custom safe dofile implementation + lua["dofile"] = [this](const std::string& filename, sol::this_state s) -> bool { + sol::state_view lua(s); + + // Get SCRIPT_DIR + sol::object scriptDirObj = lua["SCRIPT_DIR"]; + if (!scriptDirObj.is()) { + throw sol::error("dofile: SCRIPT_DIR not set. Cannot resolve relative path."); + } + std::string scriptDir = scriptDirObj.as(); + + // Reject absolute paths + if (filename.find(":") != std::string::npos || (filename.size() > 0 && (filename[0] == '/' || filename[0] == '\\'))) { + throw sol::error("dofile: Absolute paths are not allowed. Use paths relative to the script."); + } + // Reject directory traversal + if (filename.find("..") != std::string::npos) { + throw sol::error("dofile: Directory traversal ('..') is not allowed."); + } + + // Remove ./ prefix if present + std::string cleanFilename = filename; + if (cleanFilename.substr(0, 2) == "./" || cleanFilename.substr(0, 2) == ".\\") { + cleanFilename = cleanFilename.substr(2); + } + + std::string fullPath = scriptDir + "/" + cleanFilename; + + // We use executeFile which handles loading and error reporting + return this->executeFile(fullPath); + }; // Secure 'load' to prevent bytecode execution (only allow mode "t") // If the chunk starts with the bytecode signature (ESC Lua), load() normally detects it. From d8eb4d8123b630aa6acea6f241d8e0eabf32a334 Mon Sep 17 00:00:00 2001 From: Michael Araque <32944677+michyaraque@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:30:20 +0100 Subject: [PATCH 04/10] feat: allow modify api to modify combobox --- source/lua/lua_dialog.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/source/lua/lua_dialog.cpp b/source/lua/lua_dialog.cpp index 8e9a1bb1b..09fff70e0 100644 --- a/source/lua/lua_dialog.cpp +++ b/source/lua/lua_dialog.cpp @@ -1908,6 +1908,30 @@ LuaDialog* LuaDialog::modify(sol::table options) { if (props["text"].valid()) { ctrl->SetLabel(wxString(props.get_or(std::string("text"), ""s))); } + } else if (widget.type == "combobox") { + wxChoice* ctrl = static_cast(widget.widget); + if (props["options"].valid()) { + ctrl->Freeze(); + ctrl->Clear(); + sol::table opts = props["options"]; + for (size_t i = 1; i <= opts.size(); ++i) { + if (opts[i].valid()) { + ctrl->Append(wxString(opts[i].get())); + } + } + ctrl->Thaw(); + } + if (props["option"].valid()) { + std::string selected = props.get_or(std::string("option"), ""s); + int idx = ctrl->FindString(wxString(selected)); + if (idx != wxNOT_FOUND) { + ctrl->SetSelection(idx); + values[id] = sol::make_object(lua, selected); + } + } else if (ctrl->GetCount() > 0 && ctrl->GetSelection() == wxNOT_FOUND) { + ctrl->SetSelection(0); + values[id] = sol::make_object(lua, ctrl->GetString(0).ToStdString()); + } } else if (widget.type == "list") { LuaDialogListBox* ctrl = static_cast(widget.widget); if (props["icon_width"].valid() || props["icon_height"].valid() || props["icon_size"].valid()) { From eacb61e3647ec192eff9ca6341b8c01d82f17a53 Mon Sep 17 00:00:00 2001 From: michyaraque Date: Wed, 28 Jan 2026 20:06:49 +0100 Subject: [PATCH 05/10] feat: extend dialog to allow panel, include color options, improve button manipulation --- source/lua/lua_api_color.cpp | 118 ++++++---- source/lua/lua_api_color.h | 32 +-- source/lua/lua_dialog.cpp | 428 ++++++++++++++++++++++++++++++----- source/lua/lua_dialog.h | 9 +- 4 files changed, 453 insertions(+), 134 deletions(-) diff --git a/source/lua/lua_api_color.cpp b/source/lua/lua_api_color.cpp index d48a7e298..5d4f02639 100644 --- a/source/lua/lua_api_color.cpp +++ b/source/lua/lua_api_color.cpp @@ -21,60 +21,82 @@ namespace LuaAPI { void registerColor(sol::state& lua) { - // Register LuaColor as "Color" usertype - lua.new_usertype("Color", - // Constructors - sol::constructors(), + sol::table Color = lua.create_table(); - // Alternative constructor from table: Color{red=255, green=0, blue=0} - sol::call_constructor, sol::factories( - // Default constructor - []() { return LuaColor(); }, - // RGB constructor - [](uint8_t r, uint8_t g, uint8_t b) { return LuaColor(r, g, b); }, - // RGBA constructor - [](uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return LuaColor(r, g, b, a); }, - // Table constructor - [](sol::table t) { - LuaColor c; - c.red = t.get_or("red", 0); - c.green = t.get_or("green", 0); - c.blue = t.get_or("blue", 0); - c.alpha = t.get_or("alpha", 255); - return c; - } - ), + // Constructor rgb + Color["rgb"] = [&lua](int r, int g, int b) { + sol::table c = lua.create_table(); + c["r"] = r; + c["g"] = g; + c["b"] = b; + return c; + }; - // Properties (read/write) - "red", &LuaColor::red, "green", &LuaColor::green, "blue", &LuaColor::blue, "alpha", &LuaColor::alpha, + // Constructor hex + Color["hex"] = [&lua](const std::string& hex) { + unsigned long value = 0; + std::string h = hex[0] == '#' ? hex.substr(1) : hex; + if (h.length() == 3) { + h = {h[0], h[0], h[1], h[1], h[2], h[2]}; + } + try { + value = std::stoul(h, nullptr, 16); + } catch (...) { + value = 0; + } - // Shorthand aliases - "r", &LuaColor::red, "g", &LuaColor::green, "b", &LuaColor::blue, "a", &LuaColor::alpha, + sol::table c = lua.create_table(); + c["r"] = (int)((value >> 16) & 0xFF); + c["g"] = (int)((value >> 8) & 0xFF); + c["b"] = (int)(value & 0xFF); + return c; + }; - // Equality comparison - sol::meta_function::equal_to, &LuaColor::operator==, + // Lighten/Darken helper using wxColour + Color["lighten"] = [&lua](sol::table c, int percent) { + wxColour wx(c.get_or("r", 0), c.get_or("g", 0), c.get_or("b", 0)); + wxColour result = wx.ChangeLightness(100 + percent); - // String representation - sol::meta_function::to_string, [](const LuaColor& c) { - return "Color(" + std::to_string(c.red) + ", " + std::to_string(c.green) + ", " + std::to_string(c.blue) + (c.alpha != 255 ? ", " + std::to_string(c.alpha) : "") + ")"; - }); + sol::table res = lua.create_table(); + res["r"] = (int)result.Red(); + res["g"] = (int)result.Green(); + res["b"] = (int)result.Blue(); + return res; + }; - // Predefined colors as constants - sol::table colorConstants = lua.create_named_table("Colors"); - colorConstants["BLACK"] = LuaColor(0, 0, 0); - colorConstants["WHITE"] = LuaColor(255, 255, 255); - colorConstants["RED"] = LuaColor(255, 0, 0); - colorConstants["GREEN"] = LuaColor(0, 255, 0); - colorConstants["BLUE"] = LuaColor(0, 0, 255); - colorConstants["YELLOW"] = LuaColor(255, 255, 0); - colorConstants["CYAN"] = LuaColor(0, 255, 255); - colorConstants["MAGENTA"] = LuaColor(255, 0, 255); - colorConstants["ORANGE"] = LuaColor(255, 165, 0); - colorConstants["GRAY"] = LuaColor(128, 128, 128); - colorConstants["TRANSPARENT"] = LuaColor(0, 0, 0, 0); + Color["darken"] = [&lua](sol::table c, int percent) { + wxColour wx(c.get_or("r", 0), c.get_or("g", 0), c.get_or("b", 0)); + wxColour result = wx.ChangeLightness(100 - percent); + + sol::table res = lua.create_table(); + res["r"] = (int)result.Red(); + res["g"] = (int)result.Green(); + res["b"] = (int)result.Blue(); + return res; + }; + + // Helper to create a color table + auto mkColor = [&lua](int r, int g, int b) { + sol::table c = lua.create_table(); + c["r"] = r; + c["g"] = g; + c["b"] = b; + return c; + }; + + // Predefined colors + Color["white"] = mkColor(255, 255, 255); + Color["black"] = mkColor(0, 0, 0); + Color["blue"] = mkColor(49, 130, 206); + Color["red"] = mkColor(220, 53, 69); + Color["green"] = mkColor(40, 167, 69); + Color["yellow"] = mkColor(255, 193, 7); + Color["orange"] = mkColor(253, 126, 20); + Color["gray"] = mkColor(128, 128, 128); + Color["lightGray"] = mkColor(245, 247, 250); + Color["darkGray"] = mkColor(45, 55, 72); + + lua["Color"] = Color; } } // namespace LuaAPI diff --git a/source/lua/lua_api_color.h b/source/lua/lua_api_color.h index efc743089..1803f0b59 100644 --- a/source/lua/lua_api_color.h +++ b/source/lua/lua_api_color.h @@ -22,38 +22,8 @@ #include namespace LuaAPI { - - // Simple RGBA color class for Lua scripting - struct LuaColor { - uint8_t red; - uint8_t green; - uint8_t blue; - uint8_t alpha; - - LuaColor() : - red(0), green(0), blue(0), alpha(255) { } - LuaColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) : - red(r), green(g), blue(b), alpha(a) { } - - // Convert to wxColour for use with wxWidgets - wxColour toWxColour() const { - return wxColour(red, green, blue, alpha); - } - - // Create from wxColour - static LuaColor fromWxColour(const wxColour& c) { - return LuaColor(c.Red(), c.Green(), c.Blue(), c.Alpha()); - } - - // Equality comparison - bool operator==(const LuaColor& other) const { - return red == other.red && green == other.green && blue == other.blue && alpha == other.alpha; - } - }; - - // Register the Color usertype with Lua + // Register the Color module with Lua void registerColor(sol::state& lua); - } // namespace LuaAPI #endif // RME_LUA_API_COLOR_H diff --git a/source/lua/lua_dialog.cpp b/source/lua/lua_dialog.cpp index 09fff70e0..c848760d6 100644 --- a/source/lua/lua_dialog.cpp +++ b/source/lua/lua_dialog.cpp @@ -38,9 +38,127 @@ #ifdef __WXMSW__ #include #endif +#include +#include using namespace std::string_literals; +class CustomButton : public wxControl { +public: + CustomButton(wxWindow* parent, wxWindowID id, const wxString& label, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0) + : wxControl(parent, id, pos, size, style | wxBORDER_NONE), m_label(label) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + } + + void SetHoverColors(const wxColour& bg, const wxColour& fg) { + m_hoverBg = bg; + m_hoverFg = fg; + m_hasHover = true; + } + + void SetRounded(bool rounded) { + m_rounded = rounded; + Refresh(); + } + + wxSize DoGetBestSize() const override { + wxClientDC dc(const_cast(this)); + dc.SetFont(GetFont()); + wxSize labelSize = dc.GetTextExtent(m_label); + return wxSize(labelSize.x + 30, labelSize.y + 16); + } + + void SetParentBgColor(const wxColour& c) { + m_parentBg = c; + Refresh(); + } + +private: + wxString m_label; + wxColour m_hoverBg, m_hoverFg; + wxColour m_parentBg; + bool m_rounded = false; + bool m_hasHover = false; + bool m_hovered = false; + bool m_pressed = false; + + void OnPaint(wxPaintEvent& evt) { + wxAutoBufferedPaintDC dc(this); + + // Use provided parent bg or actual parent bg + wxColour clearColor = m_parentBg.IsOk() ? m_parentBg : GetParent()->GetBackgroundColour(); + dc.SetBackground(wxBrush(clearColor)); + dc.Clear(); + + wxGraphicsContext* gc = wxGraphicsContext::Create(dc); + if (gc) { + wxColour bg = GetBackgroundColour(); + wxColour fg = GetForegroundColour(); + + if (!IsEnabled()) { + bg = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + fg = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); + } else { + if (m_hasHover && m_hovered) { + if (m_hoverBg.IsOk()) bg = m_hoverBg; + if (m_hoverFg.IsOk()) fg = m_hoverFg; + } + + if (m_pressed) { + bg = bg.ChangeLightness(85); + } + } + + gc->SetBrush(wxBrush(bg)); + gc->SetPen(wxPen(bg)); // No border stroke + + wxDouble w = GetSize().GetWidth(); + wxDouble h = GetSize().GetHeight(); + + wxGraphicsPath path = gc->CreatePath(); + if (m_rounded) { + path.AddRoundedRectangle(0, 0, w, h, 8.0); + } else { + path.AddRectangle(0, 0, w, h); + } + gc->FillPath(path); + + gc->SetFont(GetFont(), fg); + + wxDouble tw, th; + gc->GetTextExtent(m_label, &tw, &th, nullptr, nullptr); + gc->DrawText(m_label, (w - tw) / 2.0, (h - th) / 2.0); + + delete gc; + } + } + + void OnEnter(wxMouseEvent& e) { m_hovered = true; Refresh(); e.Skip(); } + void OnLeave(wxMouseEvent& e) { m_hovered = false; m_pressed = false; Refresh(); e.Skip(); } + void OnDown(wxMouseEvent& e) { m_pressed = true; Refresh(); e.Skip(); } + void OnUp(wxMouseEvent& e) { + if (m_pressed) { + m_pressed = false; + Refresh(); + wxCommandEvent clickEvent(wxEVT_BUTTON, GetId()); + clickEvent.SetEventObject(this); + ProcessWindowEvent(clickEvent); + } + } + + DECLARE_EVENT_TABLE() +}; + +BEGIN_EVENT_TABLE(CustomButton, wxControl) + EVT_PAINT(CustomButton::OnPaint) + EVT_ENTER_WINDOW(CustomButton::OnEnter) + EVT_LEAVE_WINDOW(CustomButton::OnLeave) + EVT_LEFT_DOWN(CustomButton::OnDown) + EVT_LEFT_UP(CustomButton::OnUp) +END_EVENT_TABLE() + // Specialized Canvas for Lua Dialogs // Specialized Canvas for Lua Dialogs class MapPreviewCanvas : public MapCanvas { @@ -338,6 +456,9 @@ void LuaDialog::finishCurrentRow() { } wxWindow* LuaDialog::getParentForWidget() { + if (!panelStack.empty()) { + return panelStack.top(); + } if (currentTabPanel) { return currentTabPanel; } @@ -360,14 +481,12 @@ wxSizer* LuaDialog::getSizerForWidget() { LuaDialog* LuaDialog::wrap(sol::table options) { finishCurrentRow(); + rowSizerStack.push(currentRowSizer); + currentRowSizer = nullptr; - // 'wrap' is just a horizontal box sizer that wraps? - // Wx doesn't have a simple WrapSizer like that, it has wxWrapSizer. - // But usually "visual grouping side-by-side" is just a Horizontal Box. - - wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); - getSizerForWidget()->Add(sizer, 1, wxEXPAND | wxALL, 0); // Add to current - sizerStack.push(sizer); // Push to stack + wxBoxSizer* sizer = new wxBoxSizer(options.get_or(std::string("orient"), "horizontal"s) == "horizontal" ? wxHORIZONTAL : wxVERTICAL); + getSizerForWidget()->Add(sizer, 1, wxEXPAND | wxALL, 0); + sizerStack.push(sizer); return this; } @@ -377,6 +496,10 @@ LuaDialog* LuaDialog::endwrap() { if (!sizerStack.empty() && sizerStack.top() != mainSizer && (!currentTabSizer || sizerStack.top() != currentTabSizer)) { sizerStack.pop(); } + if (!rowSizerStack.empty()) { + currentRowSizer = rowSizerStack.top(); + rowSizerStack.pop(); + } return this; } @@ -385,39 +508,95 @@ LuaDialog* LuaDialog::box(sol::table options) { std::string orient = options.get_or(std::string("orient"), "vertical"s); std::string label = options.get_or(std::string("label"), ""s); + bool expand = options.get_or("expand", label.empty()); // Defaults to true only if no label (backwards compat) + if (options["expand"].valid()) expand = options.get("expand"); wxSizer* sizer; if (!label.empty()) { // Static box sizer - wxStaticBoxSizer* staticBox = new wxStaticBoxSizer(wxVERTICAL, getParentForWidget(), wxString(label)); - if (orient == "horizontal") { - // wxStaticBoxSizer constructor takes orient, but we passed vertical - // Actually can check if we can change it or construct differently - // Reconstruct correct orient - delete staticBox; - staticBox = new wxStaticBoxSizer(wxHORIZONTAL, getParentForWidget(), wxString(label)); - } + wxStaticBoxSizer* staticBox = new wxStaticBoxSizer(orient == "horizontal" ? wxHORIZONTAL : wxVERTICAL, getParentForWidget(), wxString(label)); sizer = staticBox; + + if (options["bgcolor"].valid() || options["fgcolor"].valid()) { + applyCommonOptions(staticBox->GetStaticBox(), options); + } } else { - // Normal box sizer - if (orient == "horizontal") { - sizer = new wxBoxSizer(wxHORIZONTAL); - } else { - sizer = new wxBoxSizer(wxVERTICAL); + sizer = new wxBoxSizer(orient == "horizontal" ? wxHORIZONTAL : wxVERTICAL); + } + + getSizerForWidget()->Add(sizer, expand ? 1 : 0, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options)); + + int width = options.get_or("width", -1); + int height = options.get_or("height", -1); + if (width != -1 || height != -1) { + wxSize sz(width, height); + sizer->SetMinSize(sz); + + // If it's a static box sizer, we can set the max size on the actual window + if (!expand && !label.empty()) { + wxStaticBoxSizer* sbSizer = static_cast(sizer); + sbSizer->GetStaticBox()->SetMaxSize(sz); } } - getSizerForWidget()->Add(sizer, 1, wxEXPAND | wxALL, 5); + rowSizerStack.push(currentRowSizer); + currentRowSizer = nullptr; sizerStack.push(sizer); - return this; } LuaDialog* LuaDialog::endbox() { finishCurrentRow(); - if (!sizerStack.empty() && sizerStack.top() != mainSizer && (!currentTabSizer || sizerStack.top() != currentTabSizer)) { + if (!sizerStack.empty()) { + sizerStack.pop(); + } + if (!rowSizerStack.empty()) { + currentRowSizer = rowSizerStack.top(); + rowSizerStack.pop(); + } + return this; +} + +LuaDialog* LuaDialog::panel(sol::table options) { + finishCurrentRow(); + + std::string id = options.get_or(std::string("id"), "panel_"s + std::to_string(widgets.size())); + bool expand = options.get_or("expand", false); + + wxPanel* panel = new wxPanel(getParentForWidget(), wxID_ANY); + applyCommonOptions(panel, options); + + wxBoxSizer* panelSizer = new wxBoxSizer(options.get_or("orient", "vertical") == "horizontal" ? wxHORIZONTAL : wxVERTICAL); + panel->SetSizer(panelSizer); + + getSizerForWidget()->Add(panel, expand ? 1 : 0, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options)); + + rowSizerStack.push(currentRowSizer); + currentRowSizer = nullptr; + panelStack.push(panel); + sizerStack.push(panelSizer); + + LuaDialogWidget widget; + widget.id = id; + widget.type = "panel"; + widget.widget = panel; + widgets.push_back(widget); + + return this; +} + +LuaDialog* LuaDialog::endpanel() { + finishCurrentRow(); + if (!panelStack.empty()) { + panelStack.pop(); + } + if (!sizerStack.empty()) { sizerStack.pop(); } + if (!rowSizerStack.empty()) { + currentRowSizer = rowSizerStack.top(); + rowSizerStack.pop(); + } return this; } @@ -428,7 +607,7 @@ LuaDialog* LuaDialog::label(sol::table options) { std::string id = options.get_or(std::string("id"), ""s); wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(text)); - currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); if (!id.empty()) { LuaDialogWidget widget; @@ -469,7 +648,7 @@ LuaDialog* LuaDialog::mapCanvas(sol::table options) { // Default to center of map start canvas->SetPosition(1000, 1000, 7); - currentRowSizer->Add(canvas, 1, wxEXPAND | wxALL, 0); + currentRowSizer->Add(canvas, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options)); if (!id.empty()) { LuaDialogWidget widget; @@ -497,7 +676,7 @@ LuaDialog* LuaDialog::input(sol::table options) { } wxTextCtrl* input = new wxTextCtrl(getParentForWidget(), wxID_ANY, wxString(text), wxDefaultPosition, wxSize(150, -1)); - currentRowSizer->Add(input, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(input, 1, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); input->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& event) { suspendHotkeys(); @@ -550,7 +729,7 @@ LuaDialog* LuaDialog::number(sol::table options) { wxSpinCtrlDouble* spin = new wxSpinCtrlDouble(getParentForWidget(), wxID_ANY, "", wxDefaultPosition, wxSize(100, -1), wxSP_ARROW_KEYS, minVal, maxVal, value, decimals == 0 ? 1 : std::pow(0.1, decimals)); spin->SetDigits(decimals); - currentRowSizer->Add(spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(spin, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -586,7 +765,7 @@ LuaDialog* LuaDialog::slider(sol::table options) { } wxSlider* slider = new wxSlider(getParentForWidget(), wxID_ANY, value, minVal, maxVal, wxDefaultPosition, wxSize(150, -1)); - currentRowSizer->Add(slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(slider, 1, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -616,7 +795,7 @@ LuaDialog* LuaDialog::check(sol::table options) { wxCheckBox* checkbox = new wxCheckBox(getParentForWidget(), wxID_ANY, wxString(text)); checkbox->SetValue(selected); - currentRowSizer->Add(checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(checkbox, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -649,7 +828,7 @@ LuaDialog* LuaDialog::radio(sol::table options) { wxRadioButton* radio = new wxRadioButton(getParentForWidget(), wxID_ANY, wxString(text)); radio->SetValue(selected); - currentRowSizer->Add(radio, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(radio, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -704,7 +883,7 @@ LuaDialog* LuaDialog::combobox(sol::table options) { selected = choices[0].ToStdString(); } - currentRowSizer->Add(choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(choice, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -730,19 +909,60 @@ LuaDialog* LuaDialog::button(sol::table options) { std::string id = options.get_or(std::string("id"), "button_"s + std::to_string(widgets.size())); std::string text = options.get_or(std::string("text"), "Button"s); - bool focus = options.get_or(std::string("focus"), false); + bool rounded = options.get_or("rounded", false); + bool hasHover = options["hover"].valid(); + bool hasCustomColor = options["bgcolor"].valid() || options["fgcolor"].valid(); - wxButton* btn = new wxButton(getParentForWidget(), wxID_ANY, wxString(text)); - currentRowSizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + bool useCustom = rounded || hasHover || hasCustomColor; - if (focus) { - btn->SetDefault(); + wxWindow* finalWidget = nullptr; + + if (useCustom) { + CustomButton* btn = new CustomButton(getParentForWidget(), wxID_ANY, wxString(text)); + if (rounded) btn->SetRounded(true); + + // Anti-aliasing corner fix: check if we are inside a StaticBoxSizer with custom color + wxStaticBoxSizer* sbs = wxDynamicCast(currentRowSizer, wxStaticBoxSizer); + if (sbs) { + wxStaticBox* box = sbs->GetStaticBox(); + if (box && box->GetBackgroundColour().IsOk()) { + btn->SetParentBgColor(box->GetBackgroundColour()); + } + } + + if (hasHover) { + sol::table hoverOpts = options["hover"]; + auto parseColor = [](sol::object colorObj) -> wxColour { + if (colorObj.is()) { + sol::table c = colorObj.as(); + return wxColour(c.get_or("r", 1), c.get_or("g", 2), c.get_or("b", 3)); + } else if (colorObj.is()) { + std::string s = colorObj.as(); + return (s.size() > 0 && s[0] == '#') ? wxColour(s) : wxColour(s); + } + return wxColour(); + }; + + wxColour hBg, hFg; + if (hoverOpts["bgcolor"].valid()) hBg = parseColor(hoverOpts["bgcolor"]); + if (hoverOpts["fgcolor"].valid()) hFg = parseColor(hoverOpts["fgcolor"]); + btn->SetHoverColors(hBg, hFg); + } + finalWidget = btn; + } else { + wxButton* btn = new wxButton(getParentForWidget(), wxID_ANY, wxString(text)); + if (options.get_or("focus", false)) { + btn->SetDefault(); + } + finalWidget = btn; } + currentRowSizer->Add(finalWidget, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); + LuaDialogWidget widget; widget.id = id; widget.type = "button"; - widget.widget = btn; + widget.widget = finalWidget; if (options["onclick"].valid()) { widget.onclick = options["onclick"]; } @@ -750,11 +970,11 @@ LuaDialog* LuaDialog::button(sol::table options) { values[id] = sol::make_object(lua, false); - btn->Bind(wxEVT_BUTTON, [this, id](wxCommandEvent&) { + finalWidget->Bind(wxEVT_BUTTON, [this, id](wxCommandEvent&) { onButtonClick(id); }); - applyCommonOptions(btn, options); + applyCommonOptions(finalWidget, options); return this; } @@ -779,7 +999,7 @@ LuaDialog* LuaDialog::color(sol::table options) { } wxColourPickerCtrl* picker = new wxColourPickerCtrl(getParentForWidget(), wxID_ANY, defaultColor); - currentRowSizer->Add(picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(picker, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -1064,7 +1284,7 @@ LuaDialog* LuaDialog::list(sol::table options) { } listbox->SetShowText(showText); listbox->SetSmooth(smooth); - getSizerForWidget()->Add(listbox, 1, wxEXPAND | wxALL, 5); + getSizerForWidget()->Add(listbox, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options)); // Populate items if (options["items"].valid()) { @@ -1243,7 +1463,7 @@ LuaDialog* LuaDialog::file(sol::table options) { if (!labelText.empty()) { wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText)); - currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); } wxFilePickerCtrl* picker; @@ -1252,7 +1472,7 @@ LuaDialog* LuaDialog::file(sol::table options) { } else { picker = new wxFilePickerCtrl(getParentForWidget(), wxID_ANY, wxString(filename), "Select a file", wxString(filetypes), wxDefaultPosition, wxSize(200, -1), wxFLP_OPEN | wxFLP_FILE_MUST_EXIST | wxFLP_USE_TEXTCTRL); } - currentRowSizer->Add(picker, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(picker, 1, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -1284,7 +1504,7 @@ LuaDialog* LuaDialog::image(sol::table options) { if (!labelText.empty()) { wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText)); - currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); } wxBitmap bmp; @@ -1340,7 +1560,7 @@ LuaDialog* LuaDialog::image(sol::table options) { } wxStaticBitmap* staticBmp = new wxStaticBitmap(getParentForWidget(), wxID_ANY, bmp); - currentRowSizer->Add(staticBmp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(staticBmp, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -1364,12 +1584,12 @@ LuaDialog* LuaDialog::item(sol::table options) { if (!labelText.empty()) { wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText)); - currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); } int clientId = (itemId > 100 && itemId <= g_items.getMaxID()) ? g_items[itemId].clientID : 0; ItemButton* btn = new ItemButton(getParentForWidget(), RENDER_SIZE_32x32, clientId); - currentRowSizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + currentRowSizer->Add(btn, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -1416,15 +1636,14 @@ LuaDialog* LuaDialog::item(sol::table options) { return this; } -LuaDialog* LuaDialog::separator() { +LuaDialog* LuaDialog::separator(sol::optional options) { finishCurrentRow(); wxStaticLine* line = new wxStaticLine(getParentForWidget(), wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); - getSizerForWidget()->Add(line, 0, wxEXPAND | wxALL, 5); + sol::table opts = options.value_or(lua.create_table()); + getSizerForWidget()->Add(line, 0, getSizerFlags(opts, wxEXPAND | wxALL), getSizerBorder(opts)); - // Separator doesn't support many options but we can try - // applyCommonOptions(line, options); // options not passed to separator() usually - // But signature is LuaDialog* separator(); so no options table. + applyCommonOptions(line, opts); return this; } @@ -1560,6 +1779,8 @@ LuaDialog* LuaDialog::tab(sol::table options) { tabInfos.push_back(tabInfo); } + rowSizerStack.push(currentRowSizer); + currentRowSizer = nullptr; sizerStack.push(currentTabSizer); tabSizers.push_back(currentTabSizer); @@ -1574,6 +1795,10 @@ LuaDialog* LuaDialog::endtabs() { if (!sizerStack.empty() && sizerStack.top() == topSizer) { sizerStack.pop(); } + if (!rowSizerStack.empty()) { + currentRowSizer = rowSizerStack.top(); + rowSizerStack.pop(); + } tabSizers.pop_back(); } @@ -2290,7 +2515,7 @@ LuaDialog* LuaDialog::grid(sol::table options) { } } - currentRowSizer->Add(grid, 1, wxEXPAND | wxALL, 0); + currentRowSizer->Add(grid, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options)); LuaDialogWidget widget; widget.id = id; @@ -2574,6 +2799,30 @@ void LuaDialog::collectAllValues() { } } +int LuaDialog::getSizerFlags(sol::table options, int defaultFlags) { + int flags = defaultFlags; + if (options["align"].valid()) { + std::string align = options.get("align"); + if (align == "center") flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_LEFT) | wxALIGN_CENTER_HORIZONTAL; + else if (align == "right") flags = (flags & ~wxALIGN_LEFT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_RIGHT; + else if (align == "left") flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_LEFT; + } + if (options["valign"].valid()) { + std::string valign = options.get("valign"); + if (valign == "center") flags = (flags & ~wxALIGN_TOP & ~wxALIGN_BOTTOM) | wxALIGN_CENTER_VERTICAL; + else if (valign == "top") flags = (flags & ~wxALIGN_BOTTOM & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_TOP; + else if (valign == "bottom") flags = (flags & ~wxALIGN_TOP & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_BOTTOM; + } + if (options.get_or("expand", false)) { + flags |= wxEXPAND; + } + return flags; +} + +int LuaDialog::getSizerBorder(sol::table options) { + return options.get_or("padding", 5) + options.get_or("margin", 0); +} + void LuaDialog::applyCommonOptions(wxWindow* widget, sol::table options) { if (options["tooltip"].valid()) { widget->SetToolTip(wxString(options.get("tooltip"))); @@ -2584,6 +2833,77 @@ void LuaDialog::applyCommonOptions(wxWindow* widget, sol::table options) { if (options["visible"].valid()) { widget->Show(options.get("visible")); } + + auto parseColor = [](sol::object colorObj) -> wxColour { + if (colorObj.is()) { + sol::table c = colorObj.as(); + int r = c.get_or("r", c.get_or(1, 255)); + int g = c.get_or("g", c.get_or(2, 255)); + int b = c.get_or("b", c.get_or(3, 255)); + return wxColour(r, g, b); + } else if (colorObj.is()) { + std::string colorStr = colorObj.as(); + if (!colorStr.empty() && colorStr[0] == '#') { + unsigned long hexValue = 0; + if (colorStr.length() == 7) { + hexValue = std::stoul(colorStr.substr(1), nullptr, 16); + } else if (colorStr.length() == 4) { + char r = colorStr[1], g = colorStr[2], b = colorStr[3]; + std::string expanded = {r, r, g, g, b, b}; + hexValue = std::stoul(expanded, nullptr, 16); + } + return wxColour((hexValue >> 16) & 0xFF, (hexValue >> 8) & 0xFF, hexValue & 0xFF); + } + return wxColour(wxString(colorStr)); + } + return wxColour(); + }; + + if (options["bgcolor"].valid()) { + wxColour colour = parseColor(options["bgcolor"]); + if (colour.IsOk()) { + widget->SetBackgroundColour(colour); + } + } + + if (options["fgcolor"].valid()) { + wxColour colour = parseColor(options["fgcolor"]); + if (colour.IsOk()) { + widget->SetForegroundColour(colour); + } + } + + if (options["font_size"].valid() || options["font_weight"].valid() || options["font_style"].valid()) { + wxFont font = widget->GetFont(); + if (options["font_size"].valid()) { + font.SetPointSize(options.get("font_size")); + } + if (options["font_weight"].valid()) { + std::string weight = options.get("font_weight"); + if (weight == "bold") font.SetWeight(wxFONTWEIGHT_BOLD); + else if (weight == "light") font.SetWeight(wxFONTWEIGHT_LIGHT); + else font.SetWeight(wxFONTWEIGHT_NORMAL); + } + if (options["font_style"].valid()) { + std::string style = options.get("font_style"); + if (style == "italic") font.SetStyle(wxFONTSTYLE_ITALIC); + else font.SetStyle(wxFONTSTYLE_NORMAL); + } + widget->SetFont(font); + } + + // NEW SIZE PROPERTIES + if (options["width"].valid() || options["height"].valid()) { + int w = options.get_or("width", -1); + int h = options.get_or("height", -1); + wxSize size(w, h); + + widget->SetMinSize(size); + // If expand is false, we also limit the maximum size to be strict + if (!options.get_or("expand", true)) { + widget->SetMaxSize(size); + } + } } void LuaDialog::suspendHotkeys() { @@ -2688,7 +3008,7 @@ namespace LuaAPI { sol::call_constructor, sol::factories([](sol::table options, sol::this_state ts) { return new LuaDialog(options, ts); }, [](const std::string& title, sol::this_state ts) { return new LuaDialog(title, ts); }), // Widget methods (return self for chaining) - "label", &LuaDialog::label, "input", &LuaDialog::input, "number", &LuaDialog::number, "slider", &LuaDialog::slider, "check", &LuaDialog::check, "radio", &LuaDialog::radio, "combobox", &LuaDialog::combobox, "button", &LuaDialog::button, "color", &LuaDialog::color, "item", &LuaDialog::item, "file", &LuaDialog::file, "image", &LuaDialog::image, "separator", &LuaDialog::separator, "newrow", &LuaDialog::newrow, "tab", &LuaDialog::tab, "endtabs", &LuaDialog::endtabs, "wrap", &LuaDialog::wrap, "endwrap", &LuaDialog::endwrap, "box", &LuaDialog::box, "endbox", &LuaDialog::endbox, "mapCanvas", &LuaDialog::mapCanvas, "list", &LuaDialog::list, "grid", &LuaDialog::grid, + "label", &LuaDialog::label, "input", &LuaDialog::input, "number", &LuaDialog::number, "slider", &LuaDialog::slider, "check", &LuaDialog::check, "radio", &LuaDialog::radio, "combobox", &LuaDialog::combobox, "button", &LuaDialog::button, "color", &LuaDialog::color, "item", &LuaDialog::item, "file", &LuaDialog::file, "image", &LuaDialog::image, "separator", &LuaDialog::separator, "newrow", &LuaDialog::newrow, "tab", &LuaDialog::tab, "endtabs", &LuaDialog::endtabs, "wrap", &LuaDialog::wrap, "endwrap", &LuaDialog::endwrap, "box", &LuaDialog::box, "endbox", &LuaDialog::endbox, "panel", &LuaDialog::panel, "endpanel", &LuaDialog::endpanel, "mapCanvas", &LuaDialog::mapCanvas, "list", &LuaDialog::list, "grid", &LuaDialog::grid, // Dialog control "show", &LuaDialog::show, "close", &LuaDialog::close, "modify", &LuaDialog::modify, "repaint", &LuaDialog::repaint, "clear", &LuaDialog::clear, "layout", &LuaDialog::layout, diff --git a/source/lua/lua_dialog.h b/source/lua/lua_dialog.h index 99218133d..9ac22f5ae 100644 --- a/source/lua/lua_dialog.h +++ b/source/lua/lua_dialog.h @@ -93,7 +93,7 @@ class LuaDialog : public wxDialog { LuaDialog* grid(sol::table options); LuaDialog* file(sol::table options); LuaDialog* image(sol::table options); - LuaDialog* separator(); + LuaDialog* separator(sol::optional options); LuaDialog* newrow(); LuaDialog* tab(sol::table options); @@ -107,6 +107,9 @@ class LuaDialog : public wxDialog { LuaDialog* box(sol::table options); // Generic box sizer LuaDialog* endbox(); + LuaDialog* panel(sol::table options); + LuaDialog* endpanel(); + // Dialog control LuaDialog* show(sol::optional options); void close(); @@ -135,6 +138,8 @@ class LuaDialog : public wxDialog { sol::function oncloseCallback; std::stack sizerStack; + std::stack panelStack; + std::stack rowSizerStack; std::vector tabSizers; // Parent window for widget creation (usually dialog, but can be other panels) // Actually, keeping text/buttons parenting to dialog is easier for events. @@ -166,6 +171,8 @@ class LuaDialog : public wxDialog { void updateValue(const std::string& id); void collectAllValues(); void applyCommonOptions(wxWindow* widget, sol::table options); + int getSizerFlags(sol::table options, int defaultFlags = 0); + int getSizerBorder(sol::table options); void suspendHotkeys(); void resumeHotkeys(); sol::table makeTabInfoTable(int index); From db809e3e105ee0bc8621c84011f2c5b11ffbc4ed Mon Sep 17 00:00:00 2001 From: michyaraque Date: Wed, 28 Jan 2026 20:07:04 +0100 Subject: [PATCH 06/10] docs: update documentation and linter with the new features --- scripts/README.md | 21 ++++++++++++++++ scripts/linter.lua | 62 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index 59fda686e..224bc8481 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -444,6 +444,27 @@ All widget methods return the `Dialog` object itself, allowing for method chaini | `separator()` | - | Draws a horizontal line separator. | | `tab(options)` | `{id="tab_id", text="Tab Name", button=false, index=1, onclick=func, oncontextmenu=func}` | Starts a new tab page or a tab-like button (`button=true`). | | `endtabs()` | - | Ends the tab definition. | +| `panel(options)` | `{bgcolor=Color.white, padding=5, margin=5, expand=true, height=30}` | Starts a styled container panel. Supports background color, padding, margin, and standard layout options. | +| `endpanel()` | - | Ends the current panel. | + +#### **Common Widget Options** +Most widgets support these additional layout and styling properties: + +| Property | Type | Description | +| :--- | :--- | :--- | +| `expand` | boolean | If `true`, the widget expands to fill available space in the layout direction. | +| `align` | string | Horizontal alignment: `"left"`, `"center"`, `"right"`. | +| `valign` | string | Vertical alignment: `"top"`, `"center"`, `"bottom"`. | +| `width`, `height` | number | Explicit width/height in pixels. | +| `min_width`, `min_height` | number | Minimum dimensions. | +| `max_width`, `max_height` | number | Maximum dimensions. | +| `margin` | number | Outer margin (all sides). | +| `padding` | number | Inner padding (for containers like `panel` or `box`). | +| `bgcolor` | string/Color | Background color (e.g., `Color.red` or `"#FF0000"`). | +| `fgcolor` | string/Color | Foreground (text) color. | +| `font_size` | number | Font size in points. | +| `font_weight` | string | Font weight: `"normal"`, `"bold"`. | + #### **Input Widgets** | Widget | Options | Description | diff --git a/scripts/linter.lua b/scripts/linter.lua index 8987cc551..1845ba96d 100644 --- a/scripts/linter.lua +++ b/scripts/linter.lua @@ -134,6 +134,7 @@ function SelectionClass:finish() end ---@field name string ---@field width number ---@field height number +---@field tileCount number local MapClass = {} ---@param x number ---@param y number @@ -185,13 +186,15 @@ local Events = {} ---@field goToHistory fun(self: Editor, index: number) local EditorClass = {} +---@alias WidgetOptions {id?: string, align?: "left"|"center"|"right", valign?: "top"|"center"|"bottom", expand?: boolean, width?: number, height?: number, min_width?: number, min_height?: number, max_width?: number, max_height?: number, margin?: number, padding?: number, bgcolor?: string|table, fgcolor?: string|table, font_size?: number, font_weight?: "normal"|"bold"} + ---@class Dialog ----@field data table ----@field values table ----@field bounds table ----@field dockable boolean ----@field activeTab string|nil ----@field onclose fun() +---@field data? table +---@field values? table +---@field bounds? table +---@field dockable? boolean +---@field activeTab? string|nil +---@field onclose? fun() ---@overload fun(title_or_config: string|{title?: string, resizable?: boolean, dockable?: boolean, id?: string, x?: number, y?: number, width?: number, height?: number, onclose?: fun()}): Dialog local DialogClass = {} @@ -258,6 +261,13 @@ function DialogClass:box(options) return {} --[[@as Dialog]] end ---@return Dialog function DialogClass:endbox() return {} --[[@as Dialog]] end +---@param options? WidgetOptions|{label?: string} +---@return Dialog +function DialogClass:panel(options) return {} --[[@as Dialog]] end + +---@return Dialog +function DialogClass:endpanel() return {} --[[@as Dialog]] end + ---@return Dialog function DialogClass:wrap() return {} --[[@as Dialog]] end @@ -389,7 +399,7 @@ function ImageClass.fromItemSprite(itemId) return {} --[[@as Image]] end ---@return Image function ImageClass.fromSprite(spriteId) return {} --[[@as Image]] end ---- Resizes the image to the specified dimensions + ---@param width number ---@param height number ---@param smooth? boolean Use smooth scaling (default true). Set to false for pixel-perfect scaling. @@ -402,8 +412,42 @@ function ImageClass:resize(width, height, smooth) return {} --[[@as Image]] end ---@return Image function ImageClass:scale(factor, smooth) return {} --[[@as Image]] end ----@type fun(path_or_options?: string|{path?: string, itemid?: number, spriteid?: number}): Image -Image = ImageClass +---@class ImageStatics +---@field fromFile fun(path: string): Image +---@field fromItemSprite fun(itemId: number): Image +---@field fromSprite fun(spriteId: number): Image +---@overload fun(path_or_options?: string|{path?: string, itemid?: number, spriteid?: number}): Image + +---@type ImageStatics +---@diagnostic disable-next-line: missing-fields +Image = {} --[[@as ImageStatics]] + +---@class Color +---@field red number +---@field green number +---@field blue number +---@field alpha number + +---@class ColorHelper +---@field red string|table +---@field green string|table +---@field blue string|table +---@field white string|table +---@field black string|table +---@field gray string|table +---@field darkGray string|table +---@field lightGray string|table +---@field orange string|table +---@field yellow string|table +---@field cyan string|table +---@field magenta string|table +---@field transparent string|table +---@field lighten fun(color: string|table|Color, amount: number): Color +---@field darken fun(color: string|table|Color, amount: number): Color + +---@type ColorHelper +---@diagnostic disable-next-line: missing-fields +Color = {} --[[@as ColorHelper]] -- Global variables set by the engine ---@type string The directory containing the currently executing script. Use this to load resources relative to your script. From 8dd2990c0293adbca279794b4c34dabcff7f6c8f Mon Sep 17 00:00:00 2001 From: michyaraque Date: Wed, 28 Jan 2026 20:07:49 +0100 Subject: [PATCH 07/10] refactor: update hello_world to show the new capabitilies --- scripts/hello_world/hello_world.lua | 461 +++++++++++++--------------- 1 file changed, 220 insertions(+), 241 deletions(-) diff --git a/scripts/hello_world/hello_world.lua b/scripts/hello_world/hello_world.lua index 9df19ddcb..af78db28e 100644 --- a/scripts/hello_world/hello_world.lua +++ b/scripts/hello_world/hello_world.lua @@ -24,54 +24,132 @@ local createAndShowDialog createAndShowDialog = function(is_dockable) local dlg = Dialog { - title = "RME Lua API Capabilities Demo", - width = 600, - height = 500, + title = "RME Modern UI Demo", + width = 650, + height = 400, resizable = true, dockable = is_dockable, - activeTab = "Basic Widgets", -- Set default tab + activeTab = "Modern Styles", -- Set default tab to the new one onclose = function() print("Demo dialog closed.") end } -- ---------------------------------------------------------------------------- - -- Tab 1: Basic Widgets + -- Modern Header Panel (Using new capabilities) -- ---------------------------------------------------------------------------- - dlg:tab { text = "Basic Widgets", oncontextmenu = function(d, info) - return { - { text = "Tab Context Menu", onclick = function() app.alert("Clicked Tab Context") end } - } - end } - dlg:box { orient = "vertical", label = "Input Fields" } - dlg:label { text = "Standard inputs available in the API:" } - dlg:newrow() - - dlg:input { id = "t_input", label = "Text Entry:", text = "Edit me!" } - dlg:number { id = "t_number", label = "Number Spinner:", value = 100, min = 0, max = 500 } - dlg:slider { id = "t_slider", label = "Slider:", value = 50, min = 0, max = 100 } - dlg:color { id = "t_color", label = "Color Picker:", color = { red = 100, green = 200, blue = 255 } } - dlg:file { id = "t_file", label = "File Picker:", filename = "test.txt", save = false } - dlg:endbox() + dlg:panel({ bgcolor = Color.darkGray, padding = 10, expand = false, height = 30 }) + dlg:label({ + text = "RME SCRIPTING ENGINE", + fgcolor = Color.white, + font_size = 14, + font_weight = "bold", + align = "center" + }) + dlg:label({ + text = "Demonstrating Advanced Styling & Flex Layout", + fgcolor = Color.lighten(Color.gray, 30), + font_size = 9, + align = "center" + }) + dlg:endpanel() - dlg:separator() + -- ---------------------------------------------------------------------------- + -- Tab 1: Modern Styles (The new stuff!) + -- ---------------------------------------------------------------------------- + dlg:tab { text = "Modern Styles" } + + dlg:panel({ bgcolor = "#F8FAFC", padding = 15, margin = 5, expand = true }) + dlg:label({ text = "Container Panels & Boxes", font_weight = "bold", font_size = 12 }) + + dlg:box({ label = "Fixed Size & Alignment", margin = 5, bgcolor = Color.white, expand = false, align = "center" }) + dlg:label({ text = "This box has a custom background and is centered.", align = "center", margin = 10 }) + dlg:button({ text = "I'm a styled button", bgcolor = Color.blue, fgcolor = "white", width = 180, align = "center", rounded = true, hover = { bgcolor = Color.darken(Color.blue, 10) } }) + dlg:endbox() + + dlg:newrow() + + dlg:box({ label = "Color Palette Demo", orient = "horizontal", padding = 10, expand = true }) + dlg:panel({ bgcolor = Color.red, width = 60, height = 40, margin = 2, expand = false }) + dlg:label({ text = "Red", fgcolor = "white", align = "center", valign = "center" }) + dlg:endpanel() + dlg:panel({ bgcolor = Color.green, width = 60, height = 40, margin = 2, expand = false }) + dlg:label({ text = "Green", fgcolor = "white", align = "center", valign = "center" }) + dlg:endpanel() + dlg:panel({ bgcolor = Color.blue, width = 60, height = 40, margin = 2, expand = false }) + dlg:label({ text = "Blue", fgcolor = "white", align = "center", valign = "center" }) + dlg:endpanel() + dlg:panel({ bgcolor = Color.orange, width = 60, height = 40, margin = 2, expand = false }) + dlg:label({ text = "Orange", fgcolor = "white", align = "center", valign = "center" }) + dlg:endpanel() + dlg:endbox() + + dlg:separator() + + dlg:label({ text = "Interactive Theme Switcher", font_weight = "bold" }) + dlg:box({ orient = "horizontal", align = "center" }) + dlg:button({ + text = "Dark Mode", + bgcolor = "#1A202C", + fgcolor = "#EDF2F7", + hover = { bgcolor = Color.darken(Color.black, 10) }, + onclick = function(d) + d:modify({ + style_preview = { bgcolor = "#2D3748", fgcolor = "#F7FAFC" }, + style_lbl = { text = "Viewing: Dark Theme", fgcolor = Color.green } + }) + end + }) + dlg:button({ + text = "Light Mode", + bgcolor = "#EDF2F7", + fgcolor = "#1A202C", + onclick = function(d) + d:modify({ + style_preview = { bgcolor = Color.white, fgcolor = Color.black }, + style_lbl = { text = "Viewing: Light Theme", fgcolor = Color.blue } + }) + end + }) + dlg:endbox() - dlg:box { orient = "horizontal", label = "Toggles & Choices" } - dlg:box { orient = "vertical" } - dlg:check { id = "t_check_1", text = "Checkbox Option A", selected = true } - dlg:check { id = "t_check_2", text = "Checkbox Option B", selected = false } - dlg:endbox() + dlg:panel({ id = "style_preview", bgcolor = Color.white, margin = 10, padding = 15, expand = true }) + dlg:label({ id = "style_lbl", text = "This panel can be dynamically updated using d:modify()", align = "center" }) + dlg:endpanel() + dlg:endpanel() - dlg:box { orient = "vertical" } - dlg:radio { id = "t_radio_1", text = "Radio Mode 1", selected = true } - dlg:radio { id = "t_radio_2", text = "Radio Mode 2", selected = false } + + -- ---------------------------------------------------------------------------- + -- Tab 2: Basic Widgets (Updated with light styling) + -- ---------------------------------------------------------------------------- + dlg:tab { text = "Basic Widgets" } + dlg:box { orient = "vertical", label = "Classic Control Set", padding = 10 } + dlg:label { text = "Standard inputs with optional alignment:", font_weight = "bold" } + + dlg:input { id = "t_input", label = "Text Entry:", text = "Edit me!", expand = true } + dlg:number { id = "t_number", label = "Number Spinner:", value = 100, min = 0, max = 500, align = "left" } + dlg:slider { id = "t_slider", label = "Slider:", value = 50, min = 0, max = 100, expand = true } + + dlg:box({ orient = "horizontal" }) + dlg:color { id = "t_color", label = "Color Picker:", color = { red = 100, green = 200, blue = 255 } } + dlg:file { id = "t_file", label = "File Selection:", filename = "test.txt", save = false, expand = true } + dlg:endbox() dlg:endbox() - dlg:combobox { id = "t_combo", label = "Combobox:", options = { "Red", "Green", "Blue", "Alpha" }, option = "Green" } + dlg:box { orient = "horizontal", label = "Toggles", expand = true } + dlg:panel({ padding = 5, expand = true }) + dlg:check { id = "t_check_1", text = "Enable Feature A", selected = true } + dlg:check { id = "t_check_2", text = "Enable Feature B", selected = false } + dlg:endpanel() + dlg:panel({ bgcolor = "#f0f0f0", padding = 5, expand = true }) + dlg:radio { id = "t_radio_1", text = "High Performance", selected = true } + dlg:radio { id = "t_radio_2", text = "Power Saving", selected = false } + dlg:endpanel() dlg:endbox() - dlg:separator() - dlg:button { text = "Read Values", onclick = function(d) + dlg:combobox { id = "t_combo", label = "Select Category:", options = { "Red", "Green", "Blue", "Alpha" }, option = "Green", margin = 10 } + + dlg:button { text = "Debug Current State", bgcolor = Color.darkGray, fgcolor = "white", align = "right", margin = 10, onclick = function(d) local data = d.data local info = string.format( "Input: %s\nNumber: %d\nSlider: %d\nColor: %s\nCheck A: %s\nRadio 1: %s", @@ -79,248 +157,149 @@ createAndShowDialog = function(is_dockable) tostring(data.t_check_1), tostring(data.t_radio_1) ) app.alert(info) - d:modify { t_input = { label = "Updated Label:" } } end } -- ---------------------------------------------------------------------------- - -- Tab 2: Visuals & Lists + -- Tab 3: Visuals & Lists -- ---------------------------------------------------------------------------- dlg:tab { text = "Visuals & Lists" } - dlg:wrap({}) - -- Image Widget demos - dlg:box { orient = "vertical", label = "Images" } - dlg:label { text = "Item Sprite (2160):" } - dlg:image { id = "img_item", itemid = 2160, width = 32, height = 32, smooth = false } - - dlg:label { text = "Raw Sprite (100):" } - dlg:image { id = "img_sprite", spriteid = 100, width = 32, height = 32 } - dlg:endbox() - - -- Map Canvas - dlg:box { orient = "vertical", label = "Map Preview" } - dlg:mapCanvas { id = "preview_canvas", width = 150, height = 100 } - dlg:endbox() + dlg:wrap({ padding = 5 }) + dlg:box { orient = "vertical", label = "Engine Sprites", width = 150, expand = false } + dlg:label { text = "Item ID 2160:", align = "center" } + dlg:image { id = "img_item", itemid = 2160, width = 64, height = 64, align = "center", smooth = false } + dlg:label { text = "Raw Sprite 100:", align = "center" } + dlg:image { id = "img_sprite", spriteid = 100, width = 64, height = 64, align = "center" } + dlg:endbox() + + dlg:box { orient = "vertical", label = "Map Viewport", expand = true } + dlg:mapCanvas { id = "preview_canvas", expand = true, height = 200 } + dlg:label { text = "Live preview of the map", align = "center", font_size = 8 } + dlg:endbox() dlg:endwrap() - dlg:separator() - - -- LIST WIDGET - dlg:box { orient = "horizontal", label = "List & Grid" } - - local last_click_time = 0 - - dlg:list { - id = "demo_list", - width = 200, - height = 200, - show_text = true, - items = { - { text = "Item 1 (Plain)", tooltip = "Standard Item" }, - { text = "Item 2 (Icon)", icon = 2160, tooltip = "Item with Icon" }, - { text = "Item 3 (Tooltip)", tooltip = "I have a tooltip!" }, - { text = "Double Click Me", icon = 2152, tooltip = "Double click test" } - }, - onleftclick = function(d, info) - -- Mimic favorites behavior: simple selection logic if needed - print("List Left Click index: " .. tostring(info and info.index)) - end, - ondoubleclick = function(d) - -- WORKAROUND: Engine may trigger double click twice, using temporal debounce - local now = os.clock() - if now - last_click_time < 0.5 then - return - end - last_click_time = now - - local sel = d.data.demo_list - app.alert("List Double Click! Selection: " .. tostring(sel)) - end, - oncontextmenu = function(d, info) - -- Only show context menu if valid item clicked (favorites style) - if info and info.index and info.index > 0 then - return { - { - text = "List Action (Item " .. info.index .. ")", - onclick = function() - app.alert( - "Clicked List Item " .. info.index) - end - }, - { separator = true }, - { text = "Delete", onclick = function() app.alert("Delete Action") end } + dlg:box { orient = "horizontal", label = "Advanced View Widgets", expand = true } + dlg:panel({ expand = true }) + dlg:label({ text = "Custom ListBox", font_weight = "bold" }) + dlg:list { + id = "demo_list", + height = 180, + expand = true, + items = { + { text = "Legendary Sword", icon = 2160, tooltip = "Deals massive damage" }, + { text = "Health Potion", icon = 2152, tooltip = "Restores 100 HP" }, + { text = "Mystery Key", icon = 2148, tooltip = "What does it open?" } } - else - -- Background context menu - return { - { text = "List Background Action", onclick = function() app.alert("Clicked List Background") end } - } - end - end - } - - -- GRID WIDGET - dlg:grid { - id = "demo_grid", - width = 200, - height = 200, - cell_size = 40, - item_size = 32, - show_text = true, -- Try forcing text if desired, though grid usually is icon-based - items = { - { tooltip = "Coins", image = Image.fromItemSprite(2148) }, - { tooltip = "Platinum", image = Image.fromItemSprite(2152) }, - { tooltip = "Crystal", image = Image.fromItemSprite(2160) }, - { tooltip = "Sprite 100", image = Image.fromSprite(100) }, - { tooltip = "Sprite 101", image = Image.fromSprite(101) } - }, - onleftclick = function(d, info) - print("Grid Left Click index: " .. tostring(info and info.index)) - end, - oncontextmenu = function(d, info) - -- Favorites style context menu - if info and info.index and info.index > 0 then - return { - { text = "Grid Item Action", onclick = function() app.alert("Grid Item " .. info.index) end } + } + dlg:endpanel() + + dlg:panel({ expand = true }) + dlg:label({ text = "Item Grid", font_weight = "bold" }) + dlg:grid { + id = "demo_grid", + height = 180, + cell_size = 48, + item_size = 32, + expand = true, + items = { + { tooltip = "Gold", image = Image.fromItemSprite(2148) }, + { tooltip = "Platinum", image = Image.fromItemSprite(2152) }, + { tooltip = "Crystal", image = Image.fromItemSprite(2160) }, + { tooltip = "Raw 100", image = Image.fromSprite(100) }, + { tooltip = "Raw 101", image = Image.fromSprite(101) } } - end - return { - { text = "Grid Background Action", onclick = function() app.alert("Grid BG") end } } - end - } + dlg:endpanel() dlg:endbox() -- ---------------------------------------------------------------------------- - -- Tab 3: Environment Engine + -- Tab 4: Environment -- ---------------------------------------------------------------------------- dlg:tab { text = "Environment" } - local map_info_text = "No map loaded." - if app.map then - map_info_text = string.format("Map: %s\nSize: %dx%d\nTiles: %d", - app.map.name or "Untitled", - app.map.width, app.map.height, - app.map.tileCount or 0 - ) - end - - dlg:label { id = "lbl_map_info", text = map_info_text } - - dlg:separator() - - dlg:button { text = "Inspect Selection", onclick = function() - local sel = app.selection - if not sel or sel.isEmpty then - app.alert("Selection is empty.\nSelect some tiles in the map editor first.") - else - local msg = string.format("Selected Tiles: %d\nBounds: (%d, %d, %d) to (%d, %d, %d)", - sel.size, - sel.minPosition.x, sel.minPosition.y, sel.minPosition.z, - sel.maxPosition.x, sel.maxPosition.y, sel.maxPosition.z + dlg:panel({ bgcolor = "#EDF2F7", padding = 15, margin = 10, expand = true }) + local map_info_text = "No map loaded." + if app.map then + map_info_text = string.format("Current Map: %s\nDimensions: %dx%d\nTotal Tiles: %d", + app.map.name or "Untitled", + app.map.width, app.map.height, + app.map.tileCount or 0 ) - app.alert(msg) - end - end } - - dlg:button { text = "Transaction Demo (Add Sparkles)", onclick = function() - if not app.map then - app.alert("No map loaded!") - return - end - - local sel = app.selection - if sel.isEmpty then - app.alert("Select area first to spawn sparkles (ID 2014)!") - return end - - app.transaction("Demo Sparkles", function() - for _, tile in ipairs(sel.tiles) do - -- Add magic effect / sparkles - tile:addItem(2785, 1) + dlg:label { id = "lbl_map_info", text = map_info_text, font_size = 10 } + dlg:endpanel() + + dlg:box({ orient = "horizontal", align = "center" }) + dlg:button { text = "Inspect Selection", width = 150, onclick = function() + local sel = app.selection + if not sel or sel.isEmpty then + app.alert("Selection is empty.") + else + app.alert(string.format("Selected Tiles: %d", sel.size)) end - end) - app.alert("Added blueberry bushes to " .. sel.size .. " tiles.") - end } + end } - dlg:separator() - dlg:label { text = "Map Overlays (Scripting API)" } - dlg:button { text = "Toggle 'Demo Overlay'", onclick = function() - local overlay_id = "demo_overlay" - app.mapView:addOverlay(overlay_id, { - ondraw = function(ctx) - ctx:rect { x = 10, y = 10, w = 200, h = 50, color = { r = 0, g = 0, b = 0, a = 100 }, screen = true, filled = true } - ctx:text { x = 20, y = 25, text = "Demo Overlay Active", color = { r = 255, g = 255, b = 255 }, screen = true } + dlg:button { text = "Add Blueberry Bushes", bgcolor = "#2B6CB0", fgcolor = "white", width = 150, hover = { bgcolor = "#2C5282" }, onclick = function() + if not app.map or app.selection.isEmpty then + app.alert("Select map area first!") + return end - }) - app.alert("Overlay added. Move map to see updates if it was drawing world coords.") - end } + app.transaction("Add Bushes", function() + for _, tile in ipairs(app.selection.tiles) do + tile:addItem(2785, 1) + end + end) + end } + dlg:endbox() -- ---------------------------------------------------------------------------- - -- Tab 4: System & Network + -- Tab 5: System -- ---------------------------------------------------------------------------- dlg:tab { text = "System" } - dlg:label { text = "Editor Version: " .. app.version } - - dlg:separator() - - -- DOCKABLE TOGGLE - local dock_btn_text = is_dockable and "Reopen as Floating Window" or "Reopen as Dockable Window" - dlg:button { text = dock_btn_text, onclick = function(d) - d:close() - createAndShowDialog(not is_dockable) - end } - - dlg:separator() - - dlg:label { text = "HTTP Requests:" } - dlg:label { text = "Result will appear here...", id = "lbl_http_res" } - - dlg:button { text = "Get Random Quote (HTTP JSON)", onclick = function(d) - if not http then - d:modify { lbl_http_res = { text = "Error: HTTP module not available." } } - return - end - - d:modify { lbl_http_res = { text = "Fetching..." } } - - local res = http.get("https://dummyjson.com/quotes/random") - - if res.ok then - if json and json.decode then - local status, data = pcall(json.decode, res.body) - if status and data then - local quote = data.quote or "No quote found" - local author = data.author or "Unknown" - local fmt = string.format('"%s" - %s', quote, author) - d:modify { lbl_http_res = { text = fmt } } - else - d:modify { lbl_http_res = { text = "Invalid JSON: " .. string.sub(res.body, 1, 50) } } - end + dlg:box({ orient = "vertical", padding = 10 }) + dlg:label { text = "Application Version", font_weight = "bold" } + dlg:panel({ bgcolor = Color.lightGray, padding = 5 }) + dlg:label { text = app.version, align = "center" } + dlg:endpanel() + + dlg:separator() + + local dock_btn_text = is_dockable and "Switch to Floating Window" or "Switch to Dockable Side-Panel" + dlg:button { text = dock_btn_text, expand = true, onclick = function(d) + d:close() + createAndShowDialog(not is_dockable) + end } + + dlg:separator() + + dlg:label { text = "Network Demo:", font_weight = "bold" } + dlg:panel({ id = "http_box", bgcolor = "#FFF", height = 60, padding = 10, expand = true }) + dlg:label { text = "Result will appear here...", id = "lbl_http_res", align = "center", expand = true } + dlg:endpanel() + + dlg:button { text = "Fetch Random Quote", bgcolor = Color.blue, fgcolor = "white", hover = { bgcolor = Color.darken(Color.blue, 10) }, expand = true, onclick = function(d) + if not http then return end + d:modify { lbl_http_res = { text = "Fetching..." }, http_box = { bgcolor = "#E2E8F0" } } + local res = http.get("https://dummyjson.com/quotes/random") + if res.ok and json then + local data = json.decode(res.body) + d:modify { + lbl_http_res = { text = string.format('"%s"', data.quote or "?") }, + http_box = { bgcolor = Color.white } + } else - d:modify { lbl_http_res = { text = "Raw: " .. string.sub(res.body, 1, 50) .. "..." } } + d:modify { lbl_http_res = { text = "Request failed" } } end - else - d:modify { lbl_http_res = { text = "HTTP Error: " .. (res.error or "Unknown") } } - end - end } - - dlg:separator() - - dlg:button { text = "Save Timestamp to Storage", onclick = function() - local store = app.storage("hello_world_demo") - local data = store:load() or {} - data.last_run = os.time() - store:save(data) - app.alert("Saved timestamp: " .. data.last_run) - end } + end } + dlg:endbox() dlg:endtabs() + + -- Footer dlg:separator() + dlg:button { text = "CLOSE DEMO", align = "center", margin = 5, onclick = function(d) d:close() end } - dlg:button { text = "Close Demo", onclick = function(d) d:close() end } dlg:show { wait = false } end From c13cb285681b051789f2ee8222b01b2d0bfa69ce Mon Sep 17 00:00:00 2001 From: michyaraque Date: Wed, 28 Jan 2026 20:08:31 +0100 Subject: [PATCH 08/10] fix: ignore linter.lua from lua scripts discovering --- source/lua/lua_script_manager.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/lua/lua_script_manager.cpp b/source/lua/lua_script_manager.cpp index ca3be8025..a772b781c 100644 --- a/source/lua/lua_script_manager.cpp +++ b/source/lua/lua_script_manager.cpp @@ -566,6 +566,11 @@ void LuaScriptManager::scanDirectory(const std::string& directory) { // Then, scan for standalone .lua files cont = dir.GetFirst(&name, "*.lua", wxDIR_FILES); while (cont) { + if (name == "linter.lua") { + cont = dir.GetNext(&name); + continue; + } + std::string filepath = directory + sep + name.ToStdString(); auto script = std::make_unique(filepath); From 3e5956ea6ab41762544cae8dbdf09bfc4fc746c9 Mon Sep 17 00:00:00 2001 From: michyaraque Date: Wed, 28 Jan 2026 20:08:51 +0100 Subject: [PATCH 09/10] chore: bump version from 4.2.0 > 4.3.0 --- source/definitions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/definitions.h b/source/definitions.h index e59cc8dac..86458e1fc 100644 --- a/source/definitions.h +++ b/source/definitions.h @@ -24,7 +24,7 @@ // Version info // xxyyzzt (major, minor, subversion) #define __RME_VERSION_MAJOR__ 4 -#define __RME_VERSION_MINOR__ 2 +#define __RME_VERSION_MINOR__ 3 #define __RME_SUBVERSION__ 0 #define __LIVE_NET_VERSION__ 5 From b7744baa2aea92db8163e4292aa7cd7b875ff0d3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 28 Jan 2026 19:09:24 +0000 Subject: [PATCH 10/10] Code format - (Clang-format) --- source/lua/lua_api_color.cpp | 2 +- source/lua/lua_dialog.cpp | 98 +++++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/source/lua/lua_api_color.cpp b/source/lua/lua_api_color.cpp index 5d4f02639..d28983f53 100644 --- a/source/lua/lua_api_color.cpp +++ b/source/lua/lua_api_color.cpp @@ -37,7 +37,7 @@ namespace LuaAPI { unsigned long value = 0; std::string h = hex[0] == '#' ? hex.substr(1) : hex; if (h.length() == 3) { - h = {h[0], h[0], h[1], h[1], h[2], h[2]}; + h = { h[0], h[0], h[1], h[1], h[2], h[2] }; } try { value = std::stoul(h, nullptr, 16); diff --git a/source/lua/lua_dialog.cpp b/source/lua/lua_dialog.cpp index c848760d6..ccade5e69 100644 --- a/source/lua/lua_dialog.cpp +++ b/source/lua/lua_dialog.cpp @@ -45,10 +45,8 @@ using namespace std::string_literals; class CustomButton : public wxControl { public: - CustomButton(wxWindow* parent, wxWindowID id, const wxString& label, - const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0) - : wxControl(parent, id, pos, size, style | wxBORDER_NONE), m_label(label) - { + CustomButton(wxWindow* parent, wxWindowID id, const wxString& label, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0) : + wxControl(parent, id, pos, size, style | wxBORDER_NONE), m_label(label) { SetBackgroundStyle(wxBG_STYLE_PAINT); } @@ -102,8 +100,12 @@ class CustomButton : public wxControl { fg = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); } else { if (m_hasHover && m_hovered) { - if (m_hoverBg.IsOk()) bg = m_hoverBg; - if (m_hoverFg.IsOk()) fg = m_hoverFg; + if (m_hoverBg.IsOk()) { + bg = m_hoverBg; + } + if (m_hoverFg.IsOk()) { + fg = m_hoverFg; + } } if (m_pressed) { @@ -135,9 +137,22 @@ class CustomButton : public wxControl { } } - void OnEnter(wxMouseEvent& e) { m_hovered = true; Refresh(); e.Skip(); } - void OnLeave(wxMouseEvent& e) { m_hovered = false; m_pressed = false; Refresh(); e.Skip(); } - void OnDown(wxMouseEvent& e) { m_pressed = true; Refresh(); e.Skip(); } + void OnEnter(wxMouseEvent& e) { + m_hovered = true; + Refresh(); + e.Skip(); + } + void OnLeave(wxMouseEvent& e) { + m_hovered = false; + m_pressed = false; + Refresh(); + e.Skip(); + } + void OnDown(wxMouseEvent& e) { + m_pressed = true; + Refresh(); + e.Skip(); + } void OnUp(wxMouseEvent& e) { if (m_pressed) { m_pressed = false; @@ -152,11 +167,11 @@ class CustomButton : public wxControl { }; BEGIN_EVENT_TABLE(CustomButton, wxControl) - EVT_PAINT(CustomButton::OnPaint) - EVT_ENTER_WINDOW(CustomButton::OnEnter) - EVT_LEAVE_WINDOW(CustomButton::OnLeave) - EVT_LEFT_DOWN(CustomButton::OnDown) - EVT_LEFT_UP(CustomButton::OnUp) +EVT_PAINT(CustomButton::OnPaint) +EVT_ENTER_WINDOW(CustomButton::OnEnter) +EVT_LEAVE_WINDOW(CustomButton::OnLeave) +EVT_LEFT_DOWN(CustomButton::OnDown) +EVT_LEFT_UP(CustomButton::OnUp) END_EVENT_TABLE() // Specialized Canvas for Lua Dialogs @@ -509,7 +524,9 @@ LuaDialog* LuaDialog::box(sol::table options) { std::string orient = options.get_or(std::string("orient"), "vertical"s); std::string label = options.get_or(std::string("label"), ""s); bool expand = options.get_or("expand", label.empty()); // Defaults to true only if no label (backwards compat) - if (options["expand"].valid()) expand = options.get("expand"); + if (options["expand"].valid()) { + expand = options.get("expand"); + } wxSizer* sizer; if (!label.empty()) { @@ -919,7 +936,9 @@ LuaDialog* LuaDialog::button(sol::table options) { if (useCustom) { CustomButton* btn = new CustomButton(getParentForWidget(), wxID_ANY, wxString(text)); - if (rounded) btn->SetRounded(true); + if (rounded) { + btn->SetRounded(true); + } // Anti-aliasing corner fix: check if we are inside a StaticBoxSizer with custom color wxStaticBoxSizer* sbs = wxDynamicCast(currentRowSizer, wxStaticBoxSizer); @@ -944,8 +963,12 @@ LuaDialog* LuaDialog::button(sol::table options) { }; wxColour hBg, hFg; - if (hoverOpts["bgcolor"].valid()) hBg = parseColor(hoverOpts["bgcolor"]); - if (hoverOpts["fgcolor"].valid()) hFg = parseColor(hoverOpts["fgcolor"]); + if (hoverOpts["bgcolor"].valid()) { + hBg = parseColor(hoverOpts["bgcolor"]); + } + if (hoverOpts["fgcolor"].valid()) { + hFg = parseColor(hoverOpts["fgcolor"]); + } btn->SetHoverColors(hBg, hFg); } finalWidget = btn; @@ -2803,15 +2826,23 @@ int LuaDialog::getSizerFlags(sol::table options, int defaultFlags) { int flags = defaultFlags; if (options["align"].valid()) { std::string align = options.get("align"); - if (align == "center") flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_LEFT) | wxALIGN_CENTER_HORIZONTAL; - else if (align == "right") flags = (flags & ~wxALIGN_LEFT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_RIGHT; - else if (align == "left") flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_LEFT; + if (align == "center") { + flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_LEFT) | wxALIGN_CENTER_HORIZONTAL; + } else if (align == "right") { + flags = (flags & ~wxALIGN_LEFT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_RIGHT; + } else if (align == "left") { + flags = (flags & ~wxALIGN_RIGHT & ~wxALIGN_CENTER_HORIZONTAL) | wxALIGN_LEFT; + } } if (options["valign"].valid()) { std::string valign = options.get("valign"); - if (valign == "center") flags = (flags & ~wxALIGN_TOP & ~wxALIGN_BOTTOM) | wxALIGN_CENTER_VERTICAL; - else if (valign == "top") flags = (flags & ~wxALIGN_BOTTOM & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_TOP; - else if (valign == "bottom") flags = (flags & ~wxALIGN_TOP & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_BOTTOM; + if (valign == "center") { + flags = (flags & ~wxALIGN_TOP & ~wxALIGN_BOTTOM) | wxALIGN_CENTER_VERTICAL; + } else if (valign == "top") { + flags = (flags & ~wxALIGN_BOTTOM & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_TOP; + } else if (valign == "bottom") { + flags = (flags & ~wxALIGN_TOP & ~wxALIGN_CENTER_VERTICAL) | wxALIGN_BOTTOM; + } } if (options.get_or("expand", false)) { flags |= wxEXPAND; @@ -2849,7 +2880,7 @@ void LuaDialog::applyCommonOptions(wxWindow* widget, sol::table options) { hexValue = std::stoul(colorStr.substr(1), nullptr, 16); } else if (colorStr.length() == 4) { char r = colorStr[1], g = colorStr[2], b = colorStr[3]; - std::string expanded = {r, r, g, g, b, b}; + std::string expanded = { r, r, g, g, b, b }; hexValue = std::stoul(expanded, nullptr, 16); } return wxColour((hexValue >> 16) & 0xFF, (hexValue >> 8) & 0xFF, hexValue & 0xFF); @@ -2880,14 +2911,21 @@ void LuaDialog::applyCommonOptions(wxWindow* widget, sol::table options) { } if (options["font_weight"].valid()) { std::string weight = options.get("font_weight"); - if (weight == "bold") font.SetWeight(wxFONTWEIGHT_BOLD); - else if (weight == "light") font.SetWeight(wxFONTWEIGHT_LIGHT); - else font.SetWeight(wxFONTWEIGHT_NORMAL); + if (weight == "bold") { + font.SetWeight(wxFONTWEIGHT_BOLD); + } else if (weight == "light") { + font.SetWeight(wxFONTWEIGHT_LIGHT); + } else { + font.SetWeight(wxFONTWEIGHT_NORMAL); + } } if (options["font_style"].valid()) { std::string style = options.get("font_style"); - if (style == "italic") font.SetStyle(wxFONTSTYLE_ITALIC); - else font.SetStyle(wxFONTSTYLE_NORMAL); + if (style == "italic") { + font.SetStyle(wxFONTSTYLE_ITALIC); + } else { + font.SetStyle(wxFONTSTYLE_NORMAL); + } } widget->SetFont(font); }