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 | |
45 | namespace app { |
46 | namespace script { |
47 | |
48 | using namespace ui; |
49 | |
50 | namespace { |
51 | |
52 | struct Dialog; |
53 | std::vector<Dialog*> all_dialogs; |
54 | |
55 | struct 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 | |
171 | template<typename...Args, |
172 | typename Callback> |
173 | void 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 | |
235 | int 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 | |
281 | int 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 | |
289 | int 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 | |
323 | int 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 | |
331 | int 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 | |
412 | int 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 | |
429 | int 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 | |
468 | int 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 | |
484 | template<typename T> |
485 | int 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 | |
531 | int Dialog_button(lua_State* L) |
532 | { |
533 | return Dialog_button_base<ui::Button>(L); |
534 | } |
535 | |
536 | int Dialog_check(lua_State* L) |
537 | { |
538 | return Dialog_button_base<ui::CheckBox>(L); |
539 | } |
540 | |
541 | int 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 | |
566 | int 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 | |
595 | int 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 | |
627 | int 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 | |
680 | int 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 | |
720 | int 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 | |
748 | int 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 | |
803 | int 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 | |
875 | int 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 | |
1062 | int 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 | |
1152 | int 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 | |
1240 | int 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 | |
1250 | int 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 | |
1261 | const 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 | |
1282 | const 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 | |
1290 | DEF_MTNAME(Dialog); |
1291 | |
1292 | void 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 |
1300 | void 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 | |