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/doc.h"
13#include "app/ini_file.h"
14#include "app/pref/preferences.h"
15#include "app/resource_finder.h"
16#include "app/tools/ink.h"
17#include "app/tools/tool.h"
18#include "base/fs.h"
19#include "doc/sprite.h"
20#include "os/system.h"
21#include "ui/system.h"
22
23namespace app {
24
25static Preferences* singleton = nullptr;
26
27// static
28Preferences& Preferences::instance()
29{
30#ifdef _DEBUG
31 // Preferences can be used only from the main UI thread. In other
32 // case access to std::map<> could crash the program.
33 if (ui::UISystem::instance())
34 ui::assert_ui_thread();
35#endif
36
37 ASSERT(singleton);
38 return *singleton;
39}
40
41Preferences::Preferences()
42 : app::gen::GlobalPref("")
43{
44 ASSERT(!singleton);
45 singleton = this;
46
47 // Main configuration file
48 const std::string fn = main_config_filename();
49 ASSERT(!fn.empty());
50
51 // The first time we execute the program, the configuration file
52 // doesn't exist.
53 const bool firstTime = (!base::is_file(fn));
54
55 load();
56
57 // Create a connection with the default RgbMapAlgorithm preferences
58 // to change the default algorithm in the "doc" layer.
59 quantization.rgbmapAlgorithm.AfterChange.connect(
60 [](const doc::RgbMapAlgorithm& newValue){
61 doc::Sprite::SetDefaultRgbMapAlgorithm(newValue);
62 });
63 doc::Sprite::SetDefaultRgbMapAlgorithm(quantization.rgbmapAlgorithm());
64
65 // Create a connection with the default document preferences grid
66 // bounds to sync the default grid bounds for new sprites in the
67 // "doc" layer.
68 auto& defPref = document(nullptr);
69 defPref.grid.bounds.AfterChange.connect(
70 [](const gfx::Rect& newValue){
71 doc::Sprite::SetDefaultGridBounds(newValue);
72 });
73 doc::Sprite::SetDefaultGridBounds(defPref.grid.bounds());
74
75 // Reset confusing defaults for a new instance of the program.
76 defPref.grid.snap(false);
77 if (selection.mode() != gen::SelectionMode::DEFAULT &&
78 selection.mode() != gen::SelectionMode::ADD) {
79 selection.mode(gen::SelectionMode::DEFAULT);
80 }
81
82 // Hide the menu bar depending on:
83 // 1. this is the first run of the program
84 // 2. the native menu bar is available
85 if (firstTime &&
86 os::instance() &&
87 os::instance()->menus()) {
88 general.showMenuBar(false);
89 }
90}
91
92Preferences::~Preferences()
93{
94 // Don't save preferences, this must be done by the client of the
95 // Preferences instance (the App class).
96 //save();
97
98 for (auto& pair : m_tools)
99 delete pair.second;
100
101 for (auto& pair : m_docs)
102 delete pair.second;
103
104 ASSERT(singleton == this);
105 singleton = nullptr;
106}
107
108void Preferences::load()
109{
110 app::gen::GlobalPref::load();
111}
112
113void Preferences::save()
114{
115#ifdef _DEBUG
116 if (ui::UISystem::instance())
117 ui::assert_ui_thread();
118#endif
119 app::gen::GlobalPref::save();
120
121 for (auto& pair : m_tools)
122 pair.second->save();
123
124 for (auto& pair : m_docs)
125 serializeDocPref(pair.first, pair.second, true);
126
127 flush_config_file();
128}
129
130bool Preferences::isSet(OptionBase& opt) const
131{
132 return (get_config_string(opt.section(), opt.id(), nullptr) != nullptr);
133}
134
135ToolPreferences& Preferences::tool(tools::Tool* tool)
136{
137 ASSERT(tool != NULL);
138
139 auto it = m_tools.find(tool->getId());
140 if (it != m_tools.end()) {
141 return *it->second;
142 }
143 else {
144 std::string section = std::string("tool.") + tool->getId();
145 ToolPreferences* toolPref = new ToolPreferences(section);
146
147 // Default size for eraser, blur, etc.
148 if (tool->getInk(0)->isEraser() ||
149 tool->getInk(0)->isEffect()) {
150 toolPref->brush.size.setDefaultValue(8);
151 }
152
153 m_tools[tool->getId()] = toolPref;
154 toolPref->load();
155 return *toolPref;
156 }
157}
158
159DocumentPreferences& Preferences::document(const Doc* doc)
160{
161 auto it = m_docs.find(doc);
162 if (it != m_docs.end()) {
163 return *it->second;
164 }
165 else {
166 // Create a new DocumentPreferences for the given "doc" pointer
167 DocumentPreferences* docPref = new DocumentPreferences("");
168 m_docs[doc] = docPref;
169
170 // Setup the preferences of this document with the default ones
171 // (these preferences will be overwritten in the next statement
172 // loading the preferences from the .ini file of this doc)
173 if (doc) {
174 // The default preferences for this document are the current
175 // defaults for (document=nullptr).
176 DocumentPreferences& defPref = this->document(nullptr);
177 *docPref = defPref;
178
179 // Default values for symmetry
180 docPref->symmetry.xAxis.setValueAndDefault(doc->sprite()->width()/2);
181 docPref->symmetry.yAxis.setValueAndDefault(doc->sprite()->height()/2);
182 }
183
184 // Load specific settings of this document
185 serializeDocPref(doc, docPref, false);
186
187 // Turn off snap to grid setting (it's confusing for most of the
188 // users to load this setting).
189 docPref->grid.snap.setValueAndDefault(false);
190
191 return *docPref;
192 }
193}
194
195void Preferences::resetToolPreferences(tools::Tool* tool)
196{
197 if (tool->prefAlreadyResetFromScript())
198 return;
199 tool->markPrefAlreadyResetFromScript();
200
201 auto it = m_tools.find(tool->getId());
202 if (it != m_tools.end())
203 m_tools.erase(it);
204
205 std::string section = std::string("tool.") + tool->getId();
206 del_config_section(section.c_str());
207
208 // TODO improve this, if we add new sections in pref.xml we have to
209 // update this manually :(
210 del_config_section((section + ".brush").c_str());
211 del_config_section((section + ".spray").c_str());
212 del_config_section((section + ".floodfill").c_str());
213}
214
215void Preferences::removeDocument(Doc* doc)
216{
217 ASSERT(doc);
218
219 auto it = m_docs.find(doc);
220 if (it != m_docs.end()) {
221 serializeDocPref(it->first, it->second, true);
222 delete it->second;
223 m_docs.erase(it);
224 }
225}
226
227void Preferences::onRemoveDocument(Doc* doc)
228{
229 removeDocument(doc);
230}
231
232std::string Preferences::docConfigFileName(const Doc* doc)
233{
234 if (!doc)
235 return "";
236
237 ResourceFinder rf;
238 std::string fn = doc->filename();
239 for (size_t i=0; i<fn.size(); ++i) {
240 if (fn[i] == ' ' || fn[i] == '/' || fn[i] == '\\' || fn[i] == ':' || fn[i] == '.') {
241 fn[i] = '-';
242 }
243 }
244 rf.includeUserDir(("files/" + fn + ".ini").c_str());
245 return rf.getFirstOrCreateDefault();
246}
247
248void Preferences::serializeDocPref(const Doc* doc, app::DocumentPreferences* docPref, bool save)
249{
250 bool flush_config = false;
251
252 if (doc) {
253 // We do nothing if the document isn't associated to a file and we
254 // want to save its specific preferences.
255 if (save && !doc->isAssociatedToFile())
256 return;
257
258 // We always push a new configuration file in the stack to avoid
259 // modifying the default preferences when a document in "doc" is
260 // specified.
261 push_config_state();
262 if (doc->isAssociatedToFile()) {
263 set_config_file(docConfigFileName(doc).c_str());
264 flush_config = true;
265 }
266 }
267
268 if (save) {
269 docPref->save();
270 }
271 else {
272 // Load default preferences, or preferences from .ini file.
273 docPref->load();
274 }
275
276 if (doc) {
277 if (save && flush_config)
278 flush_config_file();
279
280 pop_config_state();
281 }
282}
283
284} // namespace app
285