tibia-rme/source/lua/lua_dialog.cpp
Humberto Gontijo c136a75263
fix: make macOS build with modern sol2 and wxGL (#201)
Bug Fixes:
- Improved Lua scripting: Lua-visible nil is now consistent for missing/empty values across storage, JSON parsing, item/map/tile APIs, dialogs, app properties, and overlay hover.
- Improved macOS graphics reliability (OpenGL context/linking) and safer background/thread handling for streaming and SQLite bootstrap.
- Fixed client asset loading paths and improved SQLite migration input paths.

UI Improvements:
- Replaced “Missing …” modal dialogs with inline text boxes.
- Made “Client directory” editable; added macOS app-picker browse flow.

CI:
- Added compile-scoped macOS builds via reusable workflow and a new build-macos job.
2026-07-29 19:44:25 -03:00

3048 lines
89 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "lua_dialog.h"
#include "lua_api_image.h"
#include "../gui.h"
#include "../items.h"
#include "../map_display.h"
#include "../map_drawer.h"
#include "../editor.h"
#include <wx/msgdlg.h>
#include "../common_windows.h"
#include "../find_item_window.h"
#include "../dcbutton.h"
#include <wx/statline.h>
#include <wx/valgen.h>
#include <wx/aui/aui.h>
#include <wx/statbmp.h>
#include <wx/listctrl.h>
#include <wx/imaglist.h>
#include <wx/menu.h>
#include <wx/utils.h>
#ifdef __WXMSW__
#include <commctrl.h>
#endif
#include <wx/graphics.h>
#include <wx/dcbuffer.h>
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 {
wxScreenDC dc;
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;
wxColour 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();
std::unique_ptr<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 && m_hoverBg.IsOk()) {
bg = m_hoverBg;
}
if (m_hasHover && m_hovered && 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;
wxDouble th;
gc->GetTextExtent(m_label, &tw, &th, nullptr, nullptr);
gc->DrawText(m_label, (w - tw) / 2.0, (h - th) / 2.0);
}
}
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
class MapPreviewCanvas : public MapCanvas {
public:
MapPreviewCanvas(wxWindow* parent, Editor &editor) :
MapCanvas(nullptr, editor, nullptr) {
ChangeFloor(7);
MapPreviewCanvas::SetZoom(1.0);
}
~MapPreviewCanvas() override = default;
// Overrides to decouple from MapWindow
void SetZoom(double value) override {
if (value < 0.125) {
value = 0.125;
}
if (value > 25.00) {
value = 25.0;
}
MapCanvas::SetZoom(value);
wxGLCanvas::Refresh();
}
void GetViewBox(int* view_scroll_x, int* view_scroll_y, int* screensize_x, int* screensize_y) const override {
wxSize size = GetClientSize();
*screensize_x = size.GetWidth();
*screensize_y = size.GetHeight();
*view_scroll_x = view_x;
*view_scroll_y = view_y;
}
void ScreenToMap(int screen_x, int screen_y, int* map_x, int* map_y) override {
screen_x *= GetContentScaleFactor();
screen_y *= GetContentScaleFactor();
if (screen_x < 0) {
*map_x = (view_x + screen_x) / 32;
} else {
*map_x = int(view_x + (screen_x * GetZoom())) / 32;
}
if (screen_y < 0) {
*map_y = (view_y + screen_y) / 32;
} else {
*map_y = int(view_y + (screen_y * GetZoom())) / 32;
}
if (GetFloor() <= 7) {
*map_x += 7 - GetFloor();
*map_y += 7 - GetFloor();
}
}
void GetScreenCenter(int* map_x, int* map_y) override {
wxSize size = GetClientSize();
ScreenToMap(size.GetWidth() / 2, size.GetHeight() / 2, map_x, map_y);
}
void SetPosition(int x, int y, int z) {
wxSize size = GetClientSize();
int width = size.GetWidth();
int height = size.GetHeight();
// If not yet laid out, use a default size to avoid bad coordinates
if (width <= 0) {
width = 400;
}
if (height <= 0) {
height = 300;
}
view_x = (x * 32) - (width * GetZoom()) / 2;
view_y = (y * 32) - (height * GetZoom()) / 2;
ChangeFloor(z);
Refresh();
}
void UpdatePositionStatus(int = -1, int = -1) override {
// No-op: preview canvases do not update the status bar
}
void UpdateZoomStatus() override {
// No-op: preview canvases do not update the status bar
}
void Refresh() override {
wxGLCanvas::Refresh();
}
void OnMouseMove(wxMouseEvent &event) {
event.Skip();
}
void OnWheel(wxMouseEvent &event) {
// Do nothing - disabled to prevent confusion and potential issues
}
void OnGainMouse(wxMouseEvent &event) {
Refresh();
}
void OnLoseMouse(wxMouseEvent &event) {
Refresh();
}
void OnMouseLeftClick(wxMouseEvent &event) {
SetFocus();
}
void OnMouseLeftRelease(wxMouseEvent &event) {
// No-op: preview canvas does not handle this mouse event
}
void OnMouseRightClick(wxMouseEvent &event) {
// No-op: preview canvas does not handle this mouse event
}
void OnMouseRightRelease(wxMouseEvent &event) {
// No-op: preview canvas does not handle this mouse event
}
void OnMouseCenterClick(wxMouseEvent &event) {
// No-op: preview canvas does not handle this mouse event
}
void OnMouseCenterRelease(wxMouseEvent &event) {
// No-op: preview canvas does not handle this mouse event
}
void SetViewSize(int w, int h) {
client_w = w;
client_h = h;
// Auto-resize the window to fit the new client dimensions
auto tile_pixel_size = static_cast<int>(32 / GetZoom());
int req_w = w * tile_pixel_size;
int req_h = h * tile_pixel_size;
// Add some padding for the frame borders if needed, but SetClientSize should handle content area
// We set the MinSize of the canvas to enforce this size
SetMinSize(wxSize(req_w, req_h));
// Attempt to resize the parent dialog to fit the new canvas size
wxWindow* parent = GetParent();
if (parent) {
parent->Fit();
}
Refresh();
}
void SetLight([[maybe_unused]] bool on) {
Refresh();
}
void SyncView() {
Editor* current_editor = g_gui.GetCurrentEditor();
if (!current_editor) {
return;
}
MapTab* tab = g_gui.GetCurrentMapTab();
if (tab) {
int cx;
int cy;
tab->GetCanvas()->GetScreenCenter(&cx, &cy);
SetPosition(cx, cy, tab->GetCanvas()->GetFloor());
SetZoom(tab->GetCanvas()->GetZoom());
}
}
int GetMapX() {
wxSize size = GetClientSize();
int width = size.GetWidth() > 0 ? size.GetWidth() : 400;
return (view_x + (width * (double)GetZoom()) / 2.0) / 32;
}
int GetMapY() {
wxSize size = GetClientSize();
int height = size.GetHeight() > 0 ? size.GetHeight() : 300;
return (view_y + (height * (double)GetZoom()) / 2.0) / 32;
}
int GetClientWidth() const {
return client_w;
}
int GetClientHeight() const {
return client_h;
}
private:
int view_x = 0;
int view_y = 0;
int client_w = 25;
int client_h = 25;
DECLARE_EVENT_TABLE()
};
BEGIN_EVENT_TABLE(MapPreviewCanvas, MapCanvas)
EVT_MOTION(MapPreviewCanvas::OnMouseMove)
EVT_MOUSEWHEEL(MapPreviewCanvas::OnWheel)
EVT_ENTER_WINDOW(MapPreviewCanvas::OnGainMouse)
EVT_LEAVE_WINDOW(MapPreviewCanvas::OnLoseMouse)
EVT_LEFT_DOWN(MapPreviewCanvas::OnMouseLeftClick)
EVT_LEFT_UP(MapPreviewCanvas::OnMouseLeftRelease)
EVT_RIGHT_DOWN(MapPreviewCanvas::OnMouseRightClick)
EVT_RIGHT_UP(MapPreviewCanvas::OnMouseRightRelease)
EVT_MIDDLE_DOWN(MapPreviewCanvas::OnMouseCenterClick)
EVT_MIDDLE_UP(MapPreviewCanvas::OnMouseCenterRelease)
EVT_PAINT(MapCanvas::OnPaint)
END_EVENT_TABLE()
BEGIN_EVENT_TABLE(LuaDialog, wxDialog)
EVT_CLOSE(LuaDialog::OnClose)
END_EVENT_TABLE()
LuaDialog::LuaDialog(const std::string &title, sol::this_state ts) :
wxDialog(g_gui.root, wxID_ANY, wxString(title), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
lua(ts) {
createLayout();
}
// Overload constructor to handle options
LuaDialog::LuaDialog(sol::table options, sol::this_state ts) :
wxDialog(g_gui.root, wxID_ANY, wxString(options.get_or(std::string("title"), "Script Dialog"s)), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | (options.get_or(std::string("resizable"), true) ? wxRESIZE_BORDER : 0) | (options.get_or(std::string("topmost"), false) ? wxSTAY_ON_TOP : 0)),
lua(ts) {
reqWidth = options.get_or(std::string("width"), -1);
reqHeight = options.get_or(std::string("height"), -1);
reqX = options.get_or(std::string("x"), -1);
reqY = options.get_or(std::string("y"), -1);
applyInitialSizeAndPosition();
setupDockablePanel(options);
if (options["onclose"].valid()) {
oncloseCallback = options["onclose"];
}
createLayout();
}
void LuaDialog::applyInitialSizeAndPosition() {
if (reqWidth != -1 || reqHeight != -1) {
SetMinSize(wxSize(reqWidth != -1 ? reqWidth : 150, reqHeight != -1 ? reqHeight : 100));
SetSize(reqX != -1 ? reqX : -1, reqY != -1 ? reqY : -1, reqWidth, reqHeight);
} else if (reqX != -1 || reqY != -1) {
SetPosition(wxPoint(reqX != -1 ? reqX : GetPosition().x, reqY != -1 ? reqY : GetPosition().y));
}
}
void LuaDialog::setupDockablePanel(sol::table &options) {
if (!options.get_or(std::string("dockable"), false)) {
return;
}
dockPanel = new wxPanel(g_gui.root, wxID_ANY);
wxAuiPaneInfo info;
std::string title = options.get_or(std::string("title"), "Script Dialog"s);
std::string id = options.get_or(std::string("id"), title);
info.Name(id);
info.Caption(title);
info.Right().Layer(1).Position(1).CloseButton(true).MaximizeButton(true);
int minW = options.get_or(std::string("min_width"), reqWidth != -1 ? reqWidth : 200);
int minH = options.get_or(std::string("min_height"), reqHeight != -1 ? reqHeight : 150);
info.MinSize(wxSize(minW, minH));
if (reqWidth != -1 || reqHeight != -1) {
info.BestSize(wxSize(reqWidth != -1 ? reqWidth : 300, reqHeight != -1 ? reqHeight : 200));
} else {
info.BestSize(wxSize(300, 200));
}
info.Dockable(true).Floatable(true);
g_gui.aui_manager->AddPane(dockPanel, info);
g_gui.aui_manager->Update();
}
LuaDialog::~LuaDialog() {
while (hotkeySuspendCount > 0) {
resumeHotkeys();
}
if (dockPanel) {
if (g_gui.aui_manager) {
wxAuiPaneInfo &info = g_gui.aui_manager->GetPane(dockPanel);
if (info.IsOk()) {
g_gui.aui_manager->DetachPane(dockPanel);
g_gui.aui_manager->Update();
}
}
if (dockPanel->GetParent()) {
dockPanel->Destroy();
}
dockPanel = nullptr;
}
}
void LuaDialog::createLayout() {
mainSizer = new wxBoxSizer(wxVERTICAL);
if (dockPanel) {
dockPanel->SetSizer(mainSizer);
} else {
SetSizer(mainSizer);
}
sizerStack.push(mainSizer);
}
void LuaDialog::ensureRowSizer() {
if (!currentRowSizer) {
currentRowSizer = new wxBoxSizer(wxHORIZONTAL);
getSizerForWidget()->Add(currentRowSizer, 0, wxEXPAND | wxALL, 5);
}
}
void LuaDialog::finishCurrentRow() {
currentRowSizer = nullptr;
}
wxWindow* LuaDialog::getParentForWidget() {
if (!panelStack.empty()) {
return panelStack.top();
}
if (currentTabPanel) {
return currentTabPanel;
}
if (dockPanel) {
return dockPanel;
}
return this;
}
wxSizer* LuaDialog::getSizerForWidget() {
if (!sizerStack.empty()) {
return sizerStack.top();
}
// Fallback mechanism (should shouldn't happen if managed correctly)
if (currentTabSizer) {
return currentTabSizer;
}
return mainSizer;
}
LuaDialog* LuaDialog::wrap(sol::table options) {
finishCurrentRow();
rowSizerStack.push(currentRowSizer);
currentRowSizer = nullptr;
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;
}
LuaDialog* LuaDialog::endwrap() {
finishCurrentRow();
if (!sizerStack.empty() && sizerStack.top() != mainSizer && (!currentTabSizer || sizerStack.top() != currentTabSizer)) {
sizerStack.pop();
}
if (!rowSizerStack.empty()) {
currentRowSizer = rowSizerStack.top();
rowSizerStack.pop();
}
return this;
}
LuaDialog* LuaDialog::box(sol::table options) {
finishCurrentRow();
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<bool>("expand");
}
wxSizer* sizer;
if (!label.empty()) {
// Static box sizer
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 {
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<wxStaticBoxSizer*>(sizer);
sbSizer->GetStaticBox()->SetMaxSize(sz);
}
}
rowSizerStack.push(currentRowSizer);
currentRowSizer = nullptr;
sizerStack.push(sizer);
return this;
}
LuaDialog* LuaDialog::endbox() {
finishCurrentRow();
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"), fmt::format("panel_{}", 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<std::string>("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;
}
LuaDialog* LuaDialog::label(sol::table options) {
ensureRowSizer();
std::string text = options.get_or(std::string("text"), "label"s);
std::string id = options.get_or(std::string("id"), ""s);
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(text));
currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
if (!id.empty()) {
LuaDialogWidget widget;
widget.id = id;
widget.type = "label";
widget.widget = label;
widgets.push_back(widget);
}
applyCommonOptions(label, options);
return this;
}
LuaDialog* LuaDialog::mapCanvas(sol::table options) {
ensureRowSizer();
Editor* editor = g_gui.GetCurrentEditor();
if (!editor) {
wxMessageBox("You must have a map open to use the Map Canvas widget.", "Error", wxOK | wxICON_ERROR);
return this;
}
std::string id = options.get_or(std::string("id"), fmt::format("map_c_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
// Create the canvas
auto canvas = new MapPreviewCanvas(getParentForWidget(), *editor);
// Smaller default size to keep the window compact
canvas->SetMinSize(wxSize(200, 150));
// Default to center of map start
canvas->SetPosition(1000, 1000, 7);
currentRowSizer->Add(canvas, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options));
if (!id.empty()) {
LuaDialogWidget widget;
widget.id = id;
widget.type = "mapCanvas";
widget.widget = canvas;
widgets.push_back(widget);
}
applyCommonOptions(canvas, options);
return this;
}
LuaDialog* LuaDialog::input(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("input_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
std::string text = options.get_or(std::string("text"), ""s);
bool focus = options.get_or(std::string("focus"), false);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
wxTextCtrl* input = new wxTextCtrl(getParentForWidget(), wxID_ANY, wxString(text), wxDefaultPosition, wxSize(150, -1));
currentRowSizer->Add(input, 1, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
input->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent &event) {
suspendHotkeys();
event.Skip();
});
input->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &event) {
resumeHotkeys();
event.Skip();
});
if (focus) {
input->SetFocus();
}
LuaDialogWidget widget;
widget.id = id;
widget.type = "input";
widget.widget = input;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
// Store initial value
values[id] = sol::make_object(lua, text);
// Bind change event
input->Bind(wxEVT_TEXT, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
applyCommonOptions(input, options);
return this;
}
LuaDialog* LuaDialog::number(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("number_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
double value = options.get_or(std::string("text"), options.get_or(std::string("value"), 0.0));
int decimals = options.get_or(std::string("decimals"), 0);
double minVal = options.get_or(std::string("min"), -999999.0);
double maxVal = options.get_or(std::string("max"), 999999.0);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
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, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "number";
widget.widget = spin;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, value);
spin->Bind(wxEVT_SPINCTRLDOUBLE, [this, id](wxSpinDoubleEvent &) {
onWidgetChange(id);
});
applyCommonOptions(spin, options);
return this;
}
LuaDialog* LuaDialog::slider(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("slider_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
int value = options.get_or(std::string("value"), 0);
int minVal = options.get_or(std::string("min"), 0);
int maxVal = options.get_or(std::string("max"), 100);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
wxSlider* slider = new wxSlider(getParentForWidget(), wxID_ANY, value, minVal, maxVal, wxDefaultPosition, wxSize(150, -1));
currentRowSizer->Add(slider, 1, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "slider";
widget.widget = slider;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, value);
slider->Bind(wxEVT_SLIDER, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
applyCommonOptions(slider, options);
return this;
}
LuaDialog* LuaDialog::check(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("check_{}", widgets.size()));
std::string text = options.get_or(std::string("text"), ""s);
bool selected = options.get_or(std::string("selected"), false);
wxCheckBox* checkbox = new wxCheckBox(getParentForWidget(), wxID_ANY, wxString(text));
checkbox->SetValue(selected);
currentRowSizer->Add(checkbox, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "check";
widget.widget = checkbox;
if (options["onclick"].valid()) {
widget.onclick = options["onclick"];
}
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, selected);
checkbox->Bind(wxEVT_CHECKBOX, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
applyCommonOptions(checkbox, options);
return this;
}
LuaDialog* LuaDialog::radio(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("radio_{}", widgets.size()));
std::string text = options.get_or(std::string("text"), ""s);
bool selected = options.get_or(std::string("selected"), false);
wxRadioButton* radio = new wxRadioButton(getParentForWidget(), wxID_ANY, wxString(text));
radio->SetValue(selected);
currentRowSizer->Add(radio, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "radio";
widget.widget = radio;
if (options["onclick"].valid()) {
widget.onclick = options["onclick"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, selected);
radio->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
applyCommonOptions(radio, options);
return this;
}
LuaDialog* LuaDialog::combobox(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("combobox_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
std::string selected = options.get_or(std::string("option"), ""s);
wxArrayString choices;
if (options["options"].valid()) {
sol::table opts = options["options"];
for (size_t i = 1; i <= opts.size(); ++i) {
if (opts[i].valid()) {
choices.Add(wxString(opts[i].get<std::string>()));
}
}
}
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
wxChoice* choice = new wxChoice(getParentForWidget(), wxID_ANY, wxDefaultPosition, wxSize(150, -1), choices);
if (!selected.empty()) {
int idx = choice->FindString(wxString(selected));
if (idx != wxNOT_FOUND) {
choice->SetSelection(idx);
}
} else if (choices.GetCount() > 0) {
choice->SetSelection(0);
selected = choices[0].ToStdString();
}
currentRowSizer->Add(choice, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "combobox";
widget.widget = choice;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, selected);
choice->Bind(wxEVT_CHOICE, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
applyCommonOptions(choice, options);
return this;
}
LuaDialog* LuaDialog::button(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("button_{}", widgets.size()));
std::string text = options.get_or(std::string("text"), "Button"s);
bool rounded = options.get_or("rounded", false);
bool hasHover = options["hover"].valid();
bool hasCustomColor = options["bgcolor"].valid() || options["fgcolor"].valid();
bool useCustom = rounded || hasHover || hasCustomColor;
wxWindow* finalWidget = nullptr;
if (useCustom) {
auto 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>()) {
sol::table c = colorObj.as<sol::table>();
return wxColour(c.get_or("r", 1), c.get_or("g", 2), c.get_or("b", 3));
} else if (colorObj.is<std::string>()) {
std::string s = colorObj.as<std::string>();
return (s.size() > 0 && s[0] == '#') ? wxColour(s) : wxColour(s);
}
return wxColour();
};
wxColour hBg;
wxColour 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 = finalWidget;
if (options["onclick"].valid()) {
widget.onclick = options["onclick"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, false);
finalWidget->Bind(wxEVT_BUTTON, [this, id](wxCommandEvent &) {
onButtonClick(id);
});
applyCommonOptions(finalWidget, options);
return this;
}
LuaDialog* LuaDialog::color(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("color_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
wxColour defaultColor = *wxBLACK;
if (options["color"].valid()) {
sol::table c = options["color"];
int r = c.get_or(std::string("red"), c.get_or(1, 0));
int g = c.get_or(std::string("green"), c.get_or(2, 0));
int b = c.get_or(std::string("blue"), c.get_or(3, 0));
defaultColor = wxColour(r, g, b);
}
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
wxColourPickerCtrl* picker = new wxColourPickerCtrl(getParentForWidget(), wxID_ANY, defaultColor);
currentRowSizer->Add(picker, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "color";
widget.widget = picker;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
// Store color as table
sol::table colorTable = lua.create_table();
colorTable["red"] = defaultColor.Red();
colorTable["green"] = defaultColor.Green();
colorTable["blue"] = defaultColor.Blue();
values[id] = colorTable;
picker->Bind(wxEVT_COLOURPICKER_CHANGED, [this, id](wxColourPickerEvent &) {
onWidgetChange(id);
});
applyCommonOptions(picker, options);
return this;
}
///////////////////////////////////////////////////////////////////////////////
// LuaDialogListBox - Custom list widget for LuaDialog
///////////////////////////////////////////////////////////////////////////////
struct LuaListBoxItem {
std::string text;
int iconId = 0;
LuaAPI::LuaImage customImage;
std::string tooltip;
};
class LuaDialogListBox : public wxVListBox {
public:
LuaDialogListBox(wxWindow* parent, wxWindowID id, const wxSize &size) :
wxVListBox(parent, id, wxDefaultPosition, size, wxLB_SINGLE) {
Bind(wxEVT_LEFT_DOWN, &LuaDialogListBox::OnLeftDown, this);
Bind(wxEVT_LEFT_DCLICK, &LuaDialogListBox::OnLeftDouble, this);
Bind(wxEVT_MOTION, &LuaDialogListBox::OnMotion, this);
Bind(wxEVT_LEAVE_WINDOW, &LuaDialogListBox::OnLeaveWindow, this);
}
void AddItem(const std::string &text, int iconId, const LuaAPI::LuaImage &img, const std::string &tooltip) {
LuaListBoxItem item;
item.text = text;
item.iconId = iconId;
item.customImage = img;
item.tooltip = tooltip;
if (!item.customImage.isValid() && iconId > 0) {
item.customImage = LuaAPI::LuaImage::loadFromSprite(iconId);
}
items.push_back(item);
SetItemCount(items.size());
}
void Clear() {
items.clear();
SetItemCount(0);
lastTooltipItem = -1;
}
void SetIconSize(int width, int height) {
iconWidth = width;
iconHeight = height;
if (itemHeight < iconHeight + 6) {
itemHeight = iconHeight + 6;
}
}
void SetItemHeight(int height) {
itemHeight = height;
}
void SetShowText(bool value) {
showText = value;
}
void SetSmooth(bool value) {
smooth = value;
}
const LuaListBoxItem* GetItem(int index) const {
if (index < 0 || index >= static_cast<int>(items.size())) {
return nullptr;
}
return &items[index];
}
void OnLeftDown(wxMouseEvent &event) {
int n = HitTest(event.GetPosition());
if (n != wxNOT_FOUND) {
SetSelection(n);
CallAfter([this, n]() {
wxCommandEvent ce(wxEVT_LISTBOX, GetId());
ce.SetInt(n);
ce.SetEventObject(this);
ProcessWindowEvent(ce);
});
}
event.Skip();
}
void OnLeftDouble(wxMouseEvent &event) {
int n = HitTest(event.GetPosition());
if (n != wxNOT_FOUND) {
SetSelection(n);
SetFocus();
CallAfter([this, n]() {
wxCommandEvent ce(wxEVT_LISTBOX_DCLICK, GetId());
ce.SetInt(n);
ce.SetEventObject(this);
ProcessWindowEvent(ce);
});
}
event.Skip();
}
void OnMotion(wxMouseEvent &event) {
int n = HitTest(event.GetPosition());
if (n != wxNOT_FOUND && n < static_cast<int>(items.size())) {
if (n != lastTooltipItem) {
const std::string &tip = items[n].tooltip;
if (!tip.empty()) {
SetToolTip(wxString(tip));
} else {
UnsetToolTip();
}
lastTooltipItem = n;
}
} else if (lastTooltipItem != -1) {
UnsetToolTip();
lastTooltipItem = -1;
}
event.Skip();
}
void OnLeaveWindow(wxMouseEvent &event) {
UnsetToolTip();
lastTooltipItem = -1;
event.Skip();
}
void OnDrawItem(wxDC &dc, const wxRect &rect, size_t n) const override {
if (n >= items.size()) {
return;
}
const LuaListBoxItem &item = items[n];
bool isSelected = IsSelected(n);
if (isSelected) {
dc.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)));
dc.SetPen(wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)));
} else {
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(*wxTRANSPARENT_PEN);
}
dc.DrawRectangle(rect);
// Icon
int textX = rect.x + 5;
if (item.customImage.isValid()) {
wxBitmap bmp = item.customImage.getBitmap(iconWidth, iconHeight, smooth);
if (bmp.IsOk()) {
dc.DrawBitmap(bmp, rect.x + 2, rect.y + (rect.height - iconHeight) / 2, true);
textX += iconWidth + 4;
}
}
if (showText) {
if (isSelected) {
dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT));
} else {
dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT));
}
wxString text = wxString(item.text);
wxSize extent = dc.GetTextExtent(text);
dc.DrawText(text, textX, rect.y + (rect.height - extent.y) / 2);
}
}
virtual wxCoord OnMeasureItem(size_t n) const override {
return itemHeight;
}
int iconWidth = 16;
int iconHeight = 16;
int itemHeight = 24;
bool showText = true;
bool smooth = true;
int lastTooltipItem = -1;
std::vector<LuaListBoxItem> items;
};
// Specialized ListCtrl for Grid with Tooltips
class LuaGridCtrl : public wxListCtrl {
public:
LuaGridCtrl(wxWindow* parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) :
wxListCtrl(parent, id, pos, size, style) {
Bind(wxEVT_MOTION, &LuaGridCtrl::OnMotion, this);
Bind(wxEVT_LEAVE_WINDOW, &LuaGridCtrl::OnLeaveWindow, this);
}
std::map<long, std::string> tooltips;
long lastTooltipItem = -1;
int iconWidth = 32;
int iconHeight = 32;
int cellWidth = -1;
int cellHeight = -1;
void AddTooltip(long item, std::string_view tip) {
tooltips[item] = tip;
}
void ClearTooltips() {
tooltips.clear();
lastTooltipItem = -1;
}
std::string GetTooltip(long item) const {
auto it = tooltips.find(item);
if (it == tooltips.end()) {
return ""s;
}
return it->second;
}
void OnMotion(wxMouseEvent &event) {
int flags = 0;
long index = HitTest(event.GetPosition(), flags);
if (index != wxNOT_FOUND) {
if (index != lastTooltipItem) {
if (tooltips.contains(index)) {
SetToolTip(wxString(tooltips[index]));
} else {
UnsetToolTip();
}
lastTooltipItem = index;
}
} else {
if (lastTooltipItem != -1) {
UnsetToolTip();
lastTooltipItem = -1;
}
}
event.Skip();
}
void OnLeaveWindow(wxMouseEvent &event) {
UnsetToolTip();
lastTooltipItem = -1;
event.Skip();
}
};
void LuaDialog::handleListClick(const std::string &id, LuaDialogListBox* listbox, const sol::function &callback, wxMouseEvent &event) {
if (!callback.valid() || !lua.lua_state() || !callback.lua_state()) {
event.Skip();
return;
}
int index = listbox->HitTest(event.GetPosition());
if (index == wxNOT_FOUND) {
event.Skip();
return;
}
const LuaListBoxItem* item = listbox->GetItem(index);
if (!item) {
event.Skip();
return;
}
listbox->SetSelection(index);
listbox->SetFocus();
values[id] = sol::make_object(lua, index + 1);
sol::table info = lua.create_table();
info["type"] = "list";
info["index"] = index + 1;
info["text"] = item->text;
if (!item->tooltip.empty()) {
info["tooltip"] = item->tooltip;
}
info["widget_id"] = id;
try {
callback(this, info);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
event.Skip();
}
void LuaDialog::handleListContextMenu(const std::string &id, LuaDialogListBox* listbox, const sol::function &callback, wxContextMenuEvent &event) {
if (!callback.valid() || !lua.lua_state() || !callback.lua_state()) {
return;
}
wxPoint screenPos = event.GetPosition();
if (screenPos == wxDefaultPosition) {
screenPos = wxGetMousePosition();
}
wxPoint clientPos = listbox->ScreenToClient(screenPos);
int index = listbox->HitTest(clientPos);
if (index == wxNOT_FOUND) {
return;
}
const LuaListBoxItem* item = listbox->GetItem(index);
if (!item) {
return;
}
sol::table info = lua.create_table();
info["type"] = "list";
info["index"] = index + 1;
info["text"] = item->text;
if (!item->tooltip.empty()) {
info["tooltip"] = item->tooltip;
}
info["widget_id"] = id;
popupContextMenu(callback, info, listbox, screenPos);
}
LuaDialog* LuaDialog::list(sol::table options) {
finishCurrentRow();
std::string id = options.get_or(std::string("id"), fmt::format("list_{}", widgets.size()));
int width = options.get_or(std::string("width"), 200);
int height = options.get_or(std::string("height"), 150);
int iconWidth = options.get_or(std::string("icon_width"), 16);
int iconHeight = options.get_or(std::string("icon_height"), 16);
int iconSize = options.get_or(std::string("icon_size"), -1);
int itemHeight = options.get_or(std::string("item_height"), 24);
bool showText = options.get_or(std::string("show_text"), true);
bool smooth = options.get_or(std::string("smooth"), true);
if (iconSize > 0) {
iconWidth = iconSize;
iconHeight = iconSize;
}
auto listbox = new LuaDialogListBox(getParentForWidget(), wxID_ANY, wxSize(width, height));
listbox->SetIconSize(iconWidth, iconHeight);
if (itemHeight > 0) {
listbox->SetItemHeight(itemHeight);
}
listbox->SetShowText(showText);
listbox->SetSmooth(smooth);
getSizerForWidget()->Add(listbox, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options));
// Populate items
if (options["items"].valid()) {
sol::table itemsTable = options["items"];
for (auto &pair : itemsTable) {
if (pair.second.is<sol::table>()) {
sol::table itemTable = pair.second;
std::string text = itemTable.get_or(std::string("text"), ""s);
int icon = itemTable.get_or(std::string("icon"), 0);
std::string tooltip = itemTable.get_or(std::string("tooltip"), ""s);
LuaAPI::LuaImage img;
if (itemTable["image"].valid()) {
if (itemTable["image"].is<LuaAPI::LuaImage>()) {
img = itemTable["image"].get<LuaAPI::LuaImage>();
} else if (itemTable["image"].is<std::string>()) {
img = LuaAPI::LuaImage::loadFromFile(itemTable["image"].get<std::string>());
}
}
listbox->AddItem(text, icon, img, tooltip);
}
}
}
// Selection
int selection = options.get_or(std::string("selection"), 0);
if (selection > 0 && selection <= (int)listbox->items.size()) {
listbox->SetSelection(selection - 1);
}
LuaDialogWidget widget;
widget.id = id;
widget.type = "list";
widget.widget = listbox;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
if (options["ondoubleclick"].valid()) {
widget.ondoubleclick = options["ondoubleclick"];
}
if (options["oncontextmenu"].valid()) {
widget.oncontextmenu = options["oncontextmenu"];
}
if (options["onleftclick"].valid()) {
widget.onleftclick = options["onleftclick"];
}
if (options["onrightclick"].valid()) {
widget.onrightclick = options["onrightclick"];
}
widgets.push_back(widget);
// Bind selection event
listbox->Bind(wxEVT_LISTBOX, [this, id](wxCommandEvent &) {
onWidgetChange(id);
});
// Bind double click event
listbox->Bind(wxEVT_LISTBOX_DCLICK, [this, id](wxCommandEvent &) {
onWidgetDoubleClick(id);
});
sol::function listLeftClick = widget.onleftclick;
listbox->Bind(wxEVT_LEFT_DOWN, [this, id, listbox, listLeftClick](wxMouseEvent &event) {
handleListClick(id, listbox, listLeftClick, event);
});
sol::function listRightClick = widget.onrightclick;
listbox->Bind(wxEVT_RIGHT_DOWN, [this, id, listbox, listRightClick](wxMouseEvent &event) {
handleListClick(id, listbox, listRightClick, event);
});
// Bind context menu event
sol::function listContextMenu = widget.oncontextmenu;
listbox->Bind(wxEVT_CONTEXT_MENU, [this, id, listbox, listContextMenu](wxContextMenuEvent &event) {
handleListContextMenu(id, listbox, listContextMenu, event);
});
applyCommonOptions(listbox, options);
return this;
}
LuaDialog* LuaDialog::file(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("file_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
std::string filename = options.get_or(std::string("filename"), ""s);
std::string filetypes = options.get_or(std::string("filetypes"), "*.*"s);
bool save = options.get_or(std::string("save"), false);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
}
wxFilePickerCtrl* picker;
if (save) {
picker = new wxFilePickerCtrl(getParentForWidget(), wxID_ANY, wxString(filename), "Select a file", wxString(filetypes), wxDefaultPosition, wxSize(200, -1), wxFLP_SAVE | wxFLP_USE_TEXTCTRL);
} 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, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "file";
widget.widget = picker;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, filename);
picker->Bind(wxEVT_FILEPICKER_CHANGED, [this, id](wxFileDirPickerEvent &) {
onWidgetChange(id);
});
applyCommonOptions(picker, options);
return this;
}
LuaDialog* LuaDialog::image(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("image_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
int width = options.get_or(std::string("width"), -1);
int height = options.get_or(std::string("height"), -1);
bool smooth = options.get_or(std::string("smooth"), true); // Default to smooth scaling
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
}
wxBitmap bmp;
LuaAPI::LuaImage luaImage;
// Check for different image sources
if (options["image"].valid()) {
// Direct LuaImage object
luaImage = options["image"].get<LuaAPI::LuaImage>();
} else if (options["path"].valid()) {
// Load from file path
std::string path = options.get<std::string>("path");
luaImage = LuaAPI::LuaImage::loadFromFile(path);
} else if (options["itemid"].valid()) {
// Load from item sprite ID
int itemId = options.get<int>("itemid");
luaImage = LuaAPI::LuaImage::loadFromItemSprite(itemId);
} else if (options["spriteid"].valid()) {
// Load from raw sprite ID
int spriteId = options.get<int>("spriteid");
luaImage = LuaAPI::LuaImage::loadFromSprite(spriteId);
}
// Get bitmap with optional resizing
if (luaImage.isValid()) {
if (width > 0 && height > 0) {
bmp = luaImage.getBitmap(width, height, smooth);
} else if (width > 0) {
// Scale proportionally based on width
double factor = static_cast<double>(width) / luaImage.getWidth();
auto newHeight = static_cast<int>(luaImage.getHeight() * factor);
bmp = luaImage.getBitmap(width, newHeight, smooth);
} else if (height > 0) {
// Scale proportionally based on height
double factor = static_cast<double>(height) / luaImage.getHeight();
auto newWidth = static_cast<int>(luaImage.getWidth() * factor);
bmp = luaImage.getBitmap(newWidth, height, smooth);
} else {
bmp = luaImage.getBitmap();
}
}
// Create placeholder if no valid image
if (!bmp.IsOk()) {
int placeholderW = width > 0 ? width : 32;
int placeholderH = height > 0 ? height : 32;
bmp.Create(placeholderW, placeholderH);
wxMemoryDC dc(bmp);
dc.SetBrush(*wxLIGHT_GREY_BRUSH);
dc.SetPen(*wxGREY_PEN);
dc.DrawRectangle(0, 0, placeholderW, placeholderH);
dc.SelectObject(wxNullBitmap);
}
wxStaticBitmap* staticBmp = new wxStaticBitmap(getParentForWidget(), wxID_ANY, bmp);
currentRowSizer->Add(staticBmp, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "image";
widget.widget = staticBmp;
widgets.push_back(widget);
// Store the LuaImage in values for later retrieval/modification
values[id] = sol::make_object(lua, luaImage);
applyCommonOptions(staticBmp, options);
return this;
}
LuaDialog* LuaDialog::item(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("item_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
int itemId = options.get_or(std::string("itemid"), 0);
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
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;
auto btn = new ItemButton(getParentForWidget(), RENDER_SIZE_32x32, clientId);
currentRowSizer->Add(btn, 0, getSizerFlags(options, wxALIGN_CENTER_VERTICAL | wxRIGHT), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "item";
widget.widget = btn;
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
if (options["onclick"].valid()) {
widget.onclick = options["onclick"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, itemId);
bool readonly = options.get_or(std::string("readonly"), false);
// Bind click event
btn->Bind(wxEVT_BUTTON, [this, id, btn, readonly](wxCommandEvent &) {
handleItemButtonClick(id, btn, readonly);
});
applyCommonOptions(btn, options);
return this;
}
LuaDialog* LuaDialog::separator(sol::optional<sol::table> options) {
finishCurrentRow();
wxStaticLine* line = new wxStaticLine(getParentForWidget(), wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
sol::table opts = options.value_or(lua.create_table());
getSizerForWidget()->Add(line, 0, getSizerFlags(opts, wxEXPAND | wxALL), getSizerBorder(opts));
applyCommonOptions(line, opts);
return this;
}
LuaDialog* LuaDialog::newrow() {
finishCurrentRow();
return this;
}
void LuaDialog::ensureNotebook() {
if (currentNotebook) {
return;
}
wxWindow* parent = dockPanel ? static_cast<wxWindow*>(dockPanel) : static_cast<wxWindow*>(this);
currentNotebook = new wxNotebook(parent, wxID_ANY);
mainSizer->Add(currentNotebook, 1, wxEXPAND | wxALL, 5);
activeNotebook = currentNotebook;
tabInfos.clear();
notebookEventsBound = false;
}
void LuaDialog::onNotebookPageChanging(wxNotebookEvent &event) {
int newSelection = event.GetSelection();
if (newSelection == wxNOT_FOUND || newSelection >= static_cast<int>(tabInfos.size())) {
event.Skip();
return;
}
if (!tabInfos[newSelection].isButton) {
event.Skip();
return;
}
if (suppressTabButtonClick && suppressTabButtonIndex == newSelection) {
suppressTabButtonClick = false;
suppressTabButtonIndex = -1;
event.Veto();
return;
}
CallAfter([this, newSelection]() {
handleTabButtonClick(newSelection);
});
event.Veto();
}
void LuaDialog::onNotebookLeftDown(wxMouseEvent &event) {
if (!activeNotebook || tabInfos.empty()) {
event.Skip();
return;
}
wxPoint pos = event.GetPosition();
long flags = 0;
int index = activeNotebook->HitTest(pos, &flags);
if (index == wxNOT_FOUND || index >= static_cast<int>(tabInfos.size()) || !tabInfos[index].isButton) {
event.Skip();
return;
}
suppressTabButtonClick = true;
suppressTabButtonIndex = index;
CallAfter([this, index]() {
handleTabButtonClick(index);
});
CallAfter([this, index]() {
if (suppressTabButtonClick && suppressTabButtonIndex == index) {
suppressTabButtonClick = false;
suppressTabButtonIndex = -1;
}
});
event.StopPropagation();
}
void LuaDialog::onNotebookContextMenu(wxContextMenuEvent &event) {
if (!activeNotebook || tabInfos.empty()) {
return;
}
wxPoint screenPos = event.GetPosition();
if (screenPos == wxDefaultPosition) {
screenPos = wxGetMousePosition();
}
wxPoint clientPos = activeNotebook->ScreenToClient(screenPos);
long flags = 0;
int index = activeNotebook->HitTest(clientPos, &flags);
if (index == wxNOT_FOUND || index >= static_cast<int>(tabInfos.size())) {
return;
}
handleTabContextMenu(index, screenPos);
}
void LuaDialog::bindNotebookEvents() {
if (notebookEventsBound) {
return;
}
notebookEventsBound = true;
currentNotebook->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &LuaDialog::onNotebookPageChanging, this);
currentNotebook->Bind(wxEVT_LEFT_DOWN, &LuaDialog::onNotebookLeftDown, this);
currentNotebook->Bind(wxEVT_CONTEXT_MENU, &LuaDialog::onNotebookContextMenu, this);
}
int LuaDialog::insertTabPanel(const std::string &text, int insertIndex) {
currentTabPanel = new wxPanel(currentNotebook);
currentTabSizer = new wxBoxSizer(wxVERTICAL);
currentTabPanel->SetSizer(currentTabSizer);
if (insertIndex > 0) {
int zeroBased = std::clamp(insertIndex - 1, 0, static_cast<int>(currentNotebook->GetPageCount()));
currentNotebook->InsertPage(zeroBased, currentTabPanel, wxString(text));
return zeroBased;
}
currentNotebook->AddPage(currentTabPanel, wxString(text));
return static_cast<int>(currentNotebook->GetPageCount()) - 1;
}
LuaDialog* LuaDialog::tab(sol::table options) {
finishCurrentRow();
std::string id = options.get_or(std::string("id"), fmt::format("tab_{}", widgets.size()));
std::string text = options.get_or(std::string("text"), "Tab"s);
bool isButton = options.get_or(std::string("button"), false) || options.get_or(std::string("is_button"), false);
int insertIndex = options.get_or(std::string("index"), -1);
sol::function onclick;
sol::function oncontextmenu;
if (options["onclick"].valid()) {
onclick = options["onclick"];
}
if (options["oncontextmenu"].valid()) {
oncontextmenu = options["oncontextmenu"];
}
ensureNotebook();
bindNotebookEvents();
int pageIndex = insertTabPanel(text, insertIndex);
LuaDialogTab tabInfo;
tabInfo.id = id;
tabInfo.text = text;
tabInfo.isButton = isButton;
tabInfo.onclick = onclick;
tabInfo.oncontextmenu = oncontextmenu;
if (pageIndex >= 0 && pageIndex <= static_cast<int>(tabInfos.size())) {
tabInfos.insert(tabInfos.begin() + pageIndex, tabInfo);
} else {
tabInfos.push_back(tabInfo);
}
rowSizerStack.push(currentRowSizer);
currentRowSizer = nullptr;
sizerStack.push(currentTabSizer);
tabSizers.push_back(currentTabSizer);
return this;
}
LuaDialog* LuaDialog::endtabs() {
finishCurrentRow();
// Pop all tab sizers that were added
while (!tabSizers.empty()) {
wxSizer* topSizer = tabSizers.back();
if (!sizerStack.empty() && sizerStack.top() == topSizer) {
sizerStack.pop();
}
if (!rowSizerStack.empty()) {
currentRowSizer = rowSizerStack.top();
rowSizerStack.pop();
}
tabSizers.pop_back();
}
currentNotebook = nullptr;
currentTabPanel = nullptr;
currentTabSizer = nullptr;
return this;
}
sol::table LuaDialog::makeTabInfoTable(int index) {
sol::table info = lua.create_table();
if (index < 0 || index >= static_cast<int>(tabInfos.size())) {
return info;
}
const LuaDialogTab &tab = tabInfos[index];
info["index"] = index + 1;
info["text"] = tab.text;
info["id"] = tab.id;
info["is_button"] = tab.isButton;
return info;
}
void LuaDialog::handleItemButtonClick(const std::string &id, ItemButton* btn, bool readonly) {
bool handled = false;
for (auto &w : widgets) {
if (w.id == id && w.onclick.valid()) {
onButtonClick(id);
handled = true;
break;
}
}
if (!handled && !readonly) {
FindItemDialog dlg(this, "Select Item", false);
if (dlg.ShowModal() == wxID_OK) {
int newItemId = dlg.getResultID();
int newClientId = (newItemId > 100 && newItemId <= g_items.getMaxID()) ? g_items[newItemId].clientID : 0;
btn->SetSprite(newClientId);
btn->Refresh();
values[id] = sol::make_object(lua, newItemId);
onWidgetChange(id);
}
}
}
void LuaDialog::handleTabButtonClick(int index) {
if (index < 0 || index >= static_cast<int>(tabInfos.size())) {
return;
}
sol::function onclick = tabInfos[index].onclick;
if (!onclick.valid()) {
return;
}
if (!lua.lua_state()) {
return;
}
if (!onclick.lua_state()) {
return;
}
sol::table info = makeTabInfoTable(index);
try {
onclick(this, info);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
void LuaDialog::handleTabContextMenu(int index, const wxPoint &screenPos) {
if (index < 0 || index >= static_cast<int>(tabInfos.size())) {
return;
}
sol::function oncontextmenu = tabInfos[index].oncontextmenu;
if (!oncontextmenu.valid()) {
return;
}
if (!lua.lua_state()) {
return;
}
if (!oncontextmenu.lua_state()) {
return;
}
sol::table info = makeTabInfoTable(index);
info["type"] = "tab";
popupContextMenu(oncontextmenu, info, activeNotebook, screenPos);
}
void LuaDialog::popupContextMenu(const sol::function &callback, sol::table info, wxWindow* window, const wxPoint &screenPos) {
if (!callback.valid()) {
return;
}
if (!lua.lua_state() || !callback.lua_state()) {
return;
}
sol::object result;
try {
result = callback(this, info);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
return;
}
if (!result.valid() || !result.is<sol::table>()) {
return;
}
sol::table items = result.as<sol::table>();
wxMenu menu;
std::map<int, sol::function> callbacks;
int nextId = wxID_HIGHEST + 1;
for (auto &pair : items) {
if (!pair.second.is<sol::table>()) {
continue;
}
sol::table item = pair.second.as<sol::table>();
bool separator = item.get_or(std::string("separator"), false);
std::string text = item.get_or(std::string("text"), ""s);
if (separator || text.empty()) {
menu.AppendSeparator();
continue;
}
int id = nextId++;
wxMenuItem* menuItem = menu.Append(id, wxString(text));
bool enabled = item.get_or(std::string("enabled"), true);
menuItem->Enable(enabled);
if (item["onclick"].valid()) {
sol::function fn = item["onclick"];
if (fn.lua_state()) {
callbacks[id] = fn;
}
}
}
if (menu.GetMenuItemCount() == 0) {
return;
}
menu.Bind(wxEVT_MENU, [this, callbacks, info](wxCommandEvent &event) mutable {
if (!lua.lua_state()) {
return;
}
auto it = callbacks.find(event.GetId());
if (it == callbacks.end()) {
return;
}
if (!it->second.lua_state()) {
return;
}
try {
it->second(this, info);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
});
if (!window) {
return;
}
wxPoint clientPos = window->ScreenToClient(screenPos);
window->PopupMenu(&menu, clientPos);
}
LuaDialog* LuaDialog::show(sol::optional<sol::table> options) {
if (dockPanel) {
wxAuiPaneInfo &info = g_gui.aui_manager->GetPane(dockPanel);
if (info.IsOk()) {
info.Show();
g_gui.aui_manager->Update();
}
isShowing = true;
return this;
}
// Fit the dialog first to establish baseline size from sizers
Fit();
// Re-apply requested size from constructor if it's larger than fitted size
if (reqWidth != -1 || reqHeight != -1) {
wxSize sz = GetSize();
if (reqWidth != -1) {
sz.x = std::max(sz.x, reqWidth);
}
if (reqHeight != -1) {
sz.y = std::max(sz.y, reqHeight);
}
SetSize(sz);
}
// Process options
if (options) {
sol::table opts = *options;
waitMode = opts.get_or(std::string("wait"), true);
if (opts["bounds"].valid()) {
setBounds(opts["bounds"]);
} else if (reqX == -1 && reqY == -1) {
bool wantsCenter = true;
std::string centerOn = "parent";
if (opts["center"].valid()) {
if (opts["center"].is<bool>()) {
wantsCenter = opts.get<bool>("center");
} else if (opts["center"].is<std::string>()) {
centerOn = opts.get<std::string>("center");
}
}
if (opts["center_on"].valid() && opts["center_on"].is<std::string>()) {
centerOn = opts.get<std::string>("center_on");
}
if (wantsCenter) {
if (centerOn == "screen") {
Centre();
} else {
CentreOnParent();
}
}
}
} else {
if (reqX == -1 && reqY == -1) {
CentreOnParent();
}
}
isShowing = true;
if (waitMode) {
ShowModal();
isShowing = false;
} else {
Show();
}
return this;
}
void LuaDialog::close() {
while (hotkeySuspendCount > 0) {
resumeHotkeys();
}
if (dockPanel) {
wxAuiPaneInfo &info = g_gui.aui_manager->GetPane(dockPanel);
if (info.IsOk()) {
info.Hide();
g_gui.aui_manager->Update();
}
isShowing = false;
if (oncloseCallback.valid()) {
try {
oncloseCallback();
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error in onclose: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
return;
}
if (IsModal()) {
EndModal(wxID_OK);
} else {
Hide();
}
isShowing = false;
if (oncloseCallback.valid()) {
try {
oncloseCallback();
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error in onclose: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
}
void LuaDialog::OnClose(wxCloseEvent &event) {
if (IsModal()) {
EndModal(wxID_CANCEL);
} else {
Hide();
}
isShowing = false;
if (oncloseCallback.valid()) {
try {
oncloseCallback();
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error in onclose: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
if (!IsModal() && event.CanVeto()) {
// Prevent destruction of modeless dialog so it can be reused
// event.Veto() is not needed if we don't Skip(), but it's safer
event.Veto();
return;
}
event.Skip();
}
LuaDialog* LuaDialog::modify(sol::table options) {
// Modify widget properties
for (auto &pair : options) {
std::string id = pair.first.as<std::string>();
sol::table props = pair.second.as<sol::table>();
for (auto &widget : widgets) {
if (widget.id == id) {
// Apply property changes based on widget type
if (widget.type == "mapCanvas") {
MapPreviewCanvas* canvas = static_cast<MapPreviewCanvas*>(widget.widget);
if (props["x"].valid() && props["y"].valid()) {
canvas->SetPosition(props.get_or(std::string("x"), 1000), props.get_or(std::string("y"), 1000), props.get_or(std::string("z"), (int)canvas->GetFloor()));
}
if (props["zoom"].valid()) {
canvas->SetZoom(props.get_or(std::string("zoom"), 1.0));
}
if (props["floor"].valid()) {
canvas->ChangeFloor(props.get_or(std::string("floor"), 7));
canvas->Refresh();
}
if (props["sync"].valid() && props.get_or(std::string("sync"), false)) {
canvas->SyncView();
}
if (props["client_w"].valid() && props["client_h"].valid()) {
canvas->SetViewSize(props.get_or(std::string("client_w"), 25), props.get_or(std::string("client_h"), 25));
}
if (props["light"].valid()) {
canvas->SetLight(props.get_or(std::string("light"), false));
}
} else if (widget.type == "input") {
wxTextCtrl* ctrl = static_cast<wxTextCtrl*>(widget.widget);
if (props["text"].valid()) {
ctrl->SetValue(wxString(props.get_or(std::string("text"), ""s)));
}
} else if (widget.type == "number") {
wxSpinCtrlDouble* ctrl = static_cast<wxSpinCtrlDouble*>(widget.widget);
if (props["value"].valid()) {
ctrl->SetValue(props.get_or(std::string("value"), 0.0));
}
} else if (widget.type == "check") {
wxCheckBox* ctrl = static_cast<wxCheckBox*>(widget.widget);
if (props["selected"].valid()) {
ctrl->SetValue(props.get_or(std::string("selected"), false));
}
} else if (widget.type == "item") {
ItemButton* ctrl = static_cast<ItemButton*>(widget.widget);
if (props["itemid"].valid()) {
int newItemId = props.get_or(std::string("itemid"), 0);
ctrl->SetSprite(newItemId);
ctrl->Refresh();
}
} else if (widget.type == "label") {
wxStaticText* ctrl = static_cast<wxStaticText*>(widget.widget);
if (props["text"].valid()) {
ctrl->SetLabel(wxString(props.get_or(std::string("text"), ""s)));
}
} else if (widget.type == "combobox") {
wxChoice* ctrl = static_cast<wxChoice*>(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<std::string>()));
}
}
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<LuaDialogListBox*>(widget.widget);
if (props["icon_width"].valid() || props["icon_height"].valid() || props["icon_size"].valid()) {
int iconWidth = props.get_or(std::string("icon_width"), ctrl->iconWidth);
int iconHeight = props.get_or(std::string("icon_height"), ctrl->iconHeight);
int iconSize = props.get_or(std::string("icon_size"), -1);
if (iconSize > 0) {
iconWidth = iconSize;
iconHeight = iconSize;
}
ctrl->SetIconSize(iconWidth, iconHeight);
}
if (props["item_height"].valid()) {
ctrl->SetItemHeight(props.get_or(std::string("item_height"), ctrl->itemHeight));
}
if (props["show_text"].valid()) {
ctrl->SetShowText(props.get_or(std::string("show_text"), true));
}
if (props["smooth"].valid()) {
ctrl->SetSmooth(props.get_or(std::string("smooth"), true));
}
if (props["items"].valid()) {
ctrl->Freeze();
ctrl->Clear();
sol::table itemsTable = props["items"];
for (auto &pair : itemsTable) {
if (pair.second.is<sol::table>()) {
sol::table itemTable = pair.second;
std::string text = itemTable.get_or(std::string("text"), ""s);
int icon = itemTable.get_or(std::string("icon"), 0);
std::string tooltip = itemTable.get_or(std::string("tooltip"), ""s);
LuaAPI::LuaImage img;
if (itemTable["image"].valid()) {
if (itemTable["image"].is<LuaAPI::LuaImage>()) {
img = itemTable["image"].get<LuaAPI::LuaImage>();
} else if (itemTable["image"].is<std::string>()) {
img = LuaAPI::LuaImage::loadFromFile(itemTable["image"].get<std::string>());
}
}
ctrl->AddItem(text, icon, img, tooltip);
}
}
ctrl->Refresh();
ctrl->Thaw();
}
if (props["selection"].valid()) {
int selection = props.get_or(std::string("selection"), 0);
if (selection > 0 && selection <= (int)ctrl->items.size()) {
ctrl->SetSelection(selection - 1);
} else {
ctrl->SetSelection(wxNOT_FOUND);
}
}
} else if (widget.type == "grid") {
LuaGridCtrl* ctrl = static_cast<LuaGridCtrl*>(widget.widget);
int iconWidth = ctrl->iconWidth;
int iconHeight = ctrl->iconHeight;
int cellWidth = ctrl->cellWidth;
int cellHeight = ctrl->cellHeight;
bool updateIconSize = false;
bool updateCellSize = false;
int iconSize = props.get_or(std::string("icon_size"), -1);
int itemSize = props.get_or(std::string("item_size"), -1);
int itemWidth = props.get_or(std::string("item_width"), -1);
int itemHeight = props.get_or(std::string("item_height"), -1);
int iconWidthOpt = props.get_or(std::string("icon_width"), -1);
int iconHeightOpt = props.get_or(std::string("icon_height"), -1);
int cellSize = props.get_or(std::string("cell_size"), -1);
int cellWidthOpt = props.get_or(std::string("cell_width"), -1);
int cellHeightOpt = props.get_or(std::string("cell_height"), -1);
if (iconSize > 0) {
iconWidth = iconSize;
iconHeight = iconSize;
updateIconSize = true;
}
if (itemSize > 0) {
iconWidth = itemSize;
iconHeight = itemSize;
updateIconSize = true;
}
if (itemWidth > 0) {
iconWidth = itemWidth;
updateIconSize = true;
}
if (itemHeight > 0) {
iconHeight = itemHeight;
updateIconSize = true;
}
if (iconWidthOpt > 0) {
iconWidth = iconWidthOpt;
updateIconSize = true;
}
if (iconHeightOpt > 0) {
iconHeight = iconHeightOpt;
updateIconSize = true;
}
if (cellSize > 0) {
cellWidth = cellSize;
cellHeight = cellSize;
updateCellSize = true;
}
if (cellWidthOpt > 0) {
cellWidth = cellWidthOpt;
updateCellSize = true;
}
if (cellHeightOpt > 0) {
cellHeight = cellHeightOpt;
updateCellSize = true;
}
if (updateCellSize) {
if (cellWidth <= 0) {
cellWidth = iconWidth + 8;
}
if (cellHeight <= 0) {
cellHeight = iconHeight + 8;
}
#ifdef __WXMSW__
ListView_SetIconSpacing(static_cast<HWND>(ctrl->GetHandle()), cellWidth, cellHeight);
#endif
ctrl->cellWidth = cellWidth;
ctrl->cellHeight = cellHeight;
}
if (props["selection"].valid()) {
int selection = props.get_or("selection", 0);
long item = -1;
while ((item = ctrl->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
ctrl->SetItemState(item, 0, wxLIST_STATE_SELECTED);
}
if (selection > 0 && selection <= ctrl->GetItemCount()) {
ctrl->SetItemState(selection - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
}
}
if (props["items"].valid()) {
ctrl->Freeze();
ctrl->DeleteAllItems();
ctrl->ClearTooltips();
wxImageList* imageList = ctrl->GetImageList(wxIMAGE_LIST_NORMAL);
if (!imageList || updateIconSize) {
imageList = new wxImageList(iconWidth, iconHeight, true);
ctrl->AssignImageList(imageList, wxIMAGE_LIST_NORMAL);
ctrl->iconWidth = iconWidth;
ctrl->iconHeight = iconHeight;
} else {
imageList->RemoveAll();
}
sol::table itemsTable = props["items"];
for (auto &pair : itemsTable) {
if (pair.second.is<sol::table>()) {
sol::table itemTable = pair.second;
std::string text = itemTable.get_or("text", ""s);
LuaAPI::LuaImage img;
int imageIndex = -1;
if (itemTable["image"].valid()) {
if (itemTable["image"].is<LuaAPI::LuaImage>()) {
img = itemTable["image"].get<LuaAPI::LuaImage>();
} else if (itemTable["image"].is<std::string>()) {
img = LuaAPI::LuaImage::loadFromFile(itemTable["image"].get<std::string>());
}
}
if (img.isValid()) {
imageIndex = imageList->Add(img.getBitmap(iconWidth, iconHeight));
}
long index = ctrl->InsertItem(ctrl->GetItemCount(), text, imageIndex);
ctrl->SetItemData(index, index + 1);
std::string tooltip = itemTable.get_or("tooltip", ""s);
if (!tooltip.empty()) {
// Must match the type cast
LuaGridCtrl* gridCtrl = static_cast<LuaGridCtrl*>(ctrl);
gridCtrl->AddTooltip(index, tooltip);
}
}
}
ctrl->Thaw();
}
} else if (widget.type == "image") {
wxStaticBitmap* ctrl = static_cast<wxStaticBitmap*>(widget.widget);
LuaAPI::LuaImage luaImage;
int width = props.get_or(std::string("width"), -1);
int height = props.get_or(std::string("height"), -1);
bool smooth = props.get_or(std::string("smooth"), true);
if (props["image"].valid()) {
luaImage = props["image"].get<LuaAPI::LuaImage>();
} else if (props["path"].valid()) {
std::string path = props.get<std::string>("path");
luaImage = LuaAPI::LuaImage::loadFromFile(path);
} else if (props["itemid"].valid()) {
int itemId = props.get<int>("itemid");
luaImage = LuaAPI::LuaImage::loadFromItemSprite(itemId);
} else if (props["spriteid"].valid()) {
int spriteId = props.get<int>("spriteid");
luaImage = LuaAPI::LuaImage::loadFromSprite(spriteId);
}
if (luaImage.isValid()) {
wxBitmap bmp;
if (width > 0 && height > 0) {
bmp = luaImage.getBitmap(width, height, smooth);
} else if (width > 0) {
double factor = static_cast<double>(width) / luaImage.getWidth();
int newHeight = static_cast<int>(luaImage.getHeight() * factor);
bmp = luaImage.getBitmap(width, newHeight, smooth);
} else if (height > 0) {
double factor = static_cast<double>(height) / luaImage.getHeight();
int newWidth = static_cast<int>(luaImage.getWidth() * factor);
bmp = luaImage.getBitmap(newWidth, height, smooth);
} else {
bmp = luaImage.getBitmap();
}
if (bmp.IsOk()) {
ctrl->SetBitmap(bmp);
values[id] = sol::make_object(lua, luaImage);
}
}
}
applyCommonOptions(widget.widget, props);
break;
}
}
}
return this;
}
void LuaDialog::handleGridClick(const std::string &id, LuaGridCtrl* grid, const sol::function &callback, wxMouseEvent &event) {
if (!callback.valid() || !lua.lua_state() || !callback.lua_state()) {
event.Skip();
return;
}
int flags = 0;
long index = grid->HitTest(event.GetPosition(), flags);
if (index == wxNOT_FOUND) {
event.Skip();
return;
}
grid->SetItemState(index, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
grid->SetFocus();
values[id] = sol::make_object(lua, static_cast<int>(index + 1));
sol::table info = lua.create_table();
info["type"] = "grid";
info["index"] = static_cast<int>(index + 1);
info["text"] = grid->GetItemText(index).ToStdString();
std::string tooltip = grid->GetTooltip(index);
if (!tooltip.empty()) {
info["tooltip"] = tooltip;
}
info["widget_id"] = id;
try {
callback(this, info);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
event.Skip();
}
void LuaDialog::handleGridContextMenu(const std::string &id, LuaGridCtrl* grid, const sol::function &callback, wxContextMenuEvent &event) {
if (!callback.valid() || !lua.lua_state() || !callback.lua_state()) {
return;
}
wxPoint screenPos = event.GetPosition();
if (screenPos == wxDefaultPosition) {
screenPos = wxGetMousePosition();
}
wxPoint clientPos = grid->ScreenToClient(screenPos);
int flags = 0;
long index = grid->HitTest(clientPos, flags);
if (index == wxNOT_FOUND) {
return;
}
sol::table info = lua.create_table();
info["type"] = "grid";
info["index"] = static_cast<int>(index + 1);
info["text"] = grid->GetItemText(index).ToStdString();
std::string tooltip = grid->GetTooltip(index);
if (!tooltip.empty()) {
info["tooltip"] = tooltip;
}
info["widget_id"] = id;
popupContextMenu(callback, info, grid, screenPos);
}
LuaDialog* LuaDialog::grid(sol::table options) {
ensureRowSizer();
std::string id = options.get_or(std::string("id"), fmt::format("grid_{}", widgets.size()));
std::string labelText = options.get_or(std::string("label"), ""s);
int iconWidth = options.get_or(std::string("icon_width"), 32);
int iconHeight = options.get_or(std::string("icon_height"), 32);
int iconSize = options.get_or(std::string("icon_size"), -1);
int itemSize = options.get_or(std::string("item_size"), -1);
int itemWidth = options.get_or(std::string("item_width"), -1);
int itemHeight = options.get_or(std::string("item_height"), -1);
int cellSize = options.get_or(std::string("cell_size"), -1);
int cellWidth = options.get_or(std::string("cell_width"), -1);
int cellHeight = options.get_or(std::string("cell_height"), -1);
bool labelWrap = options.get_or(std::string("label_wrap"), true);
bool showText = options.get_or(std::string("show_text"), true);
if (iconSize > 0) {
iconWidth = iconSize;
iconHeight = iconSize;
}
if (itemSize > 0) {
iconWidth = itemSize;
iconHeight = itemSize;
}
if (itemWidth > 0) {
iconWidth = itemWidth;
}
if (itemHeight > 0) {
iconHeight = itemHeight;
}
if (iconWidth <= 0) {
iconWidth = 32;
}
if (iconHeight <= 0) {
iconHeight = 32;
}
if (cellSize > 0) {
cellWidth = cellSize;
cellHeight = cellSize;
}
if (!labelText.empty()) {
wxStaticText* label = new wxStaticText(getParentForWidget(), wxID_ANY, wxString(labelText));
currentRowSizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5);
}
// Use LuaGridCtrl in Icon mode for grid view
auto grid = new LuaGridCtrl(getParentForWidget(), wxID_ANY, wxDefaultPosition, wxSize(300, 200), wxLC_ICON | wxLC_AUTOARRANGE | wxLC_SINGLE_SEL);
// Create an image list
wxImageList* imageList = new wxImageList(iconWidth, iconHeight, true);
grid->AssignImageList(imageList, wxIMAGE_LIST_NORMAL);
if (!showText) {
wxFont font = grid->GetFont();
font.SetPointSize(1);
grid->SetFont(font);
}
#ifdef __WXMSW__
if (!labelWrap) {
HWND hwnd = static_cast<HWND>(grid->GetHandle());
LONG_PTR style = ::GetWindowLongPtr(hwnd, GWL_STYLE);
style |= LVS_NOLABELWRAP;
::SetWindowLongPtr(hwnd, GWL_STYLE, style);
::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
}
if (cellWidth > 0 || cellHeight > 0) {
if (cellWidth <= 0) {
cellWidth = iconWidth + 8;
}
if (cellHeight <= 0) {
cellHeight = iconHeight + 8;
}
ListView_SetIconSpacing(static_cast<HWND>(grid->GetHandle()), cellWidth, cellHeight);
}
#endif
grid->iconWidth = iconWidth;
grid->iconHeight = iconHeight;
grid->cellWidth = cellWidth;
grid->cellHeight = cellHeight;
if (options["items"].valid()) {
sol::table itemsTable = options["items"];
for (auto &pair : itemsTable) {
if (pair.second.is<sol::table>()) {
sol::table itemTable = pair.second;
std::string text = itemTable.get_or(std::string("text"), ""s);
if (!showText) {
text.clear();
}
LuaAPI::LuaImage img;
int imageIndex = -1;
if (itemTable["image"].valid()) {
if (itemTable["image"].is<LuaAPI::LuaImage>()) {
img = itemTable["image"].get<LuaAPI::LuaImage>();
} else if (itemTable["image"].is<std::string>()) {
img = LuaAPI::LuaImage::loadFromFile(itemTable["image"].get<std::string>());
}
}
if (img.isValid()) {
imageIndex = imageList->Add(img.getBitmap(iconWidth, iconHeight));
}
long index = grid->InsertItem(grid->GetItemCount(), text, imageIndex);
// Store original item index/id in data if needed, or rely on position
grid->SetItemData(index, index + 1); // 1-based index
std::string tooltip = itemTable.get_or(std::string("tooltip"), ""s);
if (!tooltip.empty()) {
grid->AddTooltip(index, tooltip);
}
}
}
}
currentRowSizer->Add(grid, 1, getSizerFlags(options, wxEXPAND | wxALL), getSizerBorder(options));
LuaDialogWidget widget;
widget.id = id;
widget.type = "grid";
widget.widget = grid;
if (options["ondoubleclick"].valid()) {
widget.ondoubleclick = options["ondoubleclick"];
}
if (options["onchange"].valid()) {
widget.onchange = options["onchange"];
}
if (options["onleftclick"].valid()) {
widget.onleftclick = options["onleftclick"];
}
if (options["onrightclick"].valid()) {
widget.onrightclick = options["onrightclick"];
}
if (options["oncontextmenu"].valid()) {
widget.oncontextmenu = options["oncontextmenu"];
}
widgets.push_back(widget);
values[id] = sol::make_object(lua, 0);
// Bind events
grid->Bind(wxEVT_LIST_ITEM_SELECTED, [this, id, grid](wxListEvent &event) {
values[id] = sol::make_object(lua, event.GetIndex() + 1);
onWidgetChange(id);
});
grid->Bind(wxEVT_LIST_ITEM_ACTIVATED, [this, id, grid](wxListEvent &event) {
values[id] = sol::make_object(lua, event.GetIndex() + 1);
onWidgetDoubleClick(id);
});
sol::function gridLeftClick = widget.onleftclick;
grid->Bind(wxEVT_LEFT_DOWN, [this, id, grid, gridLeftClick](wxMouseEvent &event) {
handleGridClick(id, grid, gridLeftClick, event);
});
sol::function gridRightClick = widget.onrightclick;
grid->Bind(wxEVT_RIGHT_DOWN, [this, id, grid, gridRightClick](wxMouseEvent &event) {
handleGridClick(id, grid, gridRightClick, event);
});
sol::function gridContextMenu = widget.oncontextmenu;
grid->Bind(wxEVT_CONTEXT_MENU, [this, id, grid, gridContextMenu](wxContextMenuEvent &event) {
handleGridContextMenu(id, grid, gridContextMenu, event);
});
applyCommonOptions(grid, options);
return this;
}
void LuaDialog::repaint() {
Refresh();
Update();
}
void LuaDialog::clear() {
// Clear all widgets
widgets.clear();
values.clear();
// Destroy all children of mainSizer
mainSizer->Clear(true); // true = delete windows
// Reset layout state
while (!sizerStack.empty()) {
sizerStack.pop();
}
sizerStack.push(mainSizer);
tabSizers.clear();
currentRowSizer = nullptr;
currentNotebook = nullptr;
activeNotebook = nullptr;
currentTabPanel = nullptr;
currentTabSizer = nullptr;
tabInfos.clear();
notebookEventsBound = false;
suppressTabButtonClick = false;
suppressTabButtonIndex = -1;
dockPanel ? dockPanel->Layout() : Layout();
}
void LuaDialog::layout() {
dockPanel ? dockPanel->Layout() : Layout();
}
sol::table LuaDialog::getData() {
collectAllValues();
sol::table data = lua.create_table();
for (auto &pair : values) {
data[pair.first] = pair.second;
}
return data;
}
void LuaDialog::setData(sol::table data) {
for (auto &pair : data) {
std::string id = pair.first.as<std::string>();
values[id] = pair.second;
}
}
sol::table LuaDialog::getBounds() {
sol::table bounds = lua.create_table();
wxRect rect = GetRect();
bounds["x"] = rect.x;
bounds["y"] = rect.y;
bounds["width"] = rect.width;
bounds["height"] = rect.height;
return bounds;
}
sol::object LuaDialog::getActiveTab() {
wxNotebook* notebook = activeNotebook ? activeNotebook : currentNotebook;
if (!notebook) {
return sol::make_object(lua, sol::lua_nil);
}
int selection = notebook->GetSelection();
if (selection == wxNOT_FOUND) {
return sol::make_object(lua, sol::lua_nil);
}
wxString text = notebook->GetPageText(selection);
return sol::make_object(lua, text.ToStdString());
}
void LuaDialog::setBounds(sol::table bounds) {
int x = bounds.get_or(std::string("x"), GetPosition().x);
int y = bounds.get_or(std::string("y"), GetPosition().y);
int w = bounds.get_or(std::string("width"), GetSize().GetWidth());
int h = bounds.get_or(std::string("height"), GetSize().GetHeight());
SetSize(x, y, w, h);
}
void LuaDialog::updateValue(const std::string &id) {
for (auto &widget : widgets) {
if (widget.id != id) {
continue;
}
if (widget.type == "input") {
wxTextCtrl* ctrl = static_cast<wxTextCtrl*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetValue().ToStdString());
} else if (widget.type == "number") {
wxSpinCtrlDouble* ctrl = static_cast<wxSpinCtrlDouble*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetValue());
} else if (widget.type == "slider") {
wxSlider* ctrl = static_cast<wxSlider*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetValue());
} else if (widget.type == "check") {
wxCheckBox* ctrl = static_cast<wxCheckBox*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetValue());
} else if (widget.type == "radio") {
wxRadioButton* ctrl = static_cast<wxRadioButton*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetValue());
} else if (widget.type == "combobox") {
wxChoice* ctrl = static_cast<wxChoice*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetStringSelection().ToStdString());
} else if (widget.type == "color") {
wxColourPickerCtrl* ctrl = static_cast<wxColourPickerCtrl*>(widget.widget);
wxColour c = ctrl->GetColour();
sol::table colorTable = lua.create_table();
colorTable["red"] = c.Red();
colorTable["green"] = c.Green();
colorTable["blue"] = c.Blue();
values[id] = colorTable;
} else if (widget.type == "file") {
wxFilePickerCtrl* ctrl = static_cast<wxFilePickerCtrl*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetPath().ToStdString());
} else if (widget.type == "item") {
// Value is updated in the event handler
} else if (widget.type == "mapCanvas") {
auto ctrl = static_cast<MapPreviewCanvas*>(widget.widget);
sol::table mapData = lua.create_table();
mapData["x"] = ctrl->GetMapX();
mapData["y"] = ctrl->GetMapY();
mapData["z"] = ctrl->GetFloor();
mapData["zoom"] = ctrl->GetZoom();
values[id] = mapData;
} else if (widget.type == "list") {
auto ctrl = static_cast<LuaDialogListBox*>(widget.widget);
values[id] = sol::make_object(lua, ctrl->GetSelection() + 1);
} else if (widget.type == "grid") {
wxListCtrl* ctrl = static_cast<wxListCtrl*>(widget.widget);
long item = -1;
item = ctrl->GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
if (item != -1) {
values[id] = sol::make_object(lua, item + 1);
} else {
values[id] = sol::make_object(lua, 0);
}
}
break;
}
}
void LuaDialog::collectAllValues() {
for (const auto &widget : widgets) {
updateValue(widget.id);
}
}
int LuaDialog::getSizerFlags(sol::table options, int defaultFlags) {
int flags = defaultFlags;
if (options["align"].valid()) {
std::string align = options.get<std::string>("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<std::string>("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);
}
static wxColour parseColor(sol::object colorObj) {
if (colorObj.is<sol::table>()) {
sol::table c = colorObj.as<sol::table>();
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>()) {
std::string colorStr = colorObj.as<std::string>();
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];
char g = colorStr[2];
char 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();
}
void LuaDialog::applyFontOptions(wxWindow* widget, sol::table options) {
wxFont font = widget->GetFont();
if (options["font_size"].valid()) {
font.SetPointSize(options.get<int>("font_size"));
}
if (options["font_weight"].valid()) {
std::string weight = options.get<std::string>("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<std::string>("font_style");
if (style == "italic") {
font.SetStyle(wxFONTSTYLE_ITALIC);
} else {
font.SetStyle(wxFONTSTYLE_NORMAL);
}
}
widget->SetFont(font);
}
void LuaDialog::applyCommonOptions(wxWindow* widget, sol::table options) {
if (options["tooltip"].valid()) {
widget->SetToolTip(wxString(options.get<std::string>("tooltip")));
}
if (options["enabled"].valid()) {
widget->Enable(options.get<bool>("enabled"));
}
if (options["visible"].valid()) {
widget->Show(options.get<bool>("visible"));
}
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()) {
applyFontOptions(widget, options);
}
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 (!options.get_or("expand", true)) {
widget->SetMaxSize(size);
}
}
}
void LuaDialog::suspendHotkeys() {
if (hotkeySuspendCount == 0) {
if (g_gui.AreHotkeysEnabled()) {
g_gui.DisableHotkeys();
hotkeysDisabledByDialog = true;
}
}
++hotkeySuspendCount;
}
void LuaDialog::resumeHotkeys() {
if (hotkeySuspendCount <= 0) {
hotkeySuspendCount = 0;
return;
}
--hotkeySuspendCount;
if (hotkeySuspendCount == 0 && hotkeysDisabledByDialog) {
g_gui.EnableHotkeys();
hotkeysDisabledByDialog = false;
}
}
void LuaDialog::onWidgetChange(const std::string &id) {
updateValue(id);
// Call onchange callback if set
sol::function onchange;
for (auto &widget : widgets) {
if (widget.id == id && widget.onchange.valid()) {
onchange = widget.onchange;
break;
}
}
if (onchange.valid() && lua.lua_state() && onchange.lua_state()) {
try {
onchange(this);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
}
void LuaDialog::onButtonClick(const std::string &id) {
// Set button value to true (for detecting which button was clicked)
values[id] = sol::make_object(lua, true);
// Call onclick callback if set
sol::function onclick;
for (auto &widget : widgets) {
if (widget.id == id && widget.onclick.valid()) {
onclick = widget.onclick;
break;
}
}
if (onclick.valid() && lua.lua_state() && onclick.lua_state()) {
try {
onclick(this);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
return; // onclick handles closing if needed
}
// Default behavior: close dialog on button click (for OK/Cancel buttons)
std::string lowerId = id;
std::transform(lowerId.begin(), lowerId.end(), lowerId.begin(), ::tolower);
if (lowerId == "ok" || lowerId == "cancel" || lowerId == "close") {
close();
}
}
void LuaDialog::onWidgetDoubleClick(const std::string &id) {
updateValue(id);
sol::function ondoubleclick;
for (auto &widget : widgets) {
if (widget.id == id && widget.ondoubleclick.valid()) {
ondoubleclick = widget.ondoubleclick;
break;
}
}
if (ondoubleclick.valid() && lua.lua_state() && ondoubleclick.lua_state()) {
try {
ondoubleclick(this);
} catch (const sol::error &e) {
wxMessageBox(wxString("Script error: ") + e.what(), "Lua Error", wxOK | wxICON_ERROR);
}
}
}
namespace LuaAPI {
void registerDialog(sol::state &lua) {
// Register Dialog class with constructor and method chaining
lua.new_usertype<LuaDialog>("Dialog",
// Constructor: Dialog{title = "...", topmost = true}
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, "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,
// Properties
"data", sol::property(&LuaDialog::getData, &LuaDialog::setData), "bounds", sol::property(&LuaDialog::getBounds, &LuaDialog::setBounds), "dockable", sol::property(&LuaDialog::isDockable), "activeTab", sol::property(&LuaDialog::getActiveTab));
}
} // namespace LuaAPI