1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/app.h"
13#include "app/color.h"
14#include "app/color_utils.h"
15#include "app/file_selector.h"
16#include "app/script/engine.h"
17#include "app/script/luacpp.h"
18#include "app/ui/color_button.h"
19#include "app/ui/color_shades.h"
20#include "app/ui/expr_entry.h"
21#include "app/ui/filename_field.h"
22#include "app/ui/main_window.h"
23#include "base/paths.h"
24#include "base/remove_from_container.h"
25#include "ui/box.h"
26#include "ui/button.h"
27#include "ui/combobox.h"
28#include "ui/display.h"
29#include "ui/entry.h"
30#include "ui/grid.h"
31#include "ui/label.h"
32#include "ui/manager.h"
33#include "ui/separator.h"
34#include "ui/slider.h"
35#include "ui/window.h"
36
37#include <map>
38#include <string>
39#include <vector>
40
41#ifdef ENABLE_UI
42
43#define TRACE_DIALOG(...) // TRACEARGS(__VA_ARGS__)
44
45namespace app {
46namespace script {
47
48using namespace ui;
49
50namespace {
51
52struct Dialog;
53std::vector<Dialog*> all_dialogs;
54
55struct Dialog {
56 ui::Window window;
57 ui::VBox vbox;
58 ui::Grid grid;
59 ui::HBox* hbox = nullptr;
60 bool autoNewRow = false;
61 std::map<std::string, ui::Widget*> dataWidgets;
62 std::map<std::string, ui::Widget*> labelWidgets;
63 int currentRadioGroup = 0;
64
65 // Used to create a new row when a different kind of widget is added
66 // in the dialog.
67 ui::WidgetType lastWidgetType = ui::kGenericWidget;
68
69 // Used to keep a reference to the last onclick button pressed, so
70 // then Dialog.data returns true for the button that closed the
71 // dialog.
72 ui::Widget* lastButton = nullptr;
73
74 // Reference used to keep the dialog alive (so it's not garbage
75 // collected) when it's visible.
76 int showRef = LUA_REFNIL;
77 lua_State* L = nullptr;
78
79 Dialog()
80 : window(ui::Window::WithTitleBar, "Script"),
81 grid(2, false) {
82 window.addChild(&grid);
83 all_dialogs.push_back(this);
84 }
85
86 ~Dialog() {
87 base::remove_from_container(all_dialogs, this);
88 }
89
90 void unrefShowOnClose() {
91 window.Close.connect([this](ui::CloseEvent&){ unrefShow(); });
92 }
93
94 // When we show the dialog, we reference it from the registry to
95 // keep the dialog alive in case that the user declared it as a
96 // "local" variable but called Dialog:show{wait=false}
97 void refShow(lua_State* L) {
98 if (showRef == LUA_REFNIL) {
99 this->L = L;
100 lua_pushvalue(L, 1);
101 showRef = luaL_ref(L, LUA_REGISTRYINDEX);
102 }
103 }
104
105 // When the dialog is closed, we unreference it from the registry so
106 // now the dialog can be GC'd if there are no other references to it
107 // (all references to the dialog itself from callbacks are stored in
108 // the same dialog uservalue, so when the dialog+callbacks are not
109 // used anymore they are GC'd as a group)
110 void unrefShow() {
111 if (showRef != LUA_REFNIL) {
112 luaL_unref(this->L, LUA_REGISTRYINDEX, showRef);
113 showRef = LUA_REFNIL;
114 L = nullptr;
115 }
116 }
117
118 Widget* findDataWidgetById(const char* id) {
119 auto it = dataWidgets.find(id);
120 if (it != dataWidgets.end())
121 return it->second;
122 else
123 return nullptr;
124 }
125
126 void setLabelVisibility(const char* id, bool visible) {
127 auto it = labelWidgets.find(id);
128 if (it != labelWidgets.end())
129 it->second->setVisible(visible);
130 }
131
132 void setLabelText(const char* id, const char* text) {
133 auto it = labelWidgets.find(id);
134 if (it != labelWidgets.end())
135 it->second->setText(text);
136 }
137
138 gfx::Rect getWindowBounds() const {
139 gfx::Rect bounds = window.bounds();
140 // Bounds in scripts will be relative to the the main window
141 // origin/scale.
142 if (window.ownDisplay()) {
143 const auto mainWindow = App::instance()->mainWindow();
144 const int scale = mainWindow->display()->scale();
145 const gfx::Point dialogOrigin = window.display()->nativeWindow()->contentRect().origin();
146 const gfx::Point mainOrigin = mainWindow->display()->nativeWindow()->contentRect().origin();
147 bounds.setOrigin((dialogOrigin - mainOrigin) / scale);
148 }
149 return bounds;
150 }
151
152 void setWindowBounds(const gfx::Rect& rc) {
153 if (window.ownDisplay()) {
154 window.expandWindow(rc.size());
155
156 const auto mainWindow = App::instance()->mainWindow();
157 const int scale = mainWindow->display()->scale();
158 const gfx::Point mainOrigin = mainWindow->display()->nativeWindow()->contentRect().origin();
159 gfx::Rect frame = window.display()->nativeWindow()->contentRect();
160 frame.setOrigin(mainOrigin + rc.origin() * scale);
161 window.display()->nativeWindow()->setFrame(frame);
162 }
163 else {
164 window.setBounds(rc);
165 window.invalidate();
166 }
167 }
168
169};
170
171template<typename...Args,
172 typename Callback>
173void Dialog_connect_signal(lua_State* L,
174 int dlgIdx,
175 obs::signal<void(Args...)>& signal,
176 Callback callback)
177{
178 auto dlg = get_obj<Dialog>(L, dlgIdx);
179
180 // Here we get the uservalue of the dlg (the table with
181 // functions/callbacks) and store a copy of the given function in
182 // the stack (index=-1) in that table.
183 lua_getuservalue(L, dlgIdx);
184 lua_len(L, -1);
185 const int n = 1+lua_tointegerx(L, -1, nullptr);
186 lua_pop(L, 1); // Pop the length of the table
187 lua_pushvalue(L, -2); // Copy the function in stack
188 lua_rawseti(L, -2, n); // Put the copy of the function in the uservalue
189 lua_pop(L, 1); // Pop the uservalue
190
191 signal.connect(
192 [=](Args...args) {
193 // In case that the dialog is hidden, we cannot access to the
194 // global LUA_REGISTRYINDEX to get its reference.
195 if (dlg->showRef == LUA_REFNIL)
196 return;
197
198 try {
199 // Get the function "n" from the uservalue table of the dialog
200 lua_rawgeti(L, LUA_REGISTRYINDEX, dlg->showRef);
201 lua_getuservalue(L, -1);
202 lua_rawgeti(L, -1, n);
203
204 // Use the callback with a special table in the Lua stack to
205 // send it as parameter to the Lua function in the
206 // lua_pcall() (that table is like an "event data" parameter
207 // for the function).
208 lua_newtable(L);
209 callback(L, std::forward<Args>(args)...);
210
211 if (lua_isfunction(L, -2)) {
212 if (lua_pcall(L, 1, 0, 0)) {
213 if (const char* s = lua_tostring(L, -1))
214 App::instance()
215 ->scriptEngine()
216 ->consolePrint(s);
217 }
218 }
219 else {
220 lua_pop(L, 1); // Pop the value which should have been a function
221 }
222 lua_pop(L, 2); // Pop uservalue & userdata
223 }
224 catch (const std::exception& ex) {
225 // This is used to catch unhandled exception or for
226 // example, std::runtime_error exceptions when a Tx() is
227 // created without an active sprite.
228 App::instance()
229 ->scriptEngine()
230 ->consolePrint(ex.what());
231 }
232 });
233}
234
235int Dialog_new(lua_State* L)
236{
237 // If we don't have UI, just return nil
238 if (!App::instance()->isGui())
239 return 0;
240
241 auto dlg = push_new<Dialog>(L);
242
243 // The uservalue of the dialog userdata will contain a table that
244 // stores all the callbacks to handle events. As these callbacks can
245 // reference the dialog itself, it's important to store callbacks in
246 // this table that depends on the dialog lifetime itself
247 // (i.e. uservalue) and in the global registry, because in that case
248 // we could create a cyclic reference that would be not GC'd.
249 lua_newtable(L);
250 lua_setuservalue(L, -2);
251
252 if (lua_isstring(L, 1)) {
253 dlg->window.setText(lua_tostring(L, 1));
254 }
255 else if (lua_istable(L, 1)) {
256 int type = lua_getfield(L, 1, "title");
257 if (type != LUA_TNIL)
258 dlg->window.setText(lua_tostring(L, -1));
259 lua_pop(L, 1);
260
261 type = lua_getfield(L, 1, "onclose");
262 if (type == LUA_TFUNCTION) {
263 Dialog_connect_signal(
264 L, -2, dlg->window.Close,
265 [](lua_State*, CloseEvent&){
266 // Do nothing
267 });
268 }
269 lua_pop(L, 1);
270 }
271
272 // The showRef must be the last reference to the dialog to be
273 // unreferenced after the window is closed (that's why this is the
274 // last connection to ui::Window::Close)
275 dlg->unrefShowOnClose();
276
277 TRACE_DIALOG("Dialog_new", dlg);
278 return 1;
279}
280
281int Dialog_gc(lua_State* L)
282{
283 auto dlg = get_obj<Dialog>(L, 1);
284 TRACE_DIALOG("Dialog_gc", dlg);
285 dlg->~Dialog();
286 return 0;
287}
288
289int Dialog_show(lua_State* L)
290{
291 auto dlg = get_obj<Dialog>(L, 1);
292 dlg->refShow(L);
293
294 bool wait = true;
295 obs::scoped_connection conn;
296 if (lua_istable(L, 2)) {
297 int type = lua_getfield(L, 2, "wait");
298 if (type == LUA_TBOOLEAN)
299 wait = lua_toboolean(L, -1);
300 lua_pop(L, 1);
301
302 type = lua_getfield(L, 2, "bounds");
303 if (VALID_LUATYPE(type)) {
304 const auto rc = convert_args_into_rect(L, -1);
305 if (!rc.isEmpty()) {
306 conn = dlg->window.Open.connect([dlg, rc]{
307 dlg->setWindowBounds(rc);
308 });
309 }
310 }
311 lua_pop(L, 1);
312 }
313
314 if (wait)
315 dlg->window.openWindowInForeground();
316 else
317 dlg->window.openWindow();
318
319 lua_pushvalue(L, 1);
320 return 1;
321}
322
323int Dialog_close(lua_State* L)
324{
325 auto dlg = get_obj<Dialog>(L, 1);
326 dlg->window.closeWindow(nullptr);
327 lua_pushvalue(L, 1);
328 return 1;
329}
330
331int Dialog_add_widget(lua_State* L, Widget* widget)
332{
333 auto dlg = get_obj<Dialog>(L, 1);
334 const char* label = nullptr;
335 std::string id;
336 bool visible = true;
337
338 // This is to separate different kind of widgets without label in
339 // different rows.
340 if (dlg->lastWidgetType != widget->type() ||
341 dlg->autoNewRow) {
342 dlg->lastWidgetType = widget->type();
343 dlg->hbox = nullptr;
344 }
345
346 if (lua_istable(L, 2)) {
347 // Widget ID (used to fill the Dialog_get_data table then)
348 int type = lua_getfield(L, 2, "id");
349 if (type == LUA_TSTRING) {
350 if (auto s = lua_tostring(L, -1)) {
351 id = s;
352 widget->setId(s);
353 dlg->dataWidgets[id] = widget;
354 }
355 }
356 lua_pop(L, 1);
357
358 // Label
359 type = lua_getfield(L, 2, "label");
360 if (type != LUA_TNIL)
361 label = lua_tostring(L, -1);
362 lua_pop(L, 1);
363
364 // Focus magnet
365 type = lua_getfield(L, 2, "focus");
366 if (type != LUA_TNIL && lua_toboolean(L, -1))
367 widget->setFocusMagnet(true);
368 lua_pop(L, 1);
369
370 // Enabled
371 type = lua_getfield(L, 2, "enabled");
372 if (type != LUA_TNIL)
373 widget->setEnabled(lua_toboolean(L, -1));
374 lua_pop(L, 1);
375
376 // Visible
377 type = lua_getfield(L, 2, "visible");
378 if (type != LUA_TNIL) {
379 visible = lua_toboolean(L, -1);
380 widget->setVisible(visible);
381 }
382 lua_pop(L, 1);
383 }
384
385 if (label || !dlg->hbox) {
386 if (label) {
387 auto labelWidget = new ui::Label(label);
388 if (!visible)
389 labelWidget->setVisible(false);
390
391 dlg->grid.addChildInCell(labelWidget, 1, 1, ui::LEFT | ui::TOP);
392 if (!id.empty())
393 dlg->labelWidgets[id] = labelWidget;
394 }
395 else
396 dlg->grid.addChildInCell(new ui::HBox, 1, 1, ui::LEFT | ui::TOP);
397
398 auto hbox = new ui::HBox;
399 if (widget->type() == ui::kButtonWidget)
400 hbox->enableFlags(ui::HOMOGENEOUS);
401 dlg->grid.addChildInCell(hbox, 1, 1, ui::HORIZONTAL | ui::TOP);
402 dlg->hbox = hbox;
403 }
404
405 widget->setExpansive(true);
406 dlg->hbox->addChild(widget);
407
408 lua_pushvalue(L, 1);
409 return 1;
410}
411
412int Dialog_newrow(lua_State* L)
413{
414 auto dlg = get_obj<Dialog>(L, 1);
415 dlg->hbox = nullptr;
416
417 dlg->autoNewRow = false;
418 if (lua_istable(L, 2)) {
419 // Dialog:newrow{ always }
420 if (lua_is_key_true(L, 2, "always"))
421 dlg->autoNewRow = true;
422 lua_pop(L, 1);
423 }
424
425 lua_pushvalue(L, 1);
426 return 1;
427}
428
429int Dialog_separator(lua_State* L)
430{
431 auto dlg = get_obj<Dialog>(L, 1);
432
433 std::string id, text;
434
435 if (lua_isstring(L, 2)) {
436 if (auto p = lua_tostring(L, 2))
437 text = p;
438 }
439 else if (lua_istable(L, 2)) {
440 int type = lua_getfield(L, 2, "text");
441 if (type == LUA_TSTRING) {
442 if (auto p = lua_tostring(L, -1))
443 text = p;
444 }
445 lua_pop(L, 1);
446
447 type = lua_getfield(L, 2, "id");
448 if (type == LUA_TSTRING) {
449 if (auto s = lua_tostring(L, -1))
450 id = s;
451 }
452 lua_pop(L, 1);
453 }
454
455 auto widget = new ui::Separator(text, ui::HORIZONTAL);
456 if (!id.empty()) {
457 widget->setId(id.c_str());
458 dlg->dataWidgets[id] = widget;
459 }
460
461 dlg->grid.addChildInCell(widget, 2, 1, ui::HORIZONTAL | ui::TOP);
462 dlg->hbox = nullptr;
463
464 lua_pushvalue(L, 1);
465 return 1;
466}
467
468int Dialog_label(lua_State* L)
469{
470 std::string text;
471 if (lua_istable(L, 2)) {
472 int type = lua_getfield(L, 2, "text");
473 if (type != LUA_TNIL) {
474 if (auto p = lua_tostring(L, -1))
475 text = p;
476 }
477 lua_pop(L, 1);
478 }
479
480 auto widget = new ui::Label(text.c_str());
481 return Dialog_add_widget(L, widget);
482}
483
484template<typename T>
485int Dialog_button_base(lua_State* L, T** outputWidget = nullptr)
486{
487 std::string text;
488 if (lua_istable(L, 2)) {
489 int type = lua_getfield(L, 2, "text");
490 if (type != LUA_TNIL) {
491 if (auto p = lua_tostring(L, -1))
492 text = p;
493 }
494 lua_pop(L, 1);
495 }
496
497 auto widget = new T(text.c_str());
498 if (outputWidget)
499 *outputWidget = widget;
500
501 widget->processMnemonicFromText();
502
503 bool closeWindowByDefault = (widget->type() == ui::kButtonWidget);
504
505 if (lua_istable(L, 2)) {
506 int type = lua_getfield(L, 2, "selected");
507 if (type != LUA_TNIL)
508 widget->setSelected(lua_toboolean(L, -1));
509 lua_pop(L, 1);
510
511 type = lua_getfield(L, 2, "onclick");
512 if (type == LUA_TFUNCTION) {
513 auto dlg = get_obj<Dialog>(L, 1);
514 Dialog_connect_signal(
515 L, 1, widget->Click,
516 [dlg, widget](lua_State* L, Event&){
517 if (widget->type() == ui::kButtonWidget)
518 dlg->lastButton = widget;
519 });
520 closeWindowByDefault = false;
521 }
522 lua_pop(L, 1);
523 }
524
525 if (closeWindowByDefault)
526 widget->Click.connect([widget](ui::Event&){ widget->closeWindow(); });
527
528 return Dialog_add_widget(L, widget);
529}
530
531int Dialog_button(lua_State* L)
532{
533 return Dialog_button_base<ui::Button>(L);
534}
535
536int Dialog_check(lua_State* L)
537{
538 return Dialog_button_base<ui::CheckBox>(L);
539}
540
541int Dialog_radio(lua_State* L)
542{
543 ui::RadioButton* radio = nullptr;
544 const int res = Dialog_button_base<ui::RadioButton>(L, &radio);
545 if (radio) {
546 auto dlg = get_obj<Dialog>(L, 1);
547 bool hasLabelField = false;
548
549 if (lua_istable(L, 2)) {
550 int type = lua_getfield(L, 2, "label");
551 if (type == LUA_TSTRING)
552 hasLabelField = true;
553 lua_pop(L, 1);
554 }
555
556 if (dlg->currentRadioGroup == 0 ||
557 hasLabelField) {
558 ++dlg->currentRadioGroup;
559 }
560
561 radio->setRadioGroup(dlg->currentRadioGroup);
562 }
563 return res;
564}
565
566int Dialog_entry(lua_State* L)
567{
568 std::string text;
569 if (lua_istable(L, 2)) {
570 int type = lua_getfield(L, 2, "text");
571 if (type == LUA_TSTRING) {
572 if (auto p = lua_tostring(L, -1))
573 text = p;
574 }
575 lua_pop(L, 1);
576 }
577
578 auto widget = new ui::Entry(4096, text.c_str());
579
580 if (lua_istable(L, 2)) {
581 int type = lua_getfield(L, 2, "onchange");
582 if (type == LUA_TFUNCTION) {
583 Dialog_connect_signal(
584 L, 1, widget->Change,
585 [](lua_State* L){
586 // Do nothing
587 });
588 }
589 lua_pop(L, 1);
590 }
591
592 return Dialog_add_widget(L, widget);
593}
594
595int Dialog_number(lua_State* L)
596{
597 auto widget = new ExprEntry;
598
599 if (lua_istable(L, 2)) {
600 int type = lua_getfield(L, 2, "text");
601 if (type == LUA_TSTRING) {
602 if (auto p = lua_tostring(L, -1))
603 widget->setText(p);
604 }
605 lua_pop(L, 1);
606
607 type = lua_getfield(L, 2, "decimals");
608 if (type != LUA_TNIL) {
609 widget->setDecimals(lua_tointegerx(L, -1, nullptr));
610 }
611 lua_pop(L, 1);
612
613 type = lua_getfield(L, 2, "onchange");
614 if (type == LUA_TFUNCTION) {
615 Dialog_connect_signal(
616 L, 1, widget->Change,
617 [](lua_State* L){
618 // Do nothing
619 });
620 }
621 lua_pop(L, 1);
622 }
623
624 return Dialog_add_widget(L, widget);
625}
626
627int Dialog_slider(lua_State* L)
628{
629 int min = 0;
630 int max = 100;
631 int value = 100;
632
633 if (lua_istable(L, 2)) {
634 int type = lua_getfield(L, 2, "min");
635 if (type != LUA_TNIL) {
636 min = lua_tointegerx(L, -1, nullptr);
637 }
638 lua_pop(L, 1);
639
640 type = lua_getfield(L, 2, "max");
641 if (type != LUA_TNIL) {
642 max = lua_tointegerx(L, -1, nullptr);
643 }
644 lua_pop(L, 1);
645
646 type = lua_getfield(L, 2, "value");
647 if (type != LUA_TNIL) {
648 value = lua_tointegerx(L, -1, nullptr);
649 }
650 lua_pop(L, 1);
651 }
652
653 auto widget = new ui::Slider(min, max, value);
654
655 if (lua_istable(L, 2)) {
656 int type = lua_getfield(L, 2, "onchange");
657 if (type == LUA_TFUNCTION) {
658 Dialog_connect_signal(
659 L, 1, widget->Change,
660 [](lua_State* L){
661 // Do nothing
662 });
663 }
664 lua_pop(L, 1);
665
666 type = lua_getfield(L, 2, "onrelease");
667 if (type == LUA_TFUNCTION) {
668 Dialog_connect_signal(
669 L, 1, widget->SliderReleased,
670 [](lua_State* L){
671 // Do nothing
672 });
673 }
674 lua_pop(L, 1);
675 }
676
677 return Dialog_add_widget(L, widget);
678}
679
680int Dialog_combobox(lua_State* L)
681{
682 auto widget = new ui::ComboBox;
683
684 if (lua_istable(L, 2)) {
685 int type = lua_getfield(L, 2, "options");
686 if (type == LUA_TTABLE) {
687 lua_pushnil(L);
688 while (lua_next(L, -2) != 0) {
689 if (auto p = lua_tostring(L, -1))
690 widget->addItem(p);
691 lua_pop(L, 1);
692 }
693 }
694 lua_pop(L, 1);
695
696 type = lua_getfield(L, 2, "option");
697 if (type == LUA_TSTRING) {
698 if (auto p = lua_tostring(L, -1)) {
699 int index = widget->findItemIndex(p);
700 if (index >= 0)
701 widget->setSelectedItemIndex(index);
702 }
703 }
704 lua_pop(L, 1);
705
706 type = lua_getfield(L, 2, "onchange");
707 if (type == LUA_TFUNCTION) {
708 Dialog_connect_signal(
709 L, 1, widget->Change,
710 [](lua_State* L){
711 // Do nothing
712 });
713 }
714 lua_pop(L, 1);
715 }
716
717 return Dialog_add_widget(L, widget);
718}
719
720int Dialog_color(lua_State* L)
721{
722 app::Color color;
723 if (lua_istable(L, 2)) {
724 lua_getfield(L, 2, "color");
725 color = convert_args_into_color(L, -1);
726 lua_pop(L, 1);
727 }
728
729 auto widget = new ColorButton(color,
730 app_get_current_pixel_format(),
731 ColorButtonOptions());
732
733 if (lua_istable(L, 2)) {
734 int type = lua_getfield(L, 2, "onchange");
735 if (type == LUA_TFUNCTION) {
736 Dialog_connect_signal(
737 L, 1, widget->Change,
738 [](lua_State* L, const app::Color& color){
739 push_obj<app::Color>(L, color);
740 lua_setfield(L, -2, "color");
741 });
742 }
743 }
744
745 return Dialog_add_widget(L, widget);
746}
747
748int Dialog_shades(lua_State* L)
749{
750 Shade colors;
751 // 'pick' is the default mode anyway
752 ColorShades::ClickType mode = ColorShades::ClickEntries;
753
754 if (lua_istable(L, 2)) {
755 int type = lua_getfield(L, 2, "mode");
756 if (type == LUA_TSTRING) {
757 if (const char* modeStr = lua_tostring(L, -1)) {
758 if (base::utf8_icmp(modeStr, "pick") == 0)
759 mode = ColorShades::ClickEntries;
760 else if (base::utf8_icmp(modeStr, "sort") == 0)
761 mode = ColorShades::DragAndDropEntries;
762 }
763 }
764 lua_pop(L, 1);
765
766 type = lua_getfield(L, 2, "colors");
767 if (type == LUA_TTABLE) {
768 lua_pushnil(L);
769 while (lua_next(L, -2) != 0) {
770 app::Color color = convert_args_into_color(L, -1);
771 colors.push_back(color);
772 lua_pop(L, 1);
773 }
774 }
775 lua_pop(L, 1);
776 }
777
778 auto widget = new ColorShades(colors, mode);
779
780 if (lua_istable(L, 2)) {
781 int type = lua_getfield(L, 2, "onclick");
782 if (type == LUA_TFUNCTION) {
783 Dialog_connect_signal(
784 L, 1, widget->Click,
785 [widget](lua_State* L, ColorShades::ClickEvent& ev){
786 lua_pushinteger(L, (int)ev.button());
787 lua_setfield(L, -2, "button");
788
789 const int i = widget->getHotEntry();
790 const Shade shade = widget->getShade();
791 if (i >= 0 && i < int(shade.size())) {
792 push_obj<app::Color>(L, shade[i]);
793 lua_setfield(L, -2, "color");
794 }
795 });
796 }
797 lua_pop(L, 1);
798 }
799
800 return Dialog_add_widget(L, widget);
801}
802
803int Dialog_file(lua_State* L)
804{
805 std::string title = "Open File";
806 std::string fn;
807 base::paths exts;
808 auto dlgType = FileSelectorType::Open;
809 auto fnFieldType = FilenameField::ButtonOnly;
810
811 if (lua_istable(L, 2)) {
812 lua_getfield(L, 2, "filename");
813 if (auto p = lua_tostring(L, -1))
814 fn = p;
815 lua_pop(L, 1);
816
817 int type = lua_getfield(L, 2, "save");
818 if (type == LUA_TBOOLEAN && lua_toboolean(L, -1)) {
819 dlgType = FileSelectorType::Save;
820 title = "Save File";
821 }
822 lua_pop(L, 1);
823
824 type = lua_getfield(L, 2, "title");
825 if (type == LUA_TSTRING)
826 title = lua_tostring(L, -1);
827 lua_pop(L, 1);
828
829 type = lua_getfield(L, 2, "entry");
830 if (type == LUA_TBOOLEAN) {
831 fnFieldType = FilenameField::EntryAndButton;
832 }
833 lua_pop(L, 1);
834
835 type = lua_getfield(L, 2, "filetypes");
836 if (type == LUA_TTABLE) {
837 lua_pushnil(L);
838 while (lua_next(L, -2) != 0) {
839 if (auto p = lua_tostring(L, -1))
840 exts.push_back(p);
841 lua_pop(L, 1);
842 }
843 }
844 lua_pop(L, 1);
845 }
846
847 auto widget = new FilenameField(fnFieldType, fn);
848
849 if (lua_istable(L, 2)) {
850 int type = lua_getfield(L, 2, "onchange");
851 if (type == LUA_TFUNCTION) {
852 Dialog_connect_signal(
853 L, 1, widget->Change,
854 [](lua_State* L){
855 // Do nothing
856 });
857 }
858 lua_pop(L, 1);
859 }
860
861 widget->SelectFile.connect(
862 [=]() -> std::string {
863 base::paths newfilename;
864 if (app::show_file_selector(
865 title, widget->filename(), exts,
866 dlgType,
867 newfilename))
868 return newfilename.front();
869 else
870 return widget->filename();
871 });
872 return Dialog_add_widget(L, widget);
873}
874
875int Dialog_modify(lua_State* L)
876{
877 auto dlg = get_obj<Dialog>(L, 1);
878 if (lua_istable(L, 2)) {
879 const char* id = nullptr;
880 bool relayout = false;
881
882 int type = lua_getfield(L, 2, "id");
883 if (type != LUA_TNIL)
884 id = lua_tostring(L, -1);
885 lua_pop(L, 1);
886
887 // Modify window itself when no ID is specified
888 if (id == nullptr) {
889 // "title" or "text" is the same for dialogs
890 type = lua_getfield(L, 2, "title");
891 if (type == LUA_TNIL) {
892 lua_pop(L, 1);
893 type = lua_getfield(L, 2, "text");
894 }
895 if (const char* s = lua_tostring(L, -1)) {
896 dlg->window.setText(s);
897 relayout = true;
898 }
899 lua_pop(L, 1);
900 return 0;
901 }
902
903 // Here we could use dlg->window.findChild(id) but why not use the
904 // map directly (it should be faster than iterating over all
905 // children).
906 Widget* widget = dlg->findDataWidgetById(id);
907 if (!widget)
908 return luaL_error(L, "Given id=\"%s\" in Dialog:modify{} not found in dialog", id);
909
910 type = lua_getfield(L, 2, "enabled");
911 if (type != LUA_TNIL)
912 widget->setEnabled(lua_toboolean(L, -1));
913 lua_pop(L, 1);
914
915 type = lua_getfield(L, 2, "selected");
916 if (type != LUA_TNIL)
917 widget->setSelected(lua_toboolean(L, -1));
918 lua_pop(L, 1);
919
920 type = lua_getfield(L, 2, "visible");
921 if (type != LUA_TNIL) {
922 bool state = lua_toboolean(L, -1);
923 widget->setVisible(state);
924 dlg->setLabelVisibility(id, state);
925 relayout = true;
926 }
927 lua_pop(L, 1);
928
929 type = lua_getfield(L, 2, "text");
930 if (const char* s = lua_tostring(L, -1)) {
931 widget->setText(s);
932 relayout = true;
933 }
934 lua_pop(L, 1);
935
936 type = lua_getfield(L, 2, "label");
937 if (const char* s = lua_tostring(L, -1)) {
938 dlg->setLabelText(id, s);
939 relayout = true;
940 }
941 lua_pop(L, 1);
942
943 type = lua_getfield(L, 2, "focus");
944 if (type != LUA_TNIL && lua_toboolean(L, -1)) {
945 widget->requestFocus();
946 relayout = true;
947 }
948 lua_pop(L, 1);
949
950 type = lua_getfield(L, 2, "decimals");
951 if (type != LUA_TNIL) {
952 if (auto expr = dynamic_cast<ExprEntry*>(widget)) {
953 expr->setDecimals(lua_tointegerx(L, -1, nullptr));
954 }
955 }
956 lua_pop(L, 1);
957
958 type = lua_getfield(L, 2, "min");
959 if (type != LUA_TNIL) {
960 if (auto slider = dynamic_cast<ui::Slider*>(widget)) {
961 slider->setRange(lua_tointegerx(L, -1, nullptr), slider->getMaxValue());
962 }
963 }
964 lua_pop(L, 1);
965
966 type = lua_getfield(L, 2, "max");
967 if (type != LUA_TNIL) {
968 if (auto slider = dynamic_cast<ui::Slider*>(widget)) {
969 slider->setRange(slider->getMinValue(), lua_tointegerx(L, -1, nullptr));
970 }
971 }
972 lua_pop(L, 1);
973
974 type = lua_getfield(L, 2, "value");
975 if (type != LUA_TNIL) {
976 if (auto slider = dynamic_cast<ui::Slider*>(widget)) {
977 slider->setValue(lua_tointegerx(L, -1, nullptr));
978 }
979 }
980 lua_pop(L, 1);
981
982 // Handling options before option should support
983 // using both or only one of them at the same time
984 type = lua_getfield(L, 2, "options");
985 if (type != LUA_TNIL) {
986 if (lua_istable(L, -1)) {
987 if (auto combobox = dynamic_cast<ui::ComboBox*>(widget)) {
988 combobox->deleteAllItems();
989 lua_pushnil(L);
990 bool empty = true;
991 while (lua_next(L, -2) != 0) {
992 if (auto p = lua_tostring(L, -1)) {
993 combobox->addItem(p);
994 empty = false;
995 }
996 lua_pop(L, 1);
997 }
998 if (empty)
999 combobox->getEntryWidget()->setText("");
1000 }
1001 }
1002 }
1003 lua_pop(L, 1);
1004
1005 type = lua_getfield(L, 2, "option");
1006 if (auto p = lua_tostring(L, -1)) {
1007 if (auto combobox = dynamic_cast<ui::ComboBox*>(widget)) {
1008 int index = combobox->findItemIndex(p);
1009 if (index >= 0)
1010 combobox->setSelectedItemIndex(index);
1011 }
1012 }
1013 lua_pop(L, 1);
1014
1015 type = lua_getfield(L, 2, "color");
1016 if (type != LUA_TNIL) {
1017 if (auto colorButton = dynamic_cast<ColorButton*>(widget)) {
1018 colorButton->setColor(convert_args_into_color(L, -1));
1019 }
1020 }
1021 lua_pop(L, 1);
1022
1023 type = lua_getfield(L, 2, "colors");
1024 if (type != LUA_TNIL) {
1025 if (auto colorShade = dynamic_cast<ColorShades*>(widget)) {
1026 Shade shade;
1027 if (lua_istable(L, -1)) {
1028 lua_pushnil(L);
1029 while (lua_next(L, -2) != 0) {
1030 app::Color color = convert_args_into_color(L, -1);
1031 shade.push_back(color);
1032 lua_pop(L, 1);
1033 }
1034 }
1035 colorShade->setShade(shade);
1036 }
1037 }
1038 lua_pop(L, 1);
1039
1040 type = lua_getfield(L, 2, "filename");
1041 if (auto p = lua_tostring(L, -1)) {
1042 if (auto filenameField = dynamic_cast<FilenameField*>(widget)) {
1043 filenameField->setFilename(p);
1044 }
1045 }
1046 lua_pop(L, 1);
1047
1048 // TODO shades mode? file title / open / save / filetypes? on* events?
1049
1050 if (relayout) {
1051 dlg->window.layout();
1052
1053 gfx::Rect bounds(dlg->window.bounds().w,
1054 dlg->window.sizeHint().h);
1055 dlg->window.expandWindow(bounds.size());
1056 }
1057 }
1058 lua_pushvalue(L, 1);
1059 return 1;
1060}
1061
1062int Dialog_get_data(lua_State* L)
1063{
1064 auto dlg = get_obj<Dialog>(L, 1);
1065 lua_newtable(L);
1066 for (const auto& kv : dlg->dataWidgets) {
1067 const ui::Widget* widget = kv.second;
1068 switch (widget->type()) {
1069 case ui::kSeparatorWidget:
1070 // Do nothing
1071 continue;
1072 case ui::kButtonWidget:
1073 case ui::kCheckWidget:
1074 case ui::kRadioWidget:
1075 lua_pushboolean(L, widget->isSelected() ||
1076 dlg->window.closer() == widget ||
1077 dlg->lastButton == widget);
1078 break;
1079 case ui::kEntryWidget:
1080 if (auto expr = dynamic_cast<const ExprEntry*>(widget)) {
1081 if (expr->decimals() == 0)
1082 lua_pushinteger(L, widget->textInt());
1083 else
1084 lua_pushnumber(L, widget->textDouble());
1085 }
1086 else {
1087 lua_pushstring(L, widget->text().c_str());
1088 }
1089 break;
1090 case ui::kLabelWidget:
1091 lua_pushstring(L, widget->text().c_str());
1092 break;
1093 case ui::kSliderWidget:
1094 if (auto slider = dynamic_cast<const ui::Slider*>(widget)) {
1095 lua_pushinteger(L, slider->getValue());
1096 }
1097 break;
1098 case ui::kComboBoxWidget:
1099 if (auto combobox = dynamic_cast<const ui::ComboBox*>(widget)) {
1100 if (auto sel = combobox->getSelectedItem())
1101 lua_pushstring(L, sel->text().c_str());
1102 else
1103 lua_pushnil(L);
1104 }
1105 break;
1106 default:
1107 if (auto colorButton = dynamic_cast<const ColorButton*>(widget)) {
1108 push_obj<app::Color>(L, colorButton->getColor());
1109 }
1110 else if (auto colorShade = dynamic_cast<const ColorShades*>(widget)) {
1111 switch (colorShade->clickType()) {
1112
1113 case ColorShades::ClickEntries: {
1114 Shade shade = colorShade->getShade();
1115 int i = colorShade->getHotEntry();
1116 if (i >= 0 && i < int(shade.size()))
1117 push_obj<app::Color>(L, shade[i]);
1118 else
1119 lua_pushnil(L);
1120 break;
1121 }
1122
1123 case ColorShades::DragAndDropEntries: {
1124 lua_newtable(L);
1125 Shade shade = colorShade->getShade();
1126 for (int i=0; i<int(shade.size()); ++i) {
1127 push_obj<app::Color>(L, shade[i]);
1128 lua_rawseti(L, -2, i+1);
1129 }
1130 break;
1131 }
1132
1133 default:
1134 lua_pushnil(L);
1135 break;
1136
1137 }
1138 }
1139 else if (auto filenameField = dynamic_cast<const FilenameField*>(widget)) {
1140 lua_pushstring(L, filenameField->filename().c_str());
1141 }
1142 else {
1143 lua_pushnil(L);
1144 }
1145 break;
1146 }
1147 lua_setfield(L, -2, kv.first.c_str());
1148 }
1149 return 1;
1150}
1151
1152int Dialog_set_data(lua_State* L)
1153{
1154 auto dlg = get_obj<Dialog>(L, 1);
1155 if (!lua_istable(L, 2))
1156 return 0;
1157 for (const auto& kv : dlg->dataWidgets) {
1158 lua_getfield(L, 2, kv.first.c_str());
1159
1160 ui::Widget* widget = kv.second;
1161 switch (widget->type()) {
1162 case ui::kSeparatorWidget:
1163 // Do nothing
1164 break;
1165 case ui::kButtonWidget:
1166 case ui::kCheckWidget:
1167 case ui::kRadioWidget:
1168 widget->setSelected(lua_toboolean(L, -1));
1169 break;
1170 case ui::kEntryWidget:
1171 if (auto expr = dynamic_cast<ExprEntry*>(widget)) {
1172 if (expr->decimals() == 0)
1173 expr->setTextf("%d", lua_tointeger(L, -1));
1174 else
1175 expr->setTextf("%.*g", expr->decimals(), lua_tonumber(L, -1));
1176 }
1177 else if (auto p = lua_tostring(L, -1)) {
1178 widget->setText(p);
1179 }
1180 break;
1181 case ui::kLabelWidget:
1182 if (auto p = lua_tostring(L, -1)) {
1183 widget->setText(p);
1184 }
1185 break;
1186 case ui::kSliderWidget:
1187 if (auto slider = dynamic_cast<ui::Slider*>(widget)) {
1188 slider->setValue(lua_tointeger(L, -1));
1189 }
1190 break;
1191 case ui::kComboBoxWidget:
1192 if (auto combobox = dynamic_cast<ui::ComboBox*>(widget)) {
1193 if (auto p = lua_tostring(L, -1)) {
1194 int index = combobox->findItemIndex(p);
1195 if (index >= 0)
1196 combobox->setSelectedItemIndex(index);
1197 }
1198 }
1199 break;
1200 default:
1201 if (auto colorButton = dynamic_cast<ColorButton*>(widget)) {
1202 colorButton->setColor(convert_args_into_color(L, -1));
1203 }
1204 else if (auto colorShade = dynamic_cast<ColorShades*>(widget)) {
1205 switch (colorShade->clickType()) {
1206
1207 case ColorShades::ClickEntries: {
1208 // TODO change hot entry?
1209 break;
1210 }
1211
1212 case ColorShades::DragAndDropEntries: {
1213 Shade shade;
1214 if (lua_istable(L, -1)) {
1215 lua_pushnil(L);
1216 while (lua_next(L, -2) != 0) {
1217 app::Color color = convert_args_into_color(L, -1);
1218 shade.push_back(color);
1219 lua_pop(L, 1);
1220 }
1221 }
1222 colorShade->setShade(shade);
1223 break;
1224 }
1225
1226 }
1227 }
1228 else if (auto filenameField = dynamic_cast<FilenameField*>(widget)) {
1229 if (auto p = lua_tostring(L, -1))
1230 filenameField->setFilename(p);
1231 }
1232 break;
1233 }
1234
1235 lua_pop(L, 1);
1236 }
1237 return 1;
1238}
1239
1240int Dialog_get_bounds(lua_State* L)
1241{
1242 auto dlg = get_obj<Dialog>(L, 1);
1243 if (!dlg->window.isVisible())
1244 dlg->window.remapWindow();
1245
1246 push_new<gfx::Rect>(L, dlg->getWindowBounds());
1247 return 1;
1248}
1249
1250int Dialog_set_bounds(lua_State* L)
1251{
1252 auto dlg = get_obj<Dialog>(L, 1);
1253 const auto rc = get_obj<gfx::Rect>(L, 2);
1254 if (rc) {
1255 if (*rc != dlg->getWindowBounds())
1256 dlg->setWindowBounds(*rc);
1257 }
1258 return 0;
1259}
1260
1261const luaL_Reg Dialog_methods[] = {
1262 { "__gc", Dialog_gc },
1263 { "show", Dialog_show },
1264 { "close", Dialog_close },
1265 { "newrow", Dialog_newrow },
1266 { "separator", Dialog_separator },
1267 { "label", Dialog_label },
1268 { "button", Dialog_button },
1269 { "check", Dialog_check },
1270 { "radio", Dialog_radio },
1271 { "entry", Dialog_entry },
1272 { "number", Dialog_number },
1273 { "slider", Dialog_slider },
1274 { "combobox", Dialog_combobox },
1275 { "color", Dialog_color },
1276 { "shades", Dialog_shades },
1277 { "file", Dialog_file },
1278 { "modify", Dialog_modify },
1279 { nullptr, nullptr }
1280};
1281
1282const Property Dialog_properties[] = {
1283 { "data", Dialog_get_data, Dialog_set_data },
1284 { "bounds", Dialog_get_bounds, Dialog_set_bounds },
1285 { nullptr, nullptr, nullptr }
1286};
1287
1288} // anonymous namespace
1289
1290DEF_MTNAME(Dialog);
1291
1292void register_dialog_class(lua_State* L)
1293{
1294 REG_CLASS(L, Dialog);
1295 REG_CLASS_NEW(L, Dialog);
1296 REG_CLASS_PROPERTIES(L, Dialog);
1297}
1298
1299// close all opened Dialogs before closing the UI
1300void close_all_dialogs()
1301{
1302 for (Dialog* dlg : all_dialogs) {
1303 ASSERT(dlg);
1304 if (dlg)
1305 dlg->window.closeWindow(nullptr);
1306 }
1307}
1308
1309} // namespace script
1310} // namespace app
1311
1312#endif // ENABLE_UI
1313