1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-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/app_menus.h"
14#include "app/commands/cmd_open_file.h"
15#include "app/commands/command.h"
16#include "app/commands/commands.h"
17#include "app/commands/params.h"
18#include "app/console.h"
19#include "app/crash/data_recovery.h"
20#include "app/doc.h"
21#include "app/ini_file.h"
22#include "app/modules/editors.h"
23#include "app/modules/gfx.h"
24#include "app/modules/gui.h"
25#include "app/modules/palettes.h"
26#include "app/pref/preferences.h"
27#include "app/tools/ink.h"
28#include "app/tools/tool_box.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/keyboard_shortcuts.h"
31#include "app/ui/main_menu_bar.h"
32#include "app/ui/main_menu_bar.h"
33#include "app/ui/main_window.h"
34#include "app/ui/skin/skin_property.h"
35#include "app/ui/skin/skin_theme.h"
36#include "app/ui/status_bar.h"
37#include "app/ui/toolbar.h"
38#include "app/ui_context.h"
39#include "app/util/open_batch.h"
40#include "base/fs.h"
41#include "base/memory.h"
42#include "base/string.h"
43#include "doc/sprite.h"
44#include "os/error.h"
45#include "os/screen.h"
46#include "os/surface.h"
47#include "os/system.h"
48#include "os/window.h"
49#include "ui/intern.h"
50#include "ui/ui.h"
51
52#ifdef ENABLE_STEAM
53 #include "steam/steam.h"
54#endif
55
56#include <algorithm>
57#include <cstdio>
58#include <list>
59#include <vector>
60
61#if defined(_DEBUG) && defined(ENABLE_DATA_RECOVERY)
62#include "app/crash/data_recovery.h"
63#include "app/modules/editors.h"
64#endif
65
66namespace app {
67
68using namespace gfx;
69using namespace ui;
70using namespace app::skin;
71
72static struct {
73 int width;
74 int height;
75 int scale;
76} try_resolutions[] = { { 1024, 768, 2 },
77 { 800, 600, 2 },
78 { 640, 480, 2 },
79 { 320, 240, 1 },
80 { 320, 200, 1 },
81 { 0, 0, 0 } };
82
83//////////////////////////////////////////////////////////////////////
84
85class CustomizedGuiManager : public ui::Manager
86 , public ui::LayoutIO {
87public:
88 CustomizedGuiManager(const os::WindowRef& nativeWindow)
89 : ui::Manager(nativeWindow) {
90 }
91
92protected:
93 bool onProcessMessage(Message* msg) override;
94#if ENABLE_DEVMODE
95 bool onProcessDevModeKeyDown(KeyMessage* msg);
96#endif
97 void onInitTheme(InitThemeEvent& ev) override;
98 LayoutIO* onGetLayoutIO() override { return this; }
99 void onNewDisplayConfiguration(Display* display) override;
100
101 // LayoutIO implementation
102 std::string loadLayout(Widget* widget) override;
103 void saveLayout(Widget* widget, const std::string& str) override;
104};
105
106static os::WindowRef main_window = nullptr;
107static CustomizedGuiManager* manager = nullptr;
108static Theme* gui_theme = nullptr;
109
110static ui::Timer* defered_invalid_timer = nullptr;
111static gfx::Region defered_invalid_region;
112
113// Load & save graphics configuration
114static bool load_gui_config(os::WindowSpec& spec, bool& maximized);
115static void save_gui_config();
116
117static bool create_main_window(bool gpuAccel,
118 bool& maximized,
119 std::string& lastError)
120{
121 os::WindowSpec spec;
122 if (!load_gui_config(spec, maximized))
123 return false;
124
125 // Scale is equal to 0 when it's the first time the program is
126 // executed.
127 int scale = Preferences::instance().general.screenScale();
128
129 os::instance()->setGpuAcceleration(gpuAccel);
130
131 try {
132 if (!spec.frame().isEmpty() ||
133 !spec.contentRect().isEmpty()) {
134 spec.scale(scale == 0 ? 2: std::clamp(scale, 1, 4));
135 main_window = os::instance()->makeWindow(spec);
136 }
137 }
138 catch (const os::WindowCreationException& e) {
139 lastError = e.what();
140 }
141
142 if (!main_window) {
143 for (int c=0; try_resolutions[c].width; ++c) {
144 try {
145 spec.frame();
146 spec.position(os::WindowSpec::Position::Default);
147 spec.scale(scale == 0 ? try_resolutions[c].scale: scale);
148 spec.contentRect(gfx::Rect(0, 0,
149 try_resolutions[c].width * spec.scale(),
150 try_resolutions[c].height * spec.scale()));
151 main_window = os::instance()->makeWindow(spec);
152 break;
153 }
154 catch (const os::WindowCreationException& e) {
155 lastError = e.what();
156 }
157 }
158 }
159
160 if (main_window) {
161 // Change the scale value only in the first run (this will be
162 // saved when the program is closed).
163 if (scale == 0)
164 Preferences::instance().general.screenScale(main_window->scale());
165
166 if (main_window->isMinimized())
167 main_window->maximize();
168 }
169
170 return (main_window != nullptr);
171}
172
173// Initializes GUI.
174int init_module_gui()
175{
176 auto& pref = Preferences::instance();
177 bool maximized = false;
178 std::string lastError = "Unknown error";
179 bool gpuAccel = pref.general.gpuAcceleration();
180
181 if (!create_main_window(gpuAccel, maximized, lastError)) {
182 // If we've created the native window with hardware acceleration,
183 // now we try to do it without hardware acceleration.
184 if (gpuAccel &&
185 os::instance()->hasCapability(os::Capabilities::GpuAccelerationSwitch)) {
186 if (create_main_window(false, maximized, lastError)) {
187 // Disable hardware acceleration
188 pref.general.gpuAcceleration(false);
189 }
190 }
191 }
192
193 if (!main_window) {
194 os::error_message(
195 ("Unable to create a user-interface window.\nDetails: "+lastError+"\n").c_str());
196 return -1;
197 }
198
199 // Create the default-manager
200 manager = new CustomizedGuiManager(main_window);
201
202 // Setup the GUI theme for all widgets
203 gui_theme = new SkinTheme;
204 ui::set_theme(gui_theme, pref.general.uiScale());
205
206 if (maximized)
207 main_window->maximize();
208
209 // Handle live resize too redraw the entire manager, dispatch the UI
210 // messages, and flip the window.
211 os::instance()->handleWindowResize =
212 [](os::Window* window) {
213 Display* display = Manager::getDisplayFromNativeWindow(window);
214 if (!display)
215 display = manager->display();
216 ASSERT(display);
217
218 Message* msg = new Message(kResizeDisplayMessage);
219 msg->setDisplay(display);
220 msg->setRecipient(manager);
221 msg->setPropagateToChildren(false);
222
223 manager->enqueueMessage(msg);
224 manager->dispatchMessages();
225 };
226
227 // Set graphics options for next time
228 save_gui_config();
229
230 update_windows_color_profile_from_preferences();
231
232 return 0;
233}
234
235void exit_module_gui()
236{
237 save_gui_config();
238
239 delete defered_invalid_timer;
240 delete manager;
241
242 // Now we can destroy theme
243 ui::set_theme(nullptr, ui::guiscale());
244 delete gui_theme;
245
246 // This should be the last unref() of the display to delete it.
247 main_window.reset();
248}
249
250void update_windows_color_profile_from_preferences()
251{
252 auto system = os::instance();
253
254 gen::WindowColorProfile windowProfile;
255 if (Preferences::instance().color.manage())
256 windowProfile = Preferences::instance().color.windowProfile();
257 else
258 windowProfile = gen::WindowColorProfile::SRGB;
259
260 os::ColorSpaceRef osCS = nullptr;
261
262 switch (windowProfile) {
263 case gen::WindowColorProfile::MONITOR:
264 osCS = nullptr;
265 break;
266 case gen::WindowColorProfile::SRGB:
267 osCS = system->makeColorSpace(gfx::ColorSpace::MakeSRGB());
268 break;
269 case gen::WindowColorProfile::SPECIFIC: {
270 std::string name =
271 Preferences::instance().color.windowProfileName();
272
273 std::vector<os::ColorSpaceRef> colorSpaces;
274 system->listColorSpaces(colorSpaces);
275
276 for (auto& cs : colorSpaces) {
277 auto gfxCs = cs->gfxColorSpace();
278 if (gfxCs->type() == gfx::ColorSpace::ICC &&
279 gfxCs->name() == name) {
280 osCS = cs;
281 break;
282 }
283 }
284 break;
285 }
286 }
287
288 // Set the default color space for all windows (osCS can be nullptr
289 // which means that each window should use its monitor color space)
290 system->setWindowsColorSpace(osCS);
291
292 // Set the color space of all windows
293 for (ui::Widget* widget : manager->children()) {
294 ASSERT(widget->type() == ui::kWindowWidget);
295 auto window = static_cast<ui::Window*>(widget);
296 if (window->ownDisplay()) {
297 if (auto display = window->display())
298 display->nativeWindow()->setColorSpace(osCS);
299 }
300 }
301}
302
303static bool load_gui_config(os::WindowSpec& spec, bool& maximized)
304{
305 os::ScreenRef screen = os::instance()->mainScreen();
306#ifdef LAF_SKIA
307 ASSERT(screen);
308#else
309 // Compiled without Skia (none backend), without screen.
310 if (!screen) {
311 std::printf(
312 "\n"
313 " Aseprite cannot initialize GUI because it was compiled with LAF_BACKEND=none\n"
314 "\n"
315 " Check the documentation in:\n"
316 " https://github.com/aseprite/laf/blob/main/README.md\n"
317 " https://github.com/aseprite/aseprite/blob/main/INSTALL.md\n"
318 "\n");
319 return false;
320 }
321#endif
322
323 spec.screen(screen);
324
325 gfx::Rect frame;
326 frame = get_config_rect("GfxMode", "Frame", frame);
327 if (!frame.isEmpty()) {
328 spec.position(os::WindowSpec::Position::Frame);
329
330 // Limit the content rect position into the available workarea,
331 // e.g. this is needed in case that the user closed Aseprite in a
332 // 2nd monitor that then unplugged and start Aseprite again.
333 bool ok = false;
334 os::ScreenList screens;
335 os::instance()->listScreens(screens);
336 for (const auto& screen : screens) {
337 gfx::Rect wa = screen->workarea();
338 gfx::Rect intersection = (frame & wa);
339 if (intersection.w >= 32 &&
340 intersection.h >= 32) {
341 ok = true;
342 break;
343 }
344 }
345
346 // Reset content rect
347 if (!ok) {
348 spec.position(os::WindowSpec::Position::Default);
349 frame = gfx::Rect();
350 }
351 }
352
353 if (frame.isEmpty()) {
354 frame = screen->workarea().shrink(64);
355
356 // Try to get Width/Height from previous Aseprite versions
357 frame.w = get_config_int("GfxMode", "Width", frame.w);
358 frame.h = get_config_int("GfxMode", "Height", frame.h);
359 }
360 spec.frame(frame);
361
362 maximized = get_config_bool("GfxMode", "Maximized", true);
363
364 ui::set_multiple_displays(Preferences::instance().experimental.multipleWindows());
365 return true;
366}
367
368static void save_gui_config()
369{
370 os::Window* window = manager->display()->nativeWindow();
371 if (window) {
372 const bool maximized = (window->isMaximized() ||
373 window->isFullscreen());
374 const gfx::Rect frame = (maximized ? window->restoredFrame():
375 window->frame());
376
377 set_config_bool("GfxMode", "Maximized", maximized);
378 set_config_rect("GfxMode", "Frame", frame);
379 }
380}
381
382void update_screen_for_document(const Doc* document)
383{
384 // Without document.
385 if (!document) {
386 // Well, change to the default palette.
387 if (set_current_palette(NULL, false)) {
388 // If the palette changes, refresh the whole screen.
389 if (manager)
390 manager->invalidate();
391 }
392 }
393 // With a document.
394 else {
395 const_cast<Doc*>(document)->notifyGeneralUpdate();
396
397 // Update the tabs (maybe the modified status has been changed).
398 app_rebuild_documents_tabs();
399 }
400}
401
402void load_window_pos(Window* window, const char* section,
403 const bool limitMinSize)
404{
405 Display* parentDisplay =
406 (window->display() ? window->display():
407 window->manager()->display());
408 Rect workarea =
409 (get_multiple_displays() ?
410 parentDisplay->nativeWindow()->screen()->workarea():
411 parentDisplay->bounds());
412
413 // Default position
414 Rect origPos = window->bounds();
415
416 // Load configurated position
417 Rect pos = get_config_rect(section, "WindowPos", origPos);
418
419 if (limitMinSize) {
420 pos.w = std::clamp(pos.w, origPos.w, workarea.w);
421 pos.h = std::clamp(pos.h, origPos.h, workarea.h);
422 }
423 else {
424 pos.w = std::min(pos.w, workarea.w);
425 pos.h = std::min(pos.h, workarea.h);
426 }
427
428 pos.setOrigin(Point(std::clamp(pos.x, workarea.x, workarea.x2()-pos.w),
429 std::clamp(pos.y, workarea.y, workarea.y2()-pos.h)));
430
431 window->setBounds(pos);
432
433 if (get_multiple_displays()) {
434 Rect frame = get_config_rect(section, "WindowFrame", gfx::Rect());
435 if (!frame.isEmpty()) {
436 limit_with_workarea(parentDisplay, frame);
437 window->loadNativeFrame(frame);
438 }
439 }
440 else {
441 del_config_value(section, "WindowFrame");
442 }
443}
444
445void save_window_pos(Window* window, const char* section)
446{
447 gfx::Rect rc;
448
449 if (!window->lastNativeFrame().isEmpty()) {
450 const os::Window* mainNativeWindow = manager->display()->nativeWindow();
451 rc = window->lastNativeFrame();
452 set_config_rect(section, "WindowFrame", rc);
453 rc.offset(-mainNativeWindow->frame().origin());
454 rc /= mainNativeWindow->scale();
455 }
456 else {
457 del_config_value(section, "WindowFrame");
458 rc = window->bounds();
459 }
460
461 set_config_rect(section, "WindowPos", rc);
462}
463
464// TODO Replace this with new theme styles
465Widget* setup_mini_font(Widget* widget)
466{
467 auto skinProp = get_skin_property(widget);
468 skinProp->setMiniFont();
469 return widget;
470}
471
472// TODO Replace this with new theme styles
473Widget* setup_mini_look(Widget* widget)
474{
475 auto skinProp = get_skin_property(widget);
476 skinProp->setLook(MiniLook);
477 return widget;
478}
479
480//////////////////////////////////////////////////////////////////////
481// Button style (convert radio or check buttons and draw it like
482// normal buttons)
483
484void defer_invalid_rect(const gfx::Rect& rc)
485{
486 if (!defered_invalid_timer)
487 defered_invalid_timer = new ui::Timer(250, manager);
488
489 defered_invalid_timer->stop();
490 defered_invalid_timer->start();
491 defered_invalid_region.createUnion(defered_invalid_region, gfx::Region(rc));
492}
493
494//////////////////////////////////////////////////////////////////////
495// Manager event handler.
496
497bool CustomizedGuiManager::onProcessMessage(Message* msg)
498{
499#ifdef ENABLE_STEAM
500 if (auto steamAPI = steam::SteamAPI::instance())
501 steamAPI->runCallbacks();
502#endif
503
504 switch (msg->type()) {
505
506 case kCloseDisplayMessage:
507 // Only call the exit command/close the app when the the main
508 // display is the closed window in this kCloseDisplayMessage
509 // message and it's the current running foreground window.
510 if (msg->display() == this->display() &&
511 getForegroundWindow() == App::instance()->mainWindow()) {
512 // Execute the "Exit" command.
513 Command* command = Commands::instance()->byId(CommandId::Exit());
514 UIContext::instance()->executeCommandFromMenuOrShortcut(command);
515 return true;
516 }
517 break;
518
519 case kDropFilesMessage:
520 // Files are processed only when the main window is the current
521 // window running.
522 //
523 // TODO could we send the files to each dialog?
524 if (getForegroundWindow() == App::instance()->mainWindow()) {
525 base::paths files = static_cast<DropFilesMessage*>(msg)->files();
526 UIContext* ctx = UIContext::instance();
527 OpenBatchOfFiles batch;
528
529 while (!files.empty()) {
530 auto fn = files.front();
531 files.erase(files.begin());
532
533 // If the document is already open, select it.
534 Doc* doc = ctx->documents().getByFileName(fn);
535 if (doc) {
536 DocView* docView = ctx->getFirstDocView(doc);
537 if (docView)
538 ctx->setActiveView(docView);
539 else {
540 ASSERT(false); // Must be some DocView available
541 }
542 }
543 // Load the file
544 else {
545 // Depending on the file type we will want to do different things:
546 std::string extension = base::string_to_lower(
547 base::get_file_extension(fn));
548
549 // Install the extension
550 if (extension == "aseprite-extension") {
551 Command* cmd = Commands::instance()->byId(CommandId::Options());
552 Params params;
553 params.set("installExtension", fn.c_str());
554 ctx->executeCommandFromMenuOrShortcut(cmd, params);
555 }
556 // Other extensions will be handled as an image/sprite
557 else {
558 batch.open(ctx, fn,
559 false); // Open all frames
560
561 // Remove all used file names from the "dropped files"
562 for (const auto& usedFn : batch.usedFiles()) {
563 auto it = std::find(files.begin(), files.end(), usedFn);
564 if (it != files.end())
565 files.erase(it);
566 }
567 }
568 }
569 }
570 }
571 break;
572
573 case kKeyDownMessage: {
574#if ENABLE_DEVMODE
575 if (onProcessDevModeKeyDown(static_cast<KeyMessage*>(msg)))
576 return true;
577#endif // ENABLE_DEVMODE
578
579 // Call base impl to check if there is a foreground window as
580 // top level that needs keys. (In this way we just do not
581 // process keyboard shortcuts for menus and tools).
582 if (Manager::onProcessMessage(msg))
583 return true;
584
585 KeyboardShortcuts* keys = KeyboardShortcuts::instance();
586 for (const KeyPtr& key : *keys) {
587 if (key->isPressed(msg, *keys)) {
588 // Cancel menu-bar loops (to close any popup menu)
589 App::instance()->mainWindow()->getMenuBar()->cancelMenuLoop();
590
591 switch (key->type()) {
592
593 case KeyType::Tool: {
594 tools::Tool* current_tool = App::instance()->activeTool();
595 tools::Tool* select_this_tool = key->tool();
596 tools::ToolBox* toolbox = App::instance()->toolBox();
597 std::vector<tools::Tool*> possibles;
598
599 // Collect all tools with the pressed keyboard-shortcut
600 for (tools::Tool* tool : *toolbox) {
601 const KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
602 if (key && key->isPressed(msg, *keys))
603 possibles.push_back(tool);
604 }
605
606 if (possibles.size() >= 2) {
607 bool done = false;
608
609 for (size_t i=0; i<possibles.size(); ++i) {
610 if (possibles[i] != current_tool &&
611 ToolBar::instance()->isToolVisible(possibles[i])) {
612 select_this_tool = possibles[i];
613 done = true;
614 break;
615 }
616 }
617
618 if (!done) {
619 for (size_t i=0; i<possibles.size(); ++i) {
620 // If one of the possibilities is the current tool
621 if (possibles[i] == current_tool) {
622 // We select the next tool in the possibilities
623 select_this_tool = possibles[(i+1) % possibles.size()];
624 break;
625 }
626 }
627 }
628 }
629
630 ToolBar::instance()->selectTool(select_this_tool);
631 return true;
632 }
633
634 case KeyType::Command: {
635 Command* command = key->command();
636
637 // Commands are executed only when the main window is
638 // the current window running.
639 if (getForegroundWindow() == App::instance()->mainWindow()) {
640 // OK, so we can execute the command represented
641 // by the pressed-key in the message...
642 UIContext::instance()->executeCommandFromMenuOrShortcut(
643 command, key->params());
644 return true;
645 }
646 break;
647 }
648
649 case KeyType::Quicktool: {
650 // Do nothing, it is used in the editor through the
651 // KeyboardShortcuts::getCurrentQuicktool() function.
652 break;
653 }
654
655 }
656 break;
657 }
658 }
659 break;
660 }
661
662 case kTimerMessage:
663 if (static_cast<TimerMessage*>(msg)->timer() == defered_invalid_timer) {
664 invalidateRegion(defered_invalid_region);
665 defered_invalid_region.clear();
666 defered_invalid_timer->stop();
667 }
668 break;
669
670 }
671
672 return Manager::onProcessMessage(msg);
673}
674
675#if ENABLE_DEVMODE
676bool CustomizedGuiManager::onProcessDevModeKeyDown(KeyMessage* msg)
677{
678 // Ctrl+Shift+Q generates a crash (useful to test the anticrash feature)
679 if (msg->ctrlPressed() &&
680 msg->shiftPressed() &&
681 msg->scancode() == kKeyQ) {
682 int* p = nullptr;
683 *p = 0; // *Crash*
684 return true; // This line should not be executed anyway
685 }
686
687 // Ctrl+F1 switches screen/UI scaling
688 if (msg->ctrlPressed() &&
689 msg->scancode() == kKeyF1) {
690 try {
691 os::Window* window = display()->nativeWindow();
692 int screenScale = window->scale();
693 int uiScale = ui::guiscale();
694
695 if (msg->shiftPressed()) {
696 if (screenScale == 2 && uiScale == 1) {
697 screenScale = 1;
698 uiScale = 1;
699 }
700 else if (screenScale == 1 && uiScale == 1) {
701 screenScale = 1;
702 uiScale = 2;
703 }
704 else if (screenScale == 1 && uiScale == 2) {
705 screenScale = 2;
706 uiScale = 1;
707 }
708 }
709 else {
710 if (screenScale == 2 && uiScale == 1) {
711 screenScale = 1;
712 uiScale = 2;
713 }
714 else if (screenScale == 1 && uiScale == 2) {
715 screenScale = 1;
716 uiScale = 1;
717 }
718 else if (screenScale == 1 && uiScale == 1) {
719 screenScale = 2;
720 uiScale = 1;
721 }
722 }
723
724 if (uiScale != ui::guiscale()) {
725 ui::set_theme(ui::get_theme(), uiScale);
726 }
727 if (screenScale != window->scale()) {
728 updateAllDisplaysWithNewScale(screenScale);
729 }
730 }
731 catch (const std::exception& ex) {
732 Console::showException(ex);
733 }
734 return true;
735 }
736
737#ifdef ENABLE_DATA_RECOVERY
738 // Ctrl+Shift+R recover active sprite from the backup store
739 if (msg->ctrlPressed() &&
740 msg->shiftPressed() &&
741 msg->scancode() == kKeyR &&
742 App::instance()->dataRecovery() &&
743 App::instance()->dataRecovery()->activeSession() &&
744 current_editor &&
745 current_editor->document()) {
746 Doc* doc = App::instance()
747 ->dataRecovery()
748 ->activeSession()
749 ->restoreBackupById(current_editor->document()->id(), nullptr);
750 if (doc)
751 UIContext::instance()->documents().add(doc);
752 return true;
753 }
754#endif // ENABLE_DATA_RECOVERY
755
756 return false;
757}
758#endif // ENABLE_DEVMODE
759
760void CustomizedGuiManager::onInitTheme(InitThemeEvent& ev)
761{
762 Manager::onInitTheme(ev);
763
764 // Update the theme on all menus
765 AppMenus::instance()->initTheme();
766}
767
768void CustomizedGuiManager::onNewDisplayConfiguration(Display* display)
769{
770 Manager::onNewDisplayConfiguration(display);
771
772 // Only whne the main display/window is modified
773 if (display == this->display()) {
774 save_gui_config();
775
776 // TODO Should we provide a more generic way for all ui::Window to
777 // detect the os::Window (or UI Screen Scaling) change?
778 Console::notifyNewDisplayConfiguration();
779 }
780}
781
782std::string CustomizedGuiManager::loadLayout(Widget* widget)
783{
784 if (widget->window() == nullptr)
785 return "";
786
787 std::string windowId = widget->window()->id();
788 std::string widgetId = widget->id();
789
790 return get_config_string(("layout:"+windowId).c_str(), widgetId.c_str(), "");
791}
792
793void CustomizedGuiManager::saveLayout(Widget* widget, const std::string& str)
794{
795 if (widget->window() == NULL)
796 return;
797
798 std::string windowId = widget->window()->id();
799 std::string widgetId = widget->id();
800
801 set_config_string(("layout:"+windowId).c_str(),
802 widgetId.c_str(),
803 str.c_str());
804}
805
806} // namespace app
807