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/cmd/set_grid_bounds.h"
14#include "app/commands/command.h"
15#include "app/console.h"
16#include "app/context.h"
17#include "app/context_access.h"
18#include "app/extensions.h"
19#include "app/file/file.h"
20#include "app/file_selector.h"
21#include "app/i18n/strings.h"
22#include "app/ini_file.h"
23#include "app/launcher.h"
24#include "app/modules/gui.h"
25#include "app/pref/preferences.h"
26#include "app/recent_files.h"
27#include "app/resource_finder.h"
28#include "app/tx.h"
29#include "app/ui/color_button.h"
30#include "app/ui/main_window.h"
31#include "app/ui/pref_widget.h"
32#include "app/ui/rgbmap_algorithm_selector.h"
33#include "app/ui/sampling_selector.h"
34#include "app/ui/separator_in_view.h"
35#include "app/ui/skin/skin_theme.h"
36#include "base/convert_to.h"
37#include "base/fs.h"
38#include "base/string.h"
39#include "base/version.h"
40#include "doc/image.h"
41#include "fmt/format.h"
42#include "os/system.h"
43#include "os/window.h"
44#include "render/render.h"
45#include "ui/ui.h"
46
47#if ENABLE_SENTRY
48#include "app/sentry_wrapper.h"
49#endif
50
51#include "options.xml.h"
52
53namespace app {
54
55namespace {
56
57const char* kSectionGeneralId = "section_general";
58const char* kSectionTabletId = "section_tablet";
59const char* kSectionBgId = "section_bg";
60const char* kSectionGridId = "section_grid";
61const char* kSectionThemeId = "section_theme";
62const char* kSectionExtensionsId = "section_extensions";
63
64const char* kInfiniteSymbol = "\xE2\x88\x9E"; // Infinite symbol (UTF-8)
65
66app::gen::ColorProfileBehavior filesWithCsMap[] = {
67 app::gen::ColorProfileBehavior::DISABLE,
68 app::gen::ColorProfileBehavior::EMBEDDED,
69 app::gen::ColorProfileBehavior::CONVERT,
70 app::gen::ColorProfileBehavior::ASSIGN,
71 app::gen::ColorProfileBehavior::ASK,
72};
73
74app::gen::ColorProfileBehavior missingCsMap[] = {
75 app::gen::ColorProfileBehavior::DISABLE,
76 app::gen::ColorProfileBehavior::ASSIGN,
77 app::gen::ColorProfileBehavior::ASK,
78};
79
80class ExtensionCategorySeparator : public SeparatorInView {
81public:
82 ExtensionCategorySeparator(const Extension::Category category,
83 const std::string& text)
84 : SeparatorInView(text, ui::HORIZONTAL)
85 , m_category(category) {
86 InitTheme.connect(
87 [this]{
88 auto b = this->border();
89 b.top(2*b.top());
90 b.bottom(2*b.bottom());
91 this->setBorder(b);
92 });
93 }
94 Extension::Category category() const { return m_category; }
95private:
96 Extension::Category m_category;
97};
98
99} // anonymous namespace
100
101using namespace ui;
102
103class OptionsWindow : public app::gen::Options {
104
105 class ColorSpaceItem : public ListItem {
106 public:
107 ColorSpaceItem(const os::ColorSpaceRef& cs)
108 : ListItem(cs->gfxColorSpace()->name()),
109 m_cs(cs) {
110 }
111 os::ColorSpaceRef cs() const { return m_cs; }
112 private:
113 os::ColorSpaceRef m_cs;
114 };
115
116 class ThemeItem : public ListItem {
117 public:
118 ThemeItem(const std::string& id,
119 const std::string& path,
120 const std::string& displayName = std::string(),
121 const std::string& variant = std::string())
122 : ListItem(createLabel(path, id, displayName, variant)),
123 m_path(path),
124 m_name(id) {
125 }
126
127 const std::string& themePath() const { return m_path; }
128 const std::string& themeName() const { return m_name; }
129
130 void openFolder() const {
131 app::launcher::open_folder(m_path);
132 }
133
134 bool canSelect() const {
135 return !m_name.empty();
136 }
137
138 private:
139 static std::string createLabel(const std::string& path,
140 const std::string& id,
141 const std::string& displayName,
142 const std::string& variant) {
143 if (displayName.empty()) {
144 if (id.empty())
145 return fmt::format("-- {} --", path);
146 else
147 return id;
148 }
149 else if (id == displayName) {
150 if (variant.empty())
151 return id;
152 else
153 return fmt::format("{} - {}", id, variant);
154 }
155 else {
156 if (variant.empty())
157 return displayName;
158 else
159 return fmt::format("{} - {}", displayName, variant);
160 }
161 }
162
163 std::string m_path;
164 std::string m_name;
165 };
166
167 class ExtensionItem : public ListItem {
168 public:
169 ExtensionItem(Extension* extension)
170 : ListItem(extension->displayName())
171 , m_extension(extension) {
172 setEnabled(extension->isEnabled());
173 }
174
175 Extension* extension() { return m_extension; }
176
177 Extension::Category category() const {
178 ASSERT(m_extension);
179 return m_extension->category();
180 }
181
182 bool isEnabled() const {
183 ASSERT(m_extension);
184 return m_extension->isEnabled();
185 }
186
187 bool isInstalled() const {
188 ASSERT(m_extension);
189 return m_extension->isInstalled();
190 }
191
192 bool canBeDisabled() const {
193 ASSERT(m_extension);
194 return m_extension->canBeDisabled();
195 }
196
197 bool canBeUninstalled() const {
198 ASSERT(m_extension);
199 return m_extension->canBeUninstalled();
200 }
201
202 void enable(bool state) {
203 ASSERT(m_extension);
204 App::instance()->extensions().enableExtension(m_extension, state);
205 setEnabled(m_extension->isEnabled());
206 }
207
208 void uninstall() {
209 ASSERT(m_extension);
210 ASSERT(canBeUninstalled());
211 App::instance()->extensions().uninstallExtension(m_extension,
212 DeletePluginPref::kYes);
213 m_extension = nullptr;
214 }
215
216 void openFolder() const {
217 ASSERT(m_extension);
218 app::launcher::open_folder(m_extension->path());
219 }
220
221 private:
222 Extension* m_extension;
223 };
224
225 class ThemeVariantItem : public ButtonSet::Item {
226 public:
227 ThemeVariantItem(OptionsWindow* options,
228 const std::string& id,
229 const std::string& variant)
230 : m_options(options)
231 , m_themeId(id) {
232 setText(variant);
233 }
234 private:
235 void onClick() override {
236 m_options->setUITheme(m_themeId,
237 false, // Don't adjust scale
238 false); // Don't recreate variants
239 }
240 OptionsWindow* m_options;
241 std::string m_themeId;
242 };
243
244public:
245 OptionsWindow(Context* context, int& curSection)
246 : m_context(context)
247 , m_pref(Preferences::instance())
248 , m_globPref(m_pref.document(nullptr))
249 , m_docPref(m_pref.document(context->activeDocument()))
250 , m_curPref(&m_docPref)
251 , m_curSection(curSection)
252 , m_restoreThisTheme(m_pref.theme.selected())
253 , m_restoreScreenScaling(m_pref.general.screenScale())
254 , m_restoreUIScaling(m_pref.general.uiScale())
255 {
256 sectionListbox()->Change.connect([this]{ onChangeSection(); });
257
258 // Theme variants
259 fillThemeVariants();
260
261 // Default extension to save files
262 fillExtensionsCombobox(defaultExtension(), m_pref.saveFile.defaultExtension());
263 fillExtensionsCombobox(exportImageDefaultExtension(), m_pref.exportFile.imageDefaultExtension());
264 fillExtensionsCombobox(exportAnimationDefaultExtension(), m_pref.exportFile.animationDefaultExtension());
265 fillExtensionsCombobox(exportSpriteSheetDefaultExtension(), m_pref.spriteSheet.defaultExtension());
266
267 // Number of recent items
268 recentFiles()->setValue(m_pref.general.recentItems());
269 clearRecentFiles()->Click.connect([this]{ onClearRecentFiles(); });
270
271 // Template item for active display color profiles
272 m_templateTextForDisplayCS = windowCs()->getItem(2)->text();
273 windowCs()->deleteItem(2);
274
275 // Color profiles
276 resetColorManagement()->Click.connect([this]{ onResetColorManagement(); });
277 colorManagement()->Click.connect([this]{ onColorManagement(); });
278 {
279 os::instance()->listColorSpaces(m_colorSpaces);
280 for (auto& cs : m_colorSpaces) {
281 if (cs->gfxColorSpace()->type() != gfx::ColorSpace::None)
282 workingRgbCs()->addItem(new ColorSpaceItem(cs));
283 }
284 updateColorProfileControls(m_pref.color.manage(),
285 m_pref.color.windowProfile(),
286 m_pref.color.windowProfileName(),
287 m_pref.color.workingRgbSpace(),
288 m_pref.color.filesWithProfile(),
289 m_pref.color.missingProfile());
290 }
291
292 // Alerts
293 openSequence()->setSelectedItemIndex(int(m_pref.openFile.openSequence()));
294 resetAlerts()->Click.connect([this]{ onResetAlerts(); });
295
296 // Cursor
297 paintingCursorType()->setSelectedItemIndex(int(m_pref.cursor.paintingCursorType()));
298 cursorColor()->setColor(m_pref.cursor.cursorColor());
299
300 if (cursorColor()->getColor().getType() == app::Color::MaskType) {
301 cursorColorType()->setSelectedItemIndex(0);
302 cursorColor()->setVisible(false);
303 }
304 else {
305 cursorColorType()->setSelectedItemIndex(1);
306 cursorColor()->setVisible(true);
307 }
308 cursorColorType()->Change.connect([this]{ onCursorColorType(); });
309
310 // Brush preview
311 brushPreview()->setSelectedItemIndex(
312 (int)m_pref.cursor.brushPreview());
313
314 // Guide colors
315 layerEdgesColor()->setColor(m_pref.guides.layerEdgesColor());
316 autoGuidesColor()->setColor(m_pref.guides.autoGuidesColor());
317
318 // Slices default color
319 defaultSliceColor()->setColor(m_pref.slices.defaultColor());
320
321 // Others
322 firstFrame()->setTextf("%d", m_globPref.timeline.firstFrame());
323
324 if (m_pref.general.expandMenubarOnMouseover())
325 expandMenubarOnMouseover()->setSelected(true);
326
327 if (m_pref.general.dataRecovery())
328 enableDataRecovery()->setSelected(true);
329 enableDataRecovery()->Click.connect(
330 [this](Event&){
331 const bool state = enableDataRecovery()->isSelected();
332 keepEditedSpriteData()->setEnabled(state);
333 keepEditedSpriteData()->setSelected(state);
334 keepEditedSpriteDataFor()->setEnabled(state);
335 });
336
337 if (m_pref.general.dataRecovery() &&
338 m_pref.general.keepEditedSpriteData())
339 keepEditedSpriteData()->setSelected(true);
340 else if (!m_pref.general.dataRecovery()) {
341 keepEditedSpriteData()->setEnabled(false);
342 keepEditedSpriteDataFor()->setEnabled(false);
343 }
344
345 if (m_pref.general.keepClosedSpriteOnMemory())
346 keepClosedSpriteOnMemory()->setSelected(true);
347
348 if (m_pref.general.showFullPath())
349 showFullPath()->setSelected(true);
350
351 dataRecoveryPeriod()->setSelectedItemIndex(
352 dataRecoveryPeriod()->findItemIndexByValue(
353 base::convert_to<std::string>(m_pref.general.dataRecoveryPeriod())));
354
355 keepEditedSpriteDataFor()->setSelectedItemIndex(
356 keepEditedSpriteDataFor()->findItemIndexByValue(
357 base::convert_to<std::string>(m_pref.general.keepEditedSpriteDataFor())));
358
359 keepClosedSpriteOnMemoryFor()->setSelectedItemIndex(
360 keepClosedSpriteOnMemoryFor()->findItemIndexByValue(
361 base::convert_to<std::string>(m_pref.general.keepClosedSpriteOnMemoryFor())));
362
363 if (m_pref.editor.zoomFromCenterWithWheel())
364 zoomFromCenterWithWheel()->setSelected(true);
365
366 if (m_pref.editor.zoomFromCenterWithKeys())
367 zoomFromCenterWithKeys()->setSelected(true);
368
369 if (m_pref.selection.autoOpaque())
370 autoOpaque()->setSelected(true);
371
372 if (m_pref.selection.keepSelectionAfterClear())
373 keepSelectionAfterClear()->setSelected(true);
374
375 if (m_pref.selection.autoShowSelectionEdges())
376 autoShowSelectionEdges()->setSelected(true);
377
378 if (m_pref.selection.moveEdges())
379 moveEdges()->setSelected(true);
380
381 if (m_pref.selection.modifiersDisableHandles())
382 modifiersDisableHandles()->setSelected(true);
383
384 if (m_pref.selection.moveOnAddMode())
385 moveOnAddMode()->setSelected(true);
386
387 // If the platform supports native cursors...
388 if ((int(os::instance()->capabilities()) &
389 int(os::Capabilities::CustomMouseCursor)) != 0) {
390 if (m_pref.cursor.useNativeCursor())
391 nativeCursor()->setSelected(true);
392 nativeCursor()->Click.connect([this]{ onNativeCursorChange(); });
393
394 cursorScale()->setSelectedItemIndex(
395 cursorScale()->findItemIndexByValue(
396 base::convert_to<std::string>(m_pref.cursor.cursorScale())));
397 }
398 else {
399 nativeCursor()->setEnabled(false);
400 }
401
402 onNativeCursorChange();
403
404 if (m_pref.experimental.useNativeClipboard())
405 nativeClipboard()->setSelected(true);
406
407 if (m_pref.experimental.useNativeFileDialog())
408 nativeFileDialog()->setSelected(true);
409
410#ifdef _WIN32 // Show Tablet section on Windows
411 {
412 os::TabletAPI tabletAPI = os::instance()->tabletAPI();
413
414 if (tabletAPI == os::TabletAPI::Wintab) {
415 tabletApiWintabSystem()->setSelected(true);
416 loadWintabDriver()->setSelected(true);
417 loadWintabDriver2()->setSelected(true);
418 }
419 else if (tabletAPI == os::TabletAPI::WintabPackets) {
420 tabletApiWintabDirect()->setSelected(true);
421 loadWintabDriver()->setSelected(true);
422 loadWintabDriver2()->setSelected(true);
423 }
424 else {
425 tabletApiWindowsPointer()->setSelected(true);
426 loadWintabDriver()->setSelected(false);
427 loadWintabDriver2()->setSelected(false);
428 }
429
430 tabletApiWindowsPointer()->Click.connect([this](Event&){ onTabletAPIChange(); });
431 tabletApiWintabSystem()->Click.connect([this](Event&){ onTabletAPIChange(); });
432 tabletApiWintabDirect()->Click.connect([this](Event&){ onTabletAPIChange(); });
433 loadWintabDriver()->Click.connect(
434 [this](Event&){ onLoadWintabChange(loadWintabDriver()->isSelected()); });
435 loadWintabDriver2()->Click.connect(
436 [this](Event&){ onLoadWintabChange(loadWintabDriver2()->isSelected()); });
437 }
438#else // For macOS and Linux
439 {
440 // Hide the "section_tablet" item (which is only for Windows at the moment)
441 for (auto item : sectionListbox()->children()) {
442 if (static_cast<ListItem*>(item)->getValue() == kSectionTabletId) {
443 item->setVisible(false);
444 break;
445 }
446 }
447 sectionTablet()->setVisible(false);
448 loadWintabDriverBox()->setVisible(false);
449 loadWintabDriverBox()->setVisible(false);
450 }
451#endif
452
453 if (m_pref.experimental.flashLayer())
454 flashLayer()->setSelected(true);
455
456 nonactiveLayersOpacity()->setValue(m_pref.experimental.nonactiveLayersOpacity());
457
458 rgbmapAlgorithmPlaceholder()->addChild(&m_rgbmapAlgorithmSelector);
459 m_rgbmapAlgorithmSelector.setExpansive(true);
460 m_rgbmapAlgorithmSelector.algorithm(m_pref.quantization.rgbmapAlgorithm());
461
462 if (m_pref.editor.showScrollbars())
463 showScrollbars()->setSelected(true);
464
465 if (m_pref.editor.autoScroll())
466 autoScroll()->setSelected(true);
467
468 if (m_pref.editor.straightLinePreview())
469 straightLinePreview()->setSelected(true);
470
471 if (m_pref.eyedropper.discardBrush())
472 discardBrush()->setSelected(true);
473
474 // Scope
475 bgScope()->addItem(Strings::options_bg_for_new_docs());
476 gridScope()->addItem(Strings::options_grid_for_new_docs());
477 if (context->activeDocument()) {
478 bgScope()->addItem(Strings::options_bg_for_active_doc());
479 bgScope()->setSelectedItemIndex(1);
480 bgScope()->Change.connect([this]{ onChangeBgScope(); });
481
482 gridScope()->addItem(Strings::options_grid_for_active_doc());
483 gridScope()->setSelectedItemIndex(1);
484 gridScope()->Change.connect([this]{ onChangeGridScope(); });
485 }
486
487 selectScalingItems();
488
489#ifdef _DEBUG // TODO enable this on Release when Aseprite supports
490 // GPU-acceleration properly
491 if (os::instance()->hasCapability(os::Capabilities::GpuAccelerationSwitch)) {
492 gpuAcceleration()->setSelected(m_pref.general.gpuAcceleration());
493 }
494 else
495#endif
496 {
497 gpuAcceleration()->setVisible(false);
498 }
499
500 // If the platform does support native menus, we show the option,
501 // in other case, the option doesn't make sense for this platform.
502 if (os::instance()->menus())
503 showMenuBar()->setSelected(m_pref.general.showMenuBar());
504 else
505 showMenuBar()->setVisible(false);
506
507 showHome()->setSelected(m_pref.general.showHome());
508
509 // Editor sampling
510 samplingPlaceholder()->addChild(
511 m_samplingSelector = new SamplingSelector(
512 SamplingSelector::Behavior::ChangeOnSave));
513
514 m_samplingSelector->setEnabled(newRenderEngine()->isSelected());
515 newRenderEngine()->Click.connect(
516 [this]{
517 m_samplingSelector->setEnabled(newRenderEngine()->isSelected());
518 });
519
520 // Right-click
521 static_assert(int(app::gen::RightClickMode::PAINT_BGCOLOR) == 0, "");
522 static_assert(int(app::gen::RightClickMode::PICK_FGCOLOR) == 1, "");
523 static_assert(int(app::gen::RightClickMode::ERASE) == 2, "");
524 static_assert(int(app::gen::RightClickMode::SCROLL) == 3, "");
525 static_assert(int(app::gen::RightClickMode::RECTANGULAR_MARQUEE) == 4, "");
526 static_assert(int(app::gen::RightClickMode::LASSO) == 5, "");
527 static_assert(int(app::gen::RightClickMode::SELECT_LAYER_AND_MOVE) == 6, "");
528
529 rightClickBehavior()->addItem(Strings::options_right_click_paint_bgcolor());
530 rightClickBehavior()->addItem(Strings::options_right_click_pick_fgcolor());
531 rightClickBehavior()->addItem(Strings::options_right_click_erase());
532 rightClickBehavior()->addItem(Strings::options_right_click_scroll());
533 rightClickBehavior()->addItem(Strings::options_right_click_rectangular_marquee());
534 rightClickBehavior()->addItem(Strings::options_right_click_lasso());
535 rightClickBehavior()->addItem(Strings::options_right_click_select_layer_and_move());
536 rightClickBehavior()->setSelectedItemIndex((int)m_pref.editor.rightClickMode());
537
538#ifndef __APPLE__ // Zoom sliding two fingers option only on macOS
539 slideZoom()->setVisible(false);
540#endif
541
542 // Checkered background size
543 static_assert(int(app::gen::BgType::CHECKERED_16x16) == 0, "");
544 static_assert(int(app::gen::BgType::CHECKERED_1x1) == 4, "");
545 static_assert(int(app::gen::BgType::CHECKERED_CUSTOM) == 5, "");
546 checkeredBgSize()->addItem("16x16");
547 checkeredBgSize()->addItem("8x8");
548 checkeredBgSize()->addItem("4x4");
549 checkeredBgSize()->addItem("2x2");
550 checkeredBgSize()->addItem("1x1");
551 checkeredBgSize()->addItem(Strings::options_bg_custom_size());
552 checkeredBgSize()->Change.connect([this]{ onCheckeredBgSizeChange(); });
553
554 // Reset buttons
555 resetBg()->Click.connect([this]{ onResetBg(); });
556 resetGrid()->Click.connect([this]{ onResetGrid(); });
557
558 // Links
559 locateFile()->Click.connect([this]{ onLocateConfigFile(); });
560 if (!App::instance()->memoryDumpFilename().empty())
561 locateCrashFolder()->Click.connect([this]{ onLocateCrashFolder(); });
562 else
563 locateCrashFolder()->setVisible(false);
564
565 // Share crashdb
566#if ENABLE_SENTRY
567 shareCrashdb()->setSelected(Sentry::consentGiven());
568#else
569 shareCrashdb()->setVisible(false);
570#endif
571
572 // Undo preferences
573 limitUndo()->Click.connect([this]{ onLimitUndoCheck(); });
574 limitUndo()->setSelected(m_pref.undo.sizeLimit() != 0);
575 onLimitUndoCheck();
576
577 undoGotoModified()->setSelected(m_pref.undo.gotoModified());
578 undoAllowNonlinearHistory()->setSelected(m_pref.undo.allowNonlinearHistory());
579
580 // Theme buttons
581 themeList()->Change.connect([this]{ onThemeChange(); });
582 themeList()->DoubleClickItem.connect([this]{ onSelectTheme(); });
583 selectTheme()->Click.connect([this]{ onSelectTheme(); });
584 openThemeFolder()->Click.connect([this]{ onOpenThemeFolder(); });
585
586 // Extensions buttons
587 extensionsList()->Change.connect([this]{ onExtensionChange(); });
588 addExtension()->Click.connect([this]{ onAddExtension(); });
589 disableExtension()->Click.connect([this]{ onDisableExtension(); });
590 uninstallExtension()->Click.connect([this]{ onUninstallExtension(); });
591 openExtensionFolder()->Click.connect([this]{ onOpenExtensionFolder(); });
592
593 // Apply button
594 buttonApply()->Click.connect([this]{ onApply(); });
595
596 onChangeBgScope();
597 onChangeGridScope();
598 sectionListbox()->selectIndex(m_curSection);
599
600 // Refill languages combobox when extensions are enabled/disabled
601 m_extLanguagesChanges =
602 App::instance()->extensions().LanguagesChange.connect(
603 [this]{ refillLanguages(); });
604
605 // Reload themes when extensions are enabled/disabled
606 m_extThemesChanges =
607 App::instance()->extensions().ThemesChange.connect(
608 [this]{ reloadThemes(); });
609 }
610
611 bool ok() {
612 return (closer() == buttonOk());
613 }
614
615 void saveConfig() {
616 // Save preferences in widgets that are bound to options automatically
617 {
618 Message msg(kSavePreferencesMessage);
619 msg.setPropagateToChildren(true);
620 sendMessage(&msg);
621 }
622
623 // Share crashdb
624#if ENABLE_SENTRY
625 if (shareCrashdb()->isSelected())
626 Sentry::giveConsent();
627 else
628 Sentry::revokeConsent();
629 App::instance()->mainWindow()->updateConsentCheckbox();
630#endif
631
632 // Update language
633 Strings::instance()->setCurrentLanguage(
634 language()->getItemText(language()->getSelectedItemIndex()));
635
636 m_globPref.timeline.firstFrame(firstFrame()->textInt());
637 m_pref.general.showFullPath(showFullPath()->isSelected());
638 m_pref.saveFile.defaultExtension(getExtension(defaultExtension()));
639 m_pref.exportFile.imageDefaultExtension(getExtension(exportImageDefaultExtension()));
640 m_pref.exportFile.animationDefaultExtension(getExtension(exportAnimationDefaultExtension()));
641 m_pref.spriteSheet.defaultExtension(getExtension(exportSpriteSheetDefaultExtension()));
642 {
643 const int limit = recentFiles()->getValue();
644 m_pref.general.recentItems(limit);
645 App::instance()->recentFiles()->setLimit(limit);
646 }
647
648 bool expandOnMouseover = expandMenubarOnMouseover()->isSelected();
649 m_pref.general.expandMenubarOnMouseover(expandOnMouseover);
650 ui::MenuBar::setExpandOnMouseover(expandOnMouseover);
651
652 std::string warnings;
653
654 double newPeriod = base::convert_to<double>(dataRecoveryPeriod()->getValue());
655 if (enableDataRecovery()->isSelected() != m_pref.general.dataRecovery() ||
656 newPeriod != m_pref.general.dataRecoveryPeriod()) {
657 m_pref.general.dataRecovery(enableDataRecovery()->isSelected());
658 m_pref.general.dataRecoveryPeriod(newPeriod);
659
660 warnings += "<<- " + Strings::alerts_restart_by_preferences_save_recovery_data_period();
661 }
662
663 int newLifespan = base::convert_to<int>(keepEditedSpriteDataFor()->getValue());
664 if (keepEditedSpriteData()->isSelected() != m_pref.general.keepEditedSpriteData() ||
665 newLifespan != m_pref.general.keepEditedSpriteDataFor()) {
666 m_pref.general.keepEditedSpriteData(keepEditedSpriteData()->isSelected());
667 m_pref.general.keepEditedSpriteDataFor(newLifespan);
668
669 warnings += "<<- " + Strings::alerts_restart_by_preferences_keep_edited_sprite_data_lifespan();
670 }
671
672 double newKeepClosed = base::convert_to<double>(keepClosedSpriteOnMemoryFor()->getValue());
673 if (keepClosedSpriteOnMemory()->isSelected() != m_pref.general.keepClosedSpriteOnMemory() ||
674 newKeepClosed != m_pref.general.keepClosedSpriteOnMemoryFor()) {
675 m_pref.general.keepClosedSpriteOnMemory(keepClosedSpriteOnMemory()->isSelected());
676 m_pref.general.keepClosedSpriteOnMemoryFor(newKeepClosed);
677
678 warnings += "<<- " + Strings::alerts_restart_by_preferences_keep_closed_sprite_on_memory_for();
679 }
680
681 m_pref.editor.zoomFromCenterWithWheel(zoomFromCenterWithWheel()->isSelected());
682 m_pref.editor.zoomFromCenterWithKeys(zoomFromCenterWithKeys()->isSelected());
683 m_pref.editor.showScrollbars(showScrollbars()->isSelected());
684 m_pref.editor.autoScroll(autoScroll()->isSelected());
685 m_pref.editor.straightLinePreview(straightLinePreview()->isSelected());
686 m_pref.eyedropper.discardBrush(discardBrush()->isSelected());
687 m_pref.editor.rightClickMode(static_cast<app::gen::RightClickMode>(rightClickBehavior()->getSelectedItemIndex()));
688 if (m_samplingSelector)
689 m_samplingSelector->save();
690 m_pref.cursor.paintingCursorType(static_cast<app::gen::PaintingCursorType>(paintingCursorType()->getSelectedItemIndex()));
691 m_pref.cursor.cursorColor(cursorColor()->getColor());
692 m_pref.cursor.brushPreview(static_cast<app::gen::BrushPreview>(brushPreview()->getSelectedItemIndex()));
693 m_pref.cursor.useNativeCursor(nativeCursor()->isSelected());
694 m_pref.cursor.cursorScale(base::convert_to<int>(cursorScale()->getValue()));
695 m_pref.selection.autoOpaque(autoOpaque()->isSelected());
696 m_pref.selection.keepSelectionAfterClear(keepSelectionAfterClear()->isSelected());
697 m_pref.selection.autoShowSelectionEdges(autoShowSelectionEdges()->isSelected());
698 m_pref.selection.moveEdges(moveEdges()->isSelected());
699 m_pref.selection.modifiersDisableHandles(modifiersDisableHandles()->isSelected());
700 m_pref.selection.moveOnAddMode(moveOnAddMode()->isSelected());
701 m_pref.guides.layerEdgesColor(layerEdgesColor()->getColor());
702 m_pref.guides.autoGuidesColor(autoGuidesColor()->getColor());
703 m_pref.slices.defaultColor(defaultSliceColor()->getColor());
704
705 m_pref.color.workingRgbSpace(
706 workingRgbCs()->getItemText(
707 workingRgbCs()->getSelectedItemIndex()));
708 m_pref.color.filesWithProfile(
709 filesWithCsMap[filesWithCs()->getSelectedItemIndex()]);
710 m_pref.color.missingProfile(
711 missingCsMap[missingCs()->getSelectedItemIndex()]);
712
713 int winCs = windowCs()->getSelectedItemIndex();
714 switch (winCs) {
715 case 0:
716 m_pref.color.windowProfile(gen::WindowColorProfile::MONITOR);
717 break;
718 case 1:
719 m_pref.color.windowProfile(gen::WindowColorProfile::SRGB);
720 break;
721 default: {
722 m_pref.color.windowProfile(gen::WindowColorProfile::SPECIFIC);
723
724 std::string name;
725 int j = 2;
726 for (auto& cs : m_colorSpaces) {
727 // We add ICC profiles only
728 auto gfxCs = cs->gfxColorSpace();
729 if (gfxCs->type() != gfx::ColorSpace::ICC)
730 continue;
731
732 if (j == winCs) {
733 name = gfxCs->name();
734 break;
735 }
736 ++j;
737 }
738
739 m_pref.color.windowProfileName(name);
740 break;
741 }
742 }
743 update_windows_color_profile_from_preferences();
744
745 // Change sprite grid bounds
746 if (m_context && m_context->activeDocument()) {
747 ContextWriter writer(m_context);
748 Tx tx(m_context, Strings::commands_GridSettings(), ModifyDocument);
749 tx(new cmd::SetGridBounds(writer.sprite(), gridBounds()));
750 tx.commit();
751 }
752
753 m_curPref->show.grid(gridVisible()->isSelected());
754 m_curPref->grid.bounds(gridBounds());
755 m_curPref->grid.color(gridColor()->getColor());
756 m_curPref->grid.opacity(gridOpacity()->getValue());
757 m_curPref->grid.autoOpacity(gridAutoOpacity()->isSelected());
758
759 m_curPref->show.pixelGrid(pixelGridVisible()->isSelected());
760 m_curPref->pixelGrid.color(pixelGridColor()->getColor());
761 m_curPref->pixelGrid.opacity(pixelGridOpacity()->getValue());
762 m_curPref->pixelGrid.autoOpacity(pixelGridAutoOpacity()->isSelected());
763
764 m_curPref->bg.type(app::gen::BgType(checkeredBgSize()->getSelectedItemIndex()));
765 if (m_curPref->bg.type() == app::gen::BgType::CHECKERED_CUSTOM) {
766 m_curPref->bg.size(gfx::Size(
767 checkeredBgCustomW()->textInt(),
768 checkeredBgCustomH()->textInt()));
769 }
770 m_curPref->bg.zoom(checkeredBgZoom()->isSelected());
771 m_curPref->bg.color1(checkeredBgColor1()->getColor());
772 m_curPref->bg.color2(checkeredBgColor2()->getColor());
773
774 // Alerts preferences
775 m_pref.openFile.openSequence(gen::SequenceDecision(openSequence()->getSelectedItemIndex()));
776
777 int undo_size_limit_value;
778 undo_size_limit_value = undoSizeLimit()->textInt();
779 undo_size_limit_value = std::clamp(undo_size_limit_value, 0, 999999);
780
781 m_pref.undo.sizeLimit(undo_size_limit_value);
782 m_pref.undo.gotoModified(undoGotoModified()->isSelected());
783 m_pref.undo.allowNonlinearHistory(undoAllowNonlinearHistory()->isSelected());
784
785 // Experimental features
786 m_pref.experimental.useNativeClipboard(nativeClipboard()->isSelected());
787 m_pref.experimental.useNativeFileDialog(nativeFileDialog()->isSelected());
788 m_pref.experimental.flashLayer(flashLayer()->isSelected());
789 m_pref.experimental.nonactiveLayersOpacity(nonactiveLayersOpacity()->getValue());
790 m_pref.quantization.rgbmapAlgorithm(m_rgbmapAlgorithmSelector.algorithm());
791
792#ifdef _WIN32
793 {
794 os::TabletAPI tabletAPI = os::TabletAPI::Default;
795 std::string tabletStr;
796 bool wintabState = false;
797
798 if (tabletApiWindowsPointer()->isSelected()) {
799 tabletAPI = os::TabletAPI::WindowsPointerInput;
800 tabletStr = "pointer";
801 }
802 else if (tabletApiWintabSystem()->isSelected()) {
803 tabletAPI = os::TabletAPI::Wintab;
804 tabletStr = "wintab";
805 wintabState = true;
806 }
807 else if (tabletApiWintabDirect()->isSelected()) {
808 tabletAPI = os::TabletAPI::WintabPackets;
809 tabletStr = "wintab_packets";
810 wintabState = true;
811 }
812
813 m_pref.tablet.api(tabletStr);
814 m_pref.experimental.loadWintabDriver(wintabState);
815
816 manager()->display()->nativeWindow()
817 ->setInterpretOneFingerGestureAsMouseMovement(
818 oneFingerAsMouseMovement()->isSelected());
819
820 os::instance()->setTabletAPI(tabletAPI);
821 }
822#endif
823
824 ui::set_use_native_cursors(m_pref.cursor.useNativeCursor());
825 ui::set_mouse_cursor_scale(m_pref.cursor.cursorScale());
826
827 bool reset_screen = false;
828 int newScreenScale = base::convert_to<int>(screenScale()->getValue());
829 if (newScreenScale != m_pref.general.screenScale()) {
830 m_pref.general.screenScale(newScreenScale);
831 reset_screen = true;
832 }
833
834 int newUIScale = base::convert_to<int>(uiScale()->getValue());
835 if (newUIScale != m_pref.general.uiScale()) {
836 m_pref.general.uiScale(newUIScale);
837 ui::set_theme(ui::get_theme(),
838 newUIScale);
839 reset_screen = true;
840 }
841
842 bool newGpuAccel = gpuAcceleration()->isSelected();
843 if (newGpuAccel != m_pref.general.gpuAcceleration()) {
844 m_pref.general.gpuAcceleration(newGpuAccel);
845 reset_screen = true;
846 }
847
848 if (os::instance()->menus() &&
849 m_pref.general.showMenuBar() != showMenuBar()->isSelected()) {
850 m_pref.general.showMenuBar(showMenuBar()->isSelected());
851 }
852
853 bool newShowHome = showHome()->isSelected();
854 if (newShowHome != m_pref.general.showHome())
855 m_pref.general.showHome(newShowHome);
856
857 m_pref.save();
858
859 if (!warnings.empty()) {
860 ui::Alert::show(
861 fmt::format(Strings::alerts_restart_by_preferences(),
862 warnings));
863 }
864
865 // Probably it's safe to switch this flag in runtime
866 if (m_pref.experimental.multipleWindows() != ui::get_multiple_displays())
867 ui::set_multiple_displays(m_pref.experimental.multipleWindows());
868
869 if (reset_screen)
870 updateScreenScaling();
871 }
872
873 void restoreTheme() {
874 if (m_pref.theme.selected() != m_restoreThisTheme) {
875 setUITheme(m_restoreThisTheme, false);
876
877 // Restore UI & Screen Scaling
878 if (m_restoreUIScaling != m_pref.general.uiScale()) {
879 m_pref.general.uiScale(m_restoreUIScaling);
880 ui::set_theme(ui::get_theme(), m_restoreUIScaling);
881 }
882
883 if (m_restoreScreenScaling != m_pref.general.screenScale()) {
884 m_pref.general.screenScale(m_restoreScreenScaling);
885 updateScreenScaling();
886 }
887 }
888 }
889
890 bool showDialogToInstallExtension(const std::string& filename) {
891 for (Widget* item : sectionListbox()->children()) {
892 if (auto listItem = dynamic_cast<const ListItem*>(item)) {
893 if (listItem->getValue() == kSectionExtensionsId) {
894 sectionListbox()->selectChild(item);
895 break;
896 }
897 }
898 }
899
900 // Install?
901 if (ui::Alert::show(
902 fmt::format(Strings::alerts_install_extension(), filename)) != 1)
903 return false;
904
905 installExtension(filename);
906 return true;
907 }
908
909private:
910
911 void fillThemeVariants() {
912 ButtonSet* list = nullptr;
913 for (Extension* ext : App::instance()->extensions()) {
914 if (ext->isCurrentTheme()) {
915 // Number of variants
916 int c = 0;
917 for (auto it : ext->themes()) {
918 if (!it.second.variant.empty())
919 ++c;
920 }
921
922 if (c >= 2) {
923 list = new ButtonSet(c);
924 for (auto it : ext->themes()) {
925 if (!it.second.variant.empty()) {
926 auto item = list->addItem(
927 new ThemeVariantItem(this, it.first, it.second.variant));
928
929 if (it.first == Preferences::instance().theme.selected())
930 list->setSelectedItem(item, false);
931 }
932 }
933 }
934 break;
935 }
936 }
937 if (list) {
938 themeVariants()->addChild(list);
939 }
940 if (m_themeVars) {
941 themeVariants()->removeChild(m_themeVars);
942 m_themeVars->deferDelete();
943 }
944 m_themeVars = list;
945 }
946
947 void fillExtensionsCombobox(ui::ComboBox* combobox,
948 const std::string& defExt) {
949 base::paths exts = get_writable_extensions();
950 for (const auto& e : exts) {
951 int index = combobox->addItem(e);
952 if (base::utf8_icmp(e, defExt) == 0)
953 combobox->setSelectedItemIndex(index);
954 }
955 }
956
957 std::string getExtension(ui::ComboBox* combobox) {
958 Widget* defExt = combobox->getSelectedItem();
959 ASSERT(defExt);
960 return (defExt ? defExt->text(): std::string());
961 }
962
963 void selectScalingItems() {
964 // Screen/UI Scale
965 screenScale()->setSelectedItemIndex(
966 screenScale()->findItemIndexByValue(
967 base::convert_to<std::string>(m_pref.general.screenScale())));
968
969 uiScale()->setSelectedItemIndex(
970 uiScale()->findItemIndexByValue(
971 base::convert_to<std::string>(m_pref.general.uiScale())));
972 }
973
974 void updateScreenScaling() {
975 ui::Manager* manager = ui::Manager::getDefault();
976 os::instance()->setGpuAcceleration(m_pref.general.gpuAcceleration());
977 manager->updateAllDisplaysWithNewScale(m_pref.general.screenScale());
978 }
979
980 void onApply() {
981 saveConfig();
982 m_restoreThisTheme = m_pref.theme.selected();
983 m_restoreScreenScaling = m_pref.general.screenScale();
984 m_restoreUIScaling = m_pref.general.uiScale();
985 }
986
987 void onNativeCursorChange() {
988 bool state =
989 // If the platform supports custom cursors...
990 (((int(os::instance()->capabilities()) &
991 int(os::Capabilities::CustomMouseCursor)) != 0) &&
992 // If the native cursor option is not selec
993 !nativeCursor()->isSelected());
994
995 cursorScaleLabel()->setEnabled(state);
996 cursorScale()->setEnabled(state);
997 }
998
999 void onChangeSection() {
1000 ListItem* item = static_cast<ListItem*>(sectionListbox()->getSelectedChild());
1001 if (!item)
1002 return;
1003
1004 m_curSection = sectionListbox()->getSelectedIndex();
1005
1006 // General section
1007 if (item->getValue() == kSectionGeneralId)
1008 loadLanguages();
1009 // Background section
1010 else if (item->getValue() == kSectionBgId)
1011 onChangeBgScope();
1012 // Grid section
1013 else if (item->getValue() == kSectionGridId)
1014 onChangeGridScope();
1015 // Load themes
1016 else if (item->getValue() == kSectionThemeId)
1017 loadThemes();
1018 // Load extension
1019 else if (item->getValue() == kSectionExtensionsId)
1020 loadExtensions();
1021
1022 panel()->showChild(findChild(item->getValue().c_str()));
1023 }
1024
1025 void onClearRecentFiles() {
1026 App::instance()->recentFiles()->clear();
1027 }
1028
1029 void onColorManagement() {
1030 const bool state = colorManagement()->isSelected();
1031 windowCsLabel()->setEnabled(state);
1032 windowCs()->setEnabled(state);
1033 workingRgbCsLabel()->setEnabled(state);
1034 workingRgbCs()->setEnabled(state);
1035 filesWithCsLabel()->setEnabled(state);
1036 filesWithCs()->setEnabled(state);
1037 missingCsLabel()->setEnabled(state);
1038 missingCs()->setEnabled(state);
1039 }
1040
1041 void onResetColorManagement() {
1042 updateColorProfileControls(m_pref.color.manage.defaultValue(),
1043 m_pref.color.windowProfile.defaultValue(),
1044 m_pref.color.windowProfileName.defaultValue(),
1045 m_pref.color.workingRgbSpace.defaultValue(),
1046 m_pref.color.filesWithProfile.defaultValue(),
1047 m_pref.color.missingProfile.defaultValue());
1048 }
1049
1050 void updateColorProfileControls(const bool manage,
1051 const app::gen::WindowColorProfile& windowProfile,
1052 const std::string& windowProfileName,
1053 const std::string& workingRgbSpace,
1054 const app::gen::ColorProfileBehavior& filesWithProfile,
1055 const app::gen::ColorProfileBehavior& missingProfile) {
1056 colorManagement()->setSelected(manage);
1057
1058 // Window color profile
1059 {
1060 int i = 0;
1061 if (windowProfile == gen::WindowColorProfile::MONITOR)
1062 i = 0;
1063 else if (windowProfile == gen::WindowColorProfile::SRGB)
1064 i = 1;
1065
1066 // Delete previous added items in the combobox for each display
1067 // (we'll re-add them below).
1068 while (windowCs()->getItem(2))
1069 windowCs()->deleteItem(2);
1070
1071 int j = 2;
1072 for (auto& cs : m_colorSpaces) {
1073 // We add ICC profiles only
1074 auto gfxCs = cs->gfxColorSpace();
1075 if (gfxCs->type() != gfx::ColorSpace::ICC)
1076 continue;
1077
1078 auto name = gfxCs->name();
1079 windowCs()->addItem(fmt::format(m_templateTextForDisplayCS, name));
1080 if (windowProfile == gen::WindowColorProfile::SPECIFIC &&
1081 windowProfileName == name) {
1082 i = j;
1083 }
1084 ++j;
1085 }
1086 windowCs()->setSelectedItemIndex(i);
1087 }
1088
1089 // Working color profile
1090 for (auto child : *workingRgbCs()) {
1091 if (child->text() == workingRgbSpace) {
1092 workingRgbCs()->setSelectedItem(child);
1093 break;
1094 }
1095 }
1096
1097 for (int i=0; i<sizeof(filesWithCsMap)/sizeof(filesWithCsMap[0]); ++i) {
1098 if (filesWithCsMap[i] == filesWithProfile) {
1099 filesWithCs()->setSelectedItemIndex(i);
1100 break;
1101 }
1102 }
1103
1104 for (int i=0; i<sizeof(missingCsMap)/sizeof(missingCsMap[0]); ++i) {
1105 if (missingCsMap[i] == missingProfile) {
1106 missingCs()->setSelectedItemIndex(i);
1107 break;
1108 }
1109 }
1110
1111 onColorManagement();
1112 }
1113
1114 void onResetAlerts() {
1115 fileFormatDoesntSupportAlert()->resetWithDefaultValue();
1116 exportAnimationInSequenceAlert()->resetWithDefaultValue();
1117 overwriteFilesOnExportAlert()->resetWithDefaultValue();
1118 overwriteFilesOnExportSpriteSheetAlert()->resetWithDefaultValue();
1119 advancedModeAlert()->resetWithDefaultValue();
1120 invalidFgBgColorAlert()->resetWithDefaultValue();
1121 runScriptAlert()->resetWithDefaultValue();
1122 cssOptionsAlert()->resetWithDefaultValue();
1123 gifOptionsAlert()->resetWithDefaultValue();
1124 jpegOptionsAlert()->resetWithDefaultValue();
1125 svgOptionsAlert()->resetWithDefaultValue();
1126 tgaOptionsAlert()->resetWithDefaultValue();
1127 }
1128
1129 void onChangeBgScope() {
1130 const int item = bgScope()->getSelectedItemIndex();
1131 switch (item) {
1132 case 0: m_curPref = &m_globPref; break;
1133 case 1: m_curPref = &m_docPref; break;
1134 }
1135
1136 checkeredBgSize()->setSelectedItemIndex(int(m_curPref->bg.type()));
1137 checkeredBgZoom()->setSelected(m_curPref->bg.zoom());
1138 checkeredBgColor1()->setColor(m_curPref->bg.color1());
1139 checkeredBgColor2()->setColor(m_curPref->bg.color2());
1140
1141 onCheckeredBgSizeChange();
1142 }
1143
1144 void onCheckeredBgSizeChange() {
1145 if (checkeredBgSize()->getSelectedItemIndex() == int(app::gen::BgType::CHECKERED_CUSTOM)) {
1146 checkeredBgCustomW()->setTextf("%d", m_curPref->bg.size().w);
1147 checkeredBgCustomH()->setTextf("%d", m_curPref->bg.size().h);
1148 checkeredBgCustomW()->setVisible(true);
1149 checkeredBgCustomH()->setVisible(true);
1150 }
1151 else {
1152 checkeredBgCustomW()->setVisible(false);
1153 checkeredBgCustomH()->setVisible(false);
1154 }
1155 sectionBg()->layout();
1156 }
1157
1158 void onChangeGridScope() {
1159 int item = gridScope()->getSelectedItemIndex();
1160
1161 switch (item) {
1162 case 0: m_curPref = &m_globPref; break;
1163 case 1: m_curPref = &m_docPref; break;
1164 }
1165
1166 gridVisible()->setSelected(m_curPref->show.grid());
1167 gridX()->setTextf("%d", m_curPref->grid.bounds().x);
1168 gridY()->setTextf("%d", m_curPref->grid.bounds().y);
1169 gridW()->setTextf("%d", m_curPref->grid.bounds().w);
1170 gridH()->setTextf("%d", m_curPref->grid.bounds().h);
1171
1172 gridColor()->setColor(m_curPref->grid.color());
1173 gridOpacity()->setValue(m_curPref->grid.opacity());
1174 gridAutoOpacity()->setSelected(m_curPref->grid.autoOpacity());
1175
1176 pixelGridVisible()->setSelected(m_curPref->show.pixelGrid());
1177 pixelGridColor()->setColor(m_curPref->pixelGrid.color());
1178 pixelGridOpacity()->setValue(m_curPref->pixelGrid.opacity());
1179 pixelGridAutoOpacity()->setSelected(m_curPref->pixelGrid.autoOpacity());
1180 }
1181
1182 void onResetBg() {
1183 DocumentPreferences& pref = m_globPref;
1184
1185 // Reset global preferences (use default values specified in pref.xml)
1186 if (m_curPref == &m_globPref) {
1187 checkeredBgSize()->setSelectedItemIndex(int(pref.bg.type.defaultValue()));
1188 checkeredBgCustomW()->setVisible(false);
1189 checkeredBgCustomH()->setVisible(false);
1190 checkeredBgZoom()->setSelected(pref.bg.zoom.defaultValue());
1191 checkeredBgColor1()->setColor(pref.bg.color1.defaultValue());
1192 checkeredBgColor2()->setColor(pref.bg.color2.defaultValue());
1193 }
1194 // Reset document preferences with global settings
1195 else {
1196 checkeredBgSize()->setSelectedItemIndex(int(pref.bg.type()));
1197 checkeredBgZoom()->setSelected(pref.bg.zoom());
1198 checkeredBgColor1()->setColor(pref.bg.color1());
1199 checkeredBgColor2()->setColor(pref.bg.color2());
1200 }
1201 }
1202
1203 void onResetGrid() {
1204 DocumentPreferences& pref = m_globPref;
1205
1206 // Reset global preferences (use default values specified in pref.xml)
1207 if (m_curPref == &m_globPref) {
1208 gridVisible()->setSelected(pref.show.grid.defaultValue());
1209 gridX()->setTextf("%d", pref.grid.bounds.defaultValue().x);
1210 gridY()->setTextf("%d", pref.grid.bounds.defaultValue().y);
1211 gridW()->setTextf("%d", pref.grid.bounds.defaultValue().w);
1212 gridH()->setTextf("%d", pref.grid.bounds.defaultValue().h);
1213
1214 gridColor()->setColor(pref.grid.color.defaultValue());
1215 gridOpacity()->setValue(pref.grid.opacity.defaultValue());
1216 gridAutoOpacity()->setSelected(pref.grid.autoOpacity.defaultValue());
1217
1218 pixelGridVisible()->setSelected(pref.show.pixelGrid.defaultValue());
1219 pixelGridColor()->setColor(pref.pixelGrid.color.defaultValue());
1220 pixelGridOpacity()->setValue(pref.pixelGrid.opacity.defaultValue());
1221 pixelGridAutoOpacity()->setSelected(pref.pixelGrid.autoOpacity.defaultValue());
1222 }
1223 // Reset document preferences with global settings
1224 else {
1225 gridVisible()->setSelected(pref.show.grid());
1226 gridX()->setTextf("%d", pref.grid.bounds().x);
1227 gridY()->setTextf("%d", pref.grid.bounds().y);
1228 gridW()->setTextf("%d", pref.grid.bounds().w);
1229 gridH()->setTextf("%d", pref.grid.bounds().h);
1230
1231 gridColor()->setColor(pref.grid.color());
1232 gridOpacity()->setValue(pref.grid.opacity());
1233 gridAutoOpacity()->setSelected(pref.grid.autoOpacity());
1234
1235 pixelGridVisible()->setSelected(pref.show.pixelGrid());
1236 pixelGridColor()->setColor(pref.pixelGrid.color());
1237 pixelGridOpacity()->setValue(pref.pixelGrid.opacity());
1238 pixelGridAutoOpacity()->setSelected(pref.pixelGrid.autoOpacity());
1239 }
1240 }
1241
1242 void onLocateCrashFolder() {
1243 app::launcher::open_folder(
1244 base::get_file_path(App::instance()->memoryDumpFilename()));
1245 }
1246
1247 void onLocateConfigFile() {
1248 app::launcher::open_folder(app::main_config_filename());
1249 }
1250
1251 void onLimitUndoCheck() {
1252 if (limitUndo()->isSelected()) {
1253 undoSizeLimit()->setEnabled(true);
1254 undoSizeLimit()->setTextf("%d", m_pref.undo.sizeLimit());
1255 }
1256 else {
1257 undoSizeLimit()->setEnabled(false);
1258 undoSizeLimit()->setText(kInfiniteSymbol);
1259 }
1260 }
1261
1262 void refillLanguages() {
1263 language()->deleteAllItems();
1264 loadLanguages();
1265 }
1266
1267 void loadLanguages() {
1268 // Languages already loaded
1269 if (language()->getItemCount() > 0)
1270 return;
1271
1272 Strings* strings = Strings::instance();
1273 std::string curLang = strings->currentLanguage();
1274 for (const std::string& lang : strings->availableLanguages()) {
1275 int i = language()->addItem(lang);
1276 if (lang == curLang)
1277 language()->setSelectedItemIndex(i);
1278 }
1279 }
1280
1281 void reloadThemes() {
1282 while (auto child = themeList()->lastChild())
1283 delete child;
1284
1285 loadThemes();
1286 }
1287
1288 void loadThemes() {
1289 // Themes already loaded
1290 if (themeList()->getItemsCount() > 0)
1291 return;
1292
1293 auto theme = skin::SkinTheme::get(this);
1294 auto userFolder = userThemeFolder();
1295 auto folders = themeFolders();
1296 std::sort(folders.begin(), folders.end());
1297 const auto& selectedPath = theme->path();
1298
1299 bool first = true;
1300 for (const auto& path : folders) {
1301 auto files = base::list_files(path);
1302
1303 // Only one empty theme folder: the user folder
1304 if (files.empty() && path != userFolder)
1305 continue;
1306
1307 std::sort(files.begin(), files.end());
1308 for (auto& fn : files) {
1309 std::string fullPath =
1310 base::normalize_path(
1311 base::join_path(path, fn));
1312 if (!base::is_directory(fullPath))
1313 continue;
1314
1315 if (first) {
1316 first = false;
1317 themeList()->addChild(
1318 new SeparatorInView(base::normalize_path(path), HORIZONTAL));
1319 }
1320
1321 ThemeItem* item = new ThemeItem(fn, fullPath);
1322 themeList()->addChild(item);
1323
1324 // Selected theme
1325 if (fullPath == selectedPath)
1326 themeList()->selectChild(item);
1327 }
1328 }
1329
1330 // Themes from extensions
1331 first = true;
1332 for (auto ext : App::instance()->extensions()) {
1333 if (!ext->isEnabled())
1334 continue;
1335
1336 if (ext->themes().empty())
1337 continue;
1338
1339 if (first) {
1340 first = false;
1341 themeList()->addChild(
1342 new SeparatorInView(Strings::options_extension_themes(), HORIZONTAL));
1343 }
1344
1345 for (auto it : ext->themes()) {
1346 ThemeItem* item = new ThemeItem(it.first,
1347 it.second.path,
1348 ext->displayName(),
1349 it.second.variant);
1350 themeList()->addChild(item);
1351
1352 // Selected theme
1353 if (it.second.path == selectedPath)
1354 themeList()->selectChild(item);
1355 }
1356 }
1357
1358 themeList()->layout();
1359 }
1360
1361 void loadExtensionsByCategory(const Extension::Category category,
1362 const std::string& label) {
1363 bool hasItems = false;
1364 auto sep = new ExtensionCategorySeparator(category, label);
1365 extensionsList()->addChild(sep);
1366 for (auto e : App::instance()->extensions()) {
1367 if (e->category() == category) {
1368 ExtensionItem* item = new ExtensionItem(e);
1369 extensionsList()->addChild(item);
1370 hasItems = true;
1371 }
1372 }
1373 sep->setVisible(hasItems);
1374 }
1375
1376 void loadExtensions() {
1377 // Extensions already loaded
1378 if (extensionsList()->getItemsCount() > 0)
1379 return;
1380
1381 loadExtensionsByCategory(
1382 Extension::Category::Keys,
1383 Strings::options_keys_extensions());
1384
1385 loadExtensionsByCategory(
1386 Extension::Category::Languages,
1387 Strings::options_language_extensions());
1388
1389 loadExtensionsByCategory(
1390 Extension::Category::Themes,
1391 Strings::options_theme_extensions());
1392
1393#ifdef ENABLE_SCRIPTING
1394 loadExtensionsByCategory(
1395 Extension::Category::Scripts,
1396 Strings::options_script_extensions());
1397#endif
1398
1399 loadExtensionsByCategory(
1400 Extension::Category::Palettes,
1401 Strings::options_palette_extensions());
1402
1403 loadExtensionsByCategory(
1404 Extension::Category::DitheringMatrices,
1405 Strings::options_dithering_matrix_extensions());
1406
1407 loadExtensionsByCategory(
1408 Extension::Category::Multiple,
1409 Strings::options_multiple_extensions());
1410
1411 onExtensionChange();
1412 extensionsList()->layout();
1413 }
1414
1415 void onThemeChange() {
1416 ThemeItem* item = dynamic_cast<ThemeItem*>(themeList()->getSelectedChild());
1417 selectTheme()->setEnabled(item && item->canSelect());
1418 openThemeFolder()->setEnabled(item != nullptr);
1419 }
1420
1421 void onSelectTheme() {
1422 ThemeItem* item = dynamic_cast<ThemeItem*>(themeList()->getSelectedChild());
1423 if (item)
1424 setUITheme(item->themeName(), true);
1425 }
1426
1427 void setUITheme(const std::string& themeName,
1428 const bool updateScaling,
1429 const bool recreateVariantsFields = true) {
1430 try {
1431 if (themeName != m_pref.theme.selected()) {
1432 auto theme = skin::SkinTheme::get(this);
1433
1434 // Change theme name from preferences
1435 m_pref.theme.selected(themeName);
1436
1437 // Change the UI theme
1438 ui::set_theme(theme, m_pref.general.uiScale());
1439
1440 // Ask for new scaling
1441 const int newUIScale = theme->preferredUIScaling();
1442 const int newScreenScale = theme->preferredScreenScaling();
1443
1444 if (updateScaling &&
1445 ((newUIScale > 0 && m_pref.general.uiScale() != newUIScale) ||
1446 (newScreenScale > 0 && m_pref.general.screenScale() != newScreenScale))) {
1447 // Ask if the user want to adjust the Screen/UI Scaling
1448 const int result =
1449 ui::Alert::show(
1450 fmt::format(
1451 Strings::alerts_update_screen_ui_scaling_with_theme_values(),
1452 themeName,
1453 100 * m_pref.general.screenScale(),
1454 100 * (newScreenScale > 0 ? newScreenScale: m_pref.general.screenScale()),
1455 100 * m_pref.general.uiScale(),
1456 100 * (newUIScale > 0 ? newUIScale: m_pref.general.uiScale())));
1457
1458 if (result == 1) {
1459 // Preferred UI Scaling factor
1460 if (newUIScale > 0 &&
1461 newUIScale != m_pref.general.uiScale()) {
1462 m_pref.general.uiScale(newUIScale);
1463 ui::set_theme(theme, m_pref.general.uiScale());
1464 }
1465
1466 // Preferred Screen Scaling
1467 if (newScreenScale > 0 &&
1468 newScreenScale != m_pref.general.screenScale()) {
1469 m_pref.general.screenScale(newScreenScale);
1470 updateScreenScaling();
1471 }
1472
1473 selectScalingItems();
1474 }
1475 }
1476
1477 if (recreateVariantsFields)
1478 fillThemeVariants();
1479 }
1480 }
1481 catch (const std::exception& ex) {
1482 Console::showException(ex);
1483 }
1484 }
1485
1486 void onOpenThemeFolder() {
1487 ThemeItem* item = dynamic_cast<ThemeItem*>(themeList()->getSelectedChild());
1488 if (item)
1489 item->openFolder();
1490 }
1491
1492 void onExtensionChange() {
1493 ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
1494 if (item && item->isInstalled()) {
1495 disableExtension()->setText(item->isEnabled() ?
1496 Strings::options_disable_extension():
1497 Strings::options_enable_extension());
1498 disableExtension()->processMnemonicFromText();
1499 disableExtension()->setEnabled(item->isEnabled() ? item->canBeDisabled(): true);
1500 uninstallExtension()->setEnabled(item->canBeUninstalled());
1501 openExtensionFolder()->setEnabled(true);
1502 }
1503 else {
1504 disableExtension()->setEnabled(false);
1505 uninstallExtension()->setEnabled(false);
1506 openExtensionFolder()->setEnabled(false);
1507 }
1508 }
1509
1510 void onAddExtension() {
1511 base::paths exts = { "aseprite-extension", "zip" };
1512 base::paths filename;
1513 if (!app::show_file_selector(
1514 Strings::options_add_extension_title(), "", exts,
1515 FileSelectorType::Open, filename))
1516 return;
1517
1518 ASSERT(!filename.empty());
1519 installExtension(filename.front());
1520 }
1521
1522 void installExtension(const std::string& filename) {
1523 try {
1524 Extensions& exts = App::instance()->extensions();
1525
1526 // Get the extension information from the compressed
1527 // package.json file.
1528 ExtensionInfo info = exts.getCompressedExtensionInfo(filename);
1529
1530 // Check if the extension already exist
1531 for (auto ext : exts) {
1532 if (base::string_to_lower(ext->name()) !=
1533 base::string_to_lower(info.name))
1534 continue;
1535
1536 bool isDowngrade =
1537 base::Version(info.version.c_str()) <
1538 base::Version(ext->version().c_str());
1539
1540 // Uninstall?
1541 if (ui::Alert::show(
1542 fmt::format(
1543 Strings::alerts_update_extension(),
1544 ext->name(),
1545 (isDowngrade ? Strings::alerts_update_extension_downgrade():
1546 Strings::alerts_update_extension_upgrade()),
1547 ext->version(),
1548 info.version)) != 1)
1549 return;
1550
1551 // Uninstall old version
1552 if (ext->canBeUninstalled()) {
1553 exts.uninstallExtension(ext, DeletePluginPref::kNo);
1554
1555 ExtensionItem* item = getItemByExtension(ext);
1556 if (item)
1557 deleteExtensionItem(item);
1558 }
1559 break;
1560 }
1561
1562 Extension* ext = exts.installCompressedExtension(filename, info);
1563
1564 // Enable extension
1565 exts.enableExtension(ext, true);
1566
1567 // Add the new extension in the listbox
1568 ExtensionItem* item = new ExtensionItem(ext);
1569 extensionsList()->addChild(item);
1570 updateCategoryVisibility();
1571 extensionsList()->sortItems(
1572 [](Widget* a, Widget* b){
1573 auto aSep = dynamic_cast<ExtensionCategorySeparator*>(a);
1574 auto bSep = dynamic_cast<ExtensionCategorySeparator*>(b);
1575 auto aItem = dynamic_cast<ExtensionItem*>(a);
1576 auto bItem = dynamic_cast<ExtensionItem*>(b);
1577 auto aCat = (aSep ? aSep->category():
1578 aItem ? aItem->category(): Extension::Category::None);
1579 auto bCat = (bSep ? bSep->category():
1580 bItem ? bItem->category(): Extension::Category::None);
1581 if (aCat < bCat)
1582 return true;
1583 else if (aCat == bCat) {
1584 // There are no two separators with same category.
1585 ASSERT(!(aSep && bSep));
1586
1587 if (aSep && !bSep)
1588 return true;
1589 else if (!aSep && bSep)
1590 return false;
1591 else
1592 return (base::compare_filenames(a->text(), b->text()) < 0);
1593 }
1594 else
1595 return false;
1596 });
1597 extensionsList()->layout();
1598 extensionsList()->selectChild(item);
1599 }
1600 catch (const std::exception& ex) {
1601 Console::showException(ex);
1602 }
1603 }
1604
1605 void onDisableExtension() {
1606 ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
1607 if (item) {
1608 item->enable(!item->isEnabled());
1609 onExtensionChange();
1610 }
1611 }
1612
1613 void onUninstallExtension() {
1614 ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
1615 if (!item)
1616 return;
1617
1618 if (ui::Alert::show(
1619 fmt::format(
1620 Strings::alerts_uninstall_extension_warning(),
1621 item->text())) != 1)
1622 return;
1623
1624 try {
1625 item->uninstall();
1626 deleteExtensionItem(item);
1627 }
1628 catch (const std::exception& ex) {
1629 Console::showException(ex);
1630 }
1631 }
1632
1633 void deleteExtensionItem(ExtensionItem* item) {
1634 // Remove the item from the list
1635 extensionsList()->removeChild(item);
1636 updateCategoryVisibility();
1637 extensionsList()->layout();
1638 item->deferDelete();
1639 }
1640
1641 ExtensionItem* getItemByExtension(Extension* ext) {
1642 for (auto child : extensionsList()->children()) {
1643 ExtensionItem* item = dynamic_cast<ExtensionItem*>(child);
1644 if (item && item->extension() == ext)
1645 return item;
1646 }
1647 return nullptr;
1648 }
1649
1650 void onOpenExtensionFolder() {
1651 ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
1652 if (item)
1653 item->openFolder();
1654 }
1655
1656 void onCursorColorType() {
1657 switch (cursorColorType()->getSelectedItemIndex()) {
1658 case 0:
1659 cursorColor()->setColor(app::Color::fromMask());
1660 cursorColor()->setVisible(false);
1661 break;
1662 case 1:
1663 cursorColor()->setColor(app::Color::fromRgb(0, 0, 0, 255));
1664 cursorColor()->setVisible(true);
1665 break;
1666 }
1667 layout();
1668 }
1669
1670 gfx::Rect gridBounds() const {
1671 return gfx::Rect(gridX()->textInt(), gridY()->textInt(),
1672 gridW()->textInt(), gridH()->textInt());
1673 }
1674
1675 static std::string userThemeFolder() {
1676 ResourceFinder rf;
1677 rf.includeDataDir(skin::SkinTheme::kThemesFolderName);
1678
1679#if 0 // Don't create the user folder to store themes because now we prefer extensions
1680 try {
1681 if (!base::is_directory(rf.defaultFilename()))
1682 base::make_all_directories(rf.defaultFilename());
1683 }
1684 catch (...) {
1685 // Ignore errors
1686 }
1687#endif
1688
1689 return base::normalize_path(rf.defaultFilename());
1690 }
1691
1692 static base::paths themeFolders() {
1693 ResourceFinder rf;
1694 rf.includeDataDir(skin::SkinTheme::kThemesFolderName);
1695
1696 base::paths paths;
1697 while (rf.next())
1698 paths.push_back(base::normalize_path(rf.filename()));
1699 return paths;
1700 }
1701
1702 void updateCategoryVisibility() {
1703 bool visibleCategories[int(Extension::Category::Max)];
1704 for (auto& v : visibleCategories)
1705 v = false;
1706 for (auto w : extensionsList()->children()) {
1707 if (auto item = dynamic_cast<ExtensionItem*>(w)) {
1708 visibleCategories[int(item->category())] = true;
1709 }
1710 }
1711 for (auto w : extensionsList()->children()) {
1712 if (auto sep = dynamic_cast<ExtensionCategorySeparator*>(w))
1713 sep->setVisible(visibleCategories[int(sep->category())]);
1714 }
1715 }
1716
1717#ifdef _WIN32
1718 void onTabletAPIChange() {
1719 if (tabletApiWindowsPointer()->isSelected()) {
1720 loadWintabDriver()->setSelected(false);
1721 loadWintabDriver2()->setSelected(false);
1722 }
1723 else if (tabletApiWintabSystem()->isSelected() ||
1724 tabletApiWintabDirect()->isSelected()) {
1725 loadWintabDriver()->setSelected(true);
1726 loadWintabDriver2()->setSelected(true);
1727 }
1728 }
1729 void onLoadWintabChange(bool state) {
1730 loadWintabDriver()->setSelected(state);
1731 loadWintabDriver2()->setSelected(state);
1732 if (state)
1733 tabletApiWintabSystem()->setSelected(true);
1734 else
1735 tabletApiWindowsPointer()->setSelected(true);
1736 }
1737
1738#endif // _WIN32
1739
1740 Context* m_context;
1741 Preferences& m_pref;
1742 DocumentPreferences& m_globPref;
1743 DocumentPreferences& m_docPref;
1744 DocumentPreferences* m_curPref;
1745 int& m_curSection;
1746 obs::scoped_connection m_extLanguagesChanges;
1747 obs::scoped_connection m_extThemesChanges;
1748 std::string m_restoreThisTheme;
1749 int m_restoreScreenScaling;
1750 int m_restoreUIScaling;
1751 std::vector<os::ColorSpaceRef> m_colorSpaces;
1752 std::string m_templateTextForDisplayCS;
1753 RgbMapAlgorithmSelector m_rgbmapAlgorithmSelector;
1754 ButtonSet* m_themeVars = nullptr;
1755 SamplingSelector* m_samplingSelector = nullptr;
1756};
1757
1758class OptionsCommand : public Command {
1759public:
1760 OptionsCommand();
1761
1762protected:
1763 void onLoadParams(const Params& params) override;
1764 void onExecute(Context* context) override;
1765
1766private:
1767 std::string m_installExtensionFilename;
1768};
1769
1770OptionsCommand::OptionsCommand()
1771 : Command(CommandId::Options(), CmdUIOnlyFlag)
1772{
1773 Preferences& preferences = Preferences::instance();
1774
1775 ui::MenuBar::setExpandOnMouseover(
1776 preferences.general.expandMenubarOnMouseover());
1777}
1778
1779void OptionsCommand::onLoadParams(const Params& params)
1780{
1781 m_installExtensionFilename = params.get("installExtension");
1782}
1783
1784void OptionsCommand::onExecute(Context* context)
1785{
1786 static int curSection = 0;
1787
1788 OptionsWindow window(context, curSection);
1789
1790 // As showDialogToInstallExtension() will show an ui::Alert, we need
1791 // to call this function after window.openWindowInForeground(), so
1792 // the parent window of the alert will be our OptionsWindow (and not
1793 // the main window).
1794 window.Open.connect(
1795 [&]() {
1796 if (!m_installExtensionFilename.empty()) {
1797 if (!window.showDialogToInstallExtension(this->m_installExtensionFilename)) {
1798 window.closeWindow(&window);
1799 return;
1800 }
1801 }
1802 });
1803
1804 window.openWindowInForeground();
1805 if (window.ok())
1806 window.saveConfig();
1807 else
1808 window.restoreTheme();
1809}
1810
1811Command* CommandFactory::createOptionsCommand()
1812{
1813 return new OptionsCommand;
1814}
1815
1816} // namespace app
1817